Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1 @@
|
||||
{"hit":{"dateHit":"2015-08-26T01:32:39+00:00","dateLeft":null,"page":{"id":1,"title":"PageHit","alias":"pagehit","category":null},"redirect":null,"email":null,"lead":{"id":26,"points":10,"color":null,"fields":{"core":{"title":{"id":"1","label":"Title","alias":"title","type":"lookup","group":"core","value":null},"firstname":{"id":"2","label":"First Name","alias":"firstname","type":"text","group":"core","value":null},"lastname":{"id":"3","label":"Last Name","alias":"lastname","type":"text","group":"core","value":null},"company":{"id":"4","label":"Company","alias":"company","type":"lookup","group":"core","value":null},"position":{"id":"5","label":"Position","alias":"position","type":"text","group":"core","value":null},"email":{"id":"6","label":"Email","alias":"email","type":"email","group":"core","value":"email@formsubmit.com"},"phone":{"id":"7","label":"Phone","alias":"phone","type":"tel","group":"core","value":null},"mobile":{"id":"8","label":"Mobile","alias":"mobile","type":"tel","group":"core","value":null},"fax":{"id":"9","label":"Fax","alias":"fax","type":"text","group":"core","value":null},"address1":{"id":"10","label":"Address Line 1","alias":"address1","type":"text","group":"core","value":null},"address2":{"id":"11","label":"Address Line 2","alias":"address2","type":"text","group":"core","value":null},"city":{"id":"12","label":"City","alias":"city","type":"lookup","group":"core","value":null},"state":{"id":"13","label":"State","alias":"state","type":"region","group":"core","value":null},"zipcode":{"id":"14","label":"Zipcode","alias":"zipcode","type":"lookup","group":"core","value":null},"country":{"id":"15","label":"Country","alias":"country","type":"country","group":"core","value":null},"website":{"id":"16","label":"Website","alias":"website","type":"text","group":"core","value":null}},"social":{"twitter":{"id":"17","label":"Twitter","alias":"twitter","type":"text","group":"social","value":null},"facebook":{"id":"18","label":"Facebook","alias":"facebook","type":"text","group":"social","value":null},"skype":{"id":"20","label":"Skype","alias":"skype","type":"text","group":"social","value":null},"instagram":{"id":"21","label":"Instagram","alias":"instagram","type":"text","group":"social","value":null},"foursquare":{"id":"22","label":"Foursquare","alias":"foursquare","type":"text","group":"social","value":null}},"personal":[],"professional":[]}},"ipAddress":{},"country":null,"region":null,"city":null,"isp":null,"organization":null,"code":200,"referer":null,"url":"http:\/\/mautic-gh.com\/pagehit","urlTitle":null,"userAgent":"Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/44.0.2403.157 Safari\/537.36","remoteHost":"localhost","pageLanguage":"en","browserLanguages":["en-US","en;q=0.8"],"trackingId":"833fecc93e16d37baf1530df643b6a8b10714c65","source":null,"sourceId":null}}
|
||||
@@ -0,0 +1,9 @@
|
||||
/* PageBundle */
|
||||
|
||||
.col-page-id {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.page-builder .builder-panel .panel-body {
|
||||
padding: 5px 0;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
//PageBundle
|
||||
Mautic.pageOnLoad = function (container, response) {
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'page.page');
|
||||
}
|
||||
|
||||
if (mQuery(container + ' #page_template').length) {
|
||||
Mautic.toggleBuilderButton(mQuery('#page_template').val() == '');
|
||||
|
||||
// Preload tokens for code mode builder
|
||||
Mautic.getTokens(Mautic.getBuilderTokensMethod(), function(){});
|
||||
Mautic.initSelectTheme(mQuery('#page_template'));
|
||||
}
|
||||
|
||||
// Open the builder directly when saved from the builder
|
||||
if (response && response.inBuilder) {
|
||||
Mautic.launchBuilder('page');
|
||||
Mautic.processBuilderErrors(response);
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.getPageAbTestWinnerForm = function(abKey) {
|
||||
if (abKey && mQuery(abKey).val() && mQuery(abKey).closest('.form-group').hasClass('has-error')) {
|
||||
mQuery(abKey).closest('.form-group').removeClass('has-error');
|
||||
if (mQuery(abKey).next().hasClass('help-block')) {
|
||||
mQuery(abKey).next().remove();
|
||||
}
|
||||
}
|
||||
|
||||
Mautic.activateLabelLoadingIndicator('page_variantSettings_winnerCriteria');
|
||||
|
||||
var pageId = mQuery('#page_sessionId').val();
|
||||
var query = "action=page:getAbTestForm&abKey=" + mQuery(abKey).val() + "&pageId=" + pageId;
|
||||
|
||||
mQuery.ajax({
|
||||
url: mauticAjaxUrl,
|
||||
type: "POST",
|
||||
data: query,
|
||||
dataType: "json",
|
||||
success: function (response) {
|
||||
if (typeof response.html != 'undefined') {
|
||||
if (mQuery('#page_variantSettings_properties').length) {
|
||||
mQuery('#page_variantSettings_properties').replaceWith(response.html);
|
||||
} else {
|
||||
mQuery('#page_variantSettings').append(response.html);
|
||||
}
|
||||
|
||||
if (response.html != '') {
|
||||
Mautic.onPageLoad('#page_variantSettings_properties', response);
|
||||
}
|
||||
}
|
||||
|
||||
Mautic.removeLabelLoadingIndicator();
|
||||
|
||||
},
|
||||
error: function (request, textStatus, errorThrown) {
|
||||
Mautic.processAjaxError(request, textStatus, errorThrown);
|
||||
spinner.remove();
|
||||
},
|
||||
complete: function () {
|
||||
Mautic.removeLabelLoadingIndicator();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
/** This section is only needed once per page if manually copying **/
|
||||
if (typeof MauticPrefCenterLoaded === 'undefined') {
|
||||
var MauticPrefCenterLoaded = true;
|
||||
|
||||
function togglePreferredChannel(channel) {
|
||||
var status = document.getElementById(channel).checked;
|
||||
const fieldsToToggle = [
|
||||
'frequency_number',
|
||||
'frequency_time',
|
||||
'contact_pause_start_date',
|
||||
'contact_pause_end_date',
|
||||
// Do we need the 4 above?
|
||||
'lead_channels_frequency_number',
|
||||
'lead_channels_frequency_time',
|
||||
'lead_channels_contact_pause_start_date',
|
||||
'lead_channels_contact_pause_end_date',
|
||||
];
|
||||
fieldsToToggle.forEach(field => {
|
||||
const element = document.getElementById('lead_contact_frequency_rules_' + field + '_' + channel);
|
||||
|
||||
if (element) {
|
||||
if (status) {
|
||||
element.removeAttribute('disabled');
|
||||
} else {
|
||||
element.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
element.dispatchEvent(new CustomEvent('chosen:updated'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function saveUnsubscribePreferences(formId) {
|
||||
var forms = document.getElementsByName(formId);
|
||||
for (var i = 0; i < forms.length; i++) {
|
||||
if (forms[i].tagName === 'FORM') {
|
||||
forms[i].submit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_page_index' => [
|
||||
'path' => '/pages/{page}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PageController::indexAction',
|
||||
],
|
||||
'mautic_page_action' => [
|
||||
'path' => '/pages/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PageController::executeAction',
|
||||
],
|
||||
'mautic_page_results' => [
|
||||
'path' => '/pages/results/{objectId}/{page}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PageController::resultsAction',
|
||||
],
|
||||
'mautic_page_export' => [
|
||||
'path' => '/pages/results/{objectId}/export/{format}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PageController::exportAction',
|
||||
'defaults' => [
|
||||
'format' => 'csv',
|
||||
],
|
||||
],
|
||||
],
|
||||
'public' => [
|
||||
'mautic_page_tracker' => [
|
||||
'path' => '/mtracking.gif',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PublicController::trackingImageAction',
|
||||
],
|
||||
'mautic_page_tracker_cors' => [
|
||||
'path' => '/mtc/event',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PublicController::trackingAction',
|
||||
],
|
||||
'mautic_page_tracker_getcontact' => [
|
||||
'path' => '/mtc',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PublicController::getContactIdAction',
|
||||
],
|
||||
'mautic_url_redirect' => [
|
||||
'path' => '/r/{redirectId}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PublicController::redirectAction',
|
||||
],
|
||||
'mautic_page_redirect' => [
|
||||
'path' => '/redirect/{redirectId}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PublicController::redirectAction',
|
||||
],
|
||||
'mautic_page_preview' => [
|
||||
'path' => '/page/preview/{id}/{objectType}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PublicController::previewAction',
|
||||
'defaults' => ['objectType' => null],
|
||||
],
|
||||
],
|
||||
'api' => [
|
||||
'mautic_api_pagesstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'pages',
|
||||
'path' => '/pages',
|
||||
'controller' => Mautic\PageBundle\Controller\Api\PageApiController::class,
|
||||
],
|
||||
],
|
||||
'catchall' => [
|
||||
'mautic_page_public' => [
|
||||
'path' => '/{slug}',
|
||||
'controller' => 'Mautic\PageBundle\Controller\PublicController::indexAction',
|
||||
'requirements' => [
|
||||
'slug' => '^(?!(_(profiler|wdt)|css|images|js|favicon.ico|apps/bundles/|plugins/)).+',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'menu' => [
|
||||
'main' => [
|
||||
'items' => [
|
||||
'mautic.page.pages' => [
|
||||
'route' => 'mautic_page_index',
|
||||
'access' => ['page:pages:viewown', 'page:pages:viewother'],
|
||||
'parent' => 'mautic.core.components',
|
||||
'priority' => 100,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'categories' => [
|
||||
'page' => [
|
||||
'class' => Mautic\PageBundle\Entity\Page::class,
|
||||
],
|
||||
],
|
||||
|
||||
'services' => [
|
||||
'fixtures' => [
|
||||
'mautic.page.fixture.page_category' => [
|
||||
'class' => Mautic\PageBundle\DataFixtures\ORM\LoadPageCategoryData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.category.model.category'],
|
||||
],
|
||||
'mautic.page.fixture.page' => [
|
||||
'class' => Mautic\PageBundle\DataFixtures\ORM\LoadPageData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.page.model.page'],
|
||||
],
|
||||
'mautic.page.fixture.page_hit' => [
|
||||
'class' => Mautic\PageBundle\DataFixtures\ORM\LoadPageHitData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.page.model.page'],
|
||||
],
|
||||
],
|
||||
'other' => [
|
||||
'mautic.page.helper.token' => [
|
||||
'class' => Mautic\PageBundle\Helper\TokenHelper::class,
|
||||
'arguments' => 'mautic.page.model.page',
|
||||
],
|
||||
'mautic.page.helper.tracking' => [
|
||||
'class' => Mautic\PageBundle\Helper\TrackingHelper::class,
|
||||
'arguments' => [
|
||||
'mautic.tracker.contact',
|
||||
'mautic.cache.provider',
|
||||
'mautic.helper.core_parameters',
|
||||
'request_stack',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'parameters' => [
|
||||
'cat_in_page_url' => false,
|
||||
'google_analytics' => null,
|
||||
'track_contact_by_ip' => false,
|
||||
'track_by_fingerprint' => false,
|
||||
'google_analytics_id' => null,
|
||||
'google_analytics_trackingpage_enabled' => false,
|
||||
'google_analytics_landingpage_enabled' => false,
|
||||
'google_analytics_anonymize_ip' => false,
|
||||
'facebook_pixel_id' => null,
|
||||
'facebook_pixel_trackingpage_enabled' => false,
|
||||
'facebook_pixel_landingpage_enabled' => false,
|
||||
'do_not_track_404_anonymous' => false,
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('Mautic\\PageBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\PageBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
|
||||
$services->get(Mautic\PageBundle\Model\PageModel::class)->call('setCatInUrl', ['%mautic.cat_in_page_url%']);
|
||||
$services->alias('mautic.page.model.page', Mautic\PageBundle\Model\PageModel::class);
|
||||
$services->alias('mautic.page.model.redirect', Mautic\PageBundle\Model\RedirectModel::class);
|
||||
$services->alias('mautic.page.model.trackable', Mautic\PageBundle\Model\TrackableModel::class);
|
||||
$services->alias('mautic.page.model.video', Mautic\PageBundle\Model\VideoModel::class);
|
||||
$services->alias('mautic.page.model.tracking.404', Mautic\PageBundle\Model\Tracking404Model::class);
|
||||
$services->alias('mautic.page.repository.hit', Mautic\PageBundle\Entity\HitRepository::class);
|
||||
$services->alias('mautic.page.repository.page', Mautic\PageBundle\Entity\PageRepository::class);
|
||||
$services->alias('mautic.page.repository.redirect', Mautic\PageBundle\Entity\RedirectRepository::class);
|
||||
};
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AjaxController as CommonAjaxController;
|
||||
use Mautic\CoreBundle\Controller\VariantAjaxControllerTrait;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\PageBundle\Form\Type\AbTestPropertiesType;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Twig\Environment;
|
||||
|
||||
class AjaxController extends CommonAjaxController
|
||||
{
|
||||
use VariantAjaxControllerTrait;
|
||||
|
||||
public function getAbTestFormAction(Request $request, FormFactoryInterface $formFactory, PageModel $pageModel, Environment $twig): JsonResponse
|
||||
{
|
||||
return $this->sendJsonResponse($this->getAbTestForm(
|
||||
$request,
|
||||
$pageModel,
|
||||
fn ($formType, $formOptions) => $formFactory->create(AbTestPropertiesType::class, [], ['formType' => $formType, 'formTypeOptions' => $formOptions]),
|
||||
fn ($form) => $this->renderView('@MauticPage/AbTest/form.html.twig', ['form' => $this->setFormTheme($form, $twig, ['@MauticPage/AbTest/form.html.twig', 'MauticPageBundle:FormTheme\Page'])]),
|
||||
'page_abtest_settings',
|
||||
'page'
|
||||
));
|
||||
}
|
||||
|
||||
public function pageListAction(Request $request): JsonResponse
|
||||
{
|
||||
$filter = InputHelper::clean($request->query->get('filter'));
|
||||
$pageModel = $this->getModel('page.page');
|
||||
\assert($pageModel instanceof PageModel);
|
||||
$results = $pageModel->getLookupResults('page', $filter);
|
||||
$dataArray = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$dataArray[] = [
|
||||
'label' => $r['title']." ({$r['id']}:{$r['alias']})",
|
||||
'value' => $r['id'],
|
||||
];
|
||||
}
|
||||
|
||||
return $this->sendJsonResponse($dataArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by parent::getBuilderTokensAction().
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getBuilderTokens($query)
|
||||
{
|
||||
/** @var PageModel $model */
|
||||
$model = $this->getModel('page');
|
||||
|
||||
return $model->getBuilderComponents(null, ['tokens'], $query ?? '');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Page>
|
||||
*/
|
||||
class PageApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var PageModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$pageModel = $modelFactory->getModel('page');
|
||||
\assert($pageModel instanceof PageModel);
|
||||
|
||||
$this->model = $pageModel;
|
||||
$this->entityClass = Page::class;
|
||||
$this->entityNameOne = 'page';
|
||||
$this->entityNameMulti = 'pages';
|
||||
$this->serializerGroups = ['pageDetails', 'categoryList', 'publishDetails'];
|
||||
$this->dataInputMasks = ['customHtml' => 'html'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of pages.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function getEntitiesAction(Request $request, UserHelper $userHelper)
|
||||
{
|
||||
// get parent level only
|
||||
$this->listFilters[] = [
|
||||
'column' => 'p.variantParent',
|
||||
'expr' => 'isNull',
|
||||
];
|
||||
|
||||
$this->listFilters[] = [
|
||||
'column' => 'p.translationParent',
|
||||
'expr' => 'isNull',
|
||||
];
|
||||
|
||||
return parent::getEntitiesAction($request, $userHelper);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,626 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\AbstractFormController;
|
||||
use Mautic\CoreBundle\Exception\FileNotFoundException;
|
||||
use Mautic\CoreBundle\Exception\InvalidDecodedStringException;
|
||||
use Mautic\CoreBundle\Helper\CookieHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\ThemeHelper;
|
||||
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\PageConfig;
|
||||
use Mautic\PageBundle\Helper\TrackingHelper;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Mautic\PageBundle\Model\RedirectModel;
|
||||
use Mautic\PageBundle\Model\Tracking404Model;
|
||||
use Mautic\PageBundle\Model\VideoModel;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
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
|
||||
{
|
||||
/**
|
||||
* @param string $slug
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Exception
|
||||
* @throws FileNotFoundException
|
||||
*/
|
||||
public function indexAction(
|
||||
Request $request,
|
||||
ContactRequestHelper $contactRequestHelper,
|
||||
CookieHelper $cookieHelper,
|
||||
AnalyticsHelper $analyticsHelper,
|
||||
AssetsHelper $assetsHelper,
|
||||
ThemeHelper $themeHelper,
|
||||
Tracking404Model $tracking404Model,
|
||||
RouterInterface $router,
|
||||
DeviceTrackingServiceInterface $deviceTrackingService,
|
||||
$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());
|
||||
|
||||
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('</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((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('</head>', $analytics."\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,
|
||||
): 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<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'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CategoryBundle\Model\CategoryModel;
|
||||
|
||||
class LoadPageCategoryData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CategoryModel $categoryModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$today = new \DateTime();
|
||||
$cat = new Category();
|
||||
$events = 'Events';
|
||||
|
||||
$cat->setBundle('page');
|
||||
$cat->setDateAdded($today);
|
||||
$cat->setTitle($events);
|
||||
$cat->setAlias(strtolower($events));
|
||||
|
||||
$this->categoryModel->getRepository()->saveEntity($cat);
|
||||
$this->setReference('page-cat-1', $cat);
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Helper\Serializer;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
|
||||
class LoadPageData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PageModel $pageModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$pages = CsvHelper::csv_to_array(__DIR__.'/fakepagedata.csv');
|
||||
foreach ($pages as $count => $rows) {
|
||||
$page = new Page();
|
||||
$key = $count + 1;
|
||||
foreach ($rows as $col => $val) {
|
||||
if ('NULL' != $val) {
|
||||
$setter = 'set'.ucfirst($col);
|
||||
if (in_array($col, ['translationParent', 'variantParent'])) {
|
||||
$page->$setter($this->getReference('page-'.$val));
|
||||
} elseif (in_array($col, ['dateAdded', 'variantStartDate'])) {
|
||||
$page->$setter(new \DateTime($val));
|
||||
} elseif (in_array($col, ['content', 'variantSettings'])) {
|
||||
$val = Serializer::decode(stripslashes($val));
|
||||
$page->$setter($val);
|
||||
} else {
|
||||
$page->$setter($val);
|
||||
}
|
||||
}
|
||||
}
|
||||
$page->setCategory($this->getReference('page-cat-1'));
|
||||
$this->pageModel->getRepository()->saveEntity($page);
|
||||
|
||||
$this->setReference('page-'.$key, $page);
|
||||
}
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 7;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\CoreBundle\Helper\CsvHelper;
|
||||
use Mautic\CoreBundle\Helper\Serializer;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
|
||||
class LoadPageHitData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PageModel $pageModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$hits = CsvHelper::csv_to_array(__DIR__.'/fakepagehitdata.csv');
|
||||
|
||||
foreach ($hits as $rows) {
|
||||
$hit = new Hit();
|
||||
foreach ($rows as $col => $val) {
|
||||
if ('NULL' != $val) {
|
||||
$setter = 'set'.ucfirst($col);
|
||||
if (in_array($col, ['page', 'ipAddress'])) {
|
||||
$hit->$setter($this->getReference($col.'-'.$val));
|
||||
} elseif (in_array($col, ['dateHit', 'dateLeft'])) {
|
||||
$hit->$setter(new \DateTime($val));
|
||||
} elseif ('browserLanguages' == $col) {
|
||||
$val = Serializer::decode(stripslashes($val));
|
||||
$hit->$setter($val);
|
||||
} else {
|
||||
$hit->$setter($val);
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->pageModel->getRepository()->saveEntity($hit);
|
||||
}
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
translationParent,variantParent,isPublished,dateAdded,title,alias,template,language,content,hits,uniqueHits,variantHits,revision,metaDescription,variantSettings,variantStartDate
|
||||
NULL,NULL,1,8/9/14 0:00,Kaleidoscope Conference 2014,kaleidoscope-conference-2014,blank,en,"a:5:{s:4:\"top2\";s:151:\"<div><h1><span style=\"font-family:comic sans\"><span style=\"color:#0000FF\"><span dir=\"auto\">Kaleidoscope Conference 2014</span></span></span></h1></div>\";s:4:\"main\";s:44:\"<div>Sign up today!</div><div>{form=1}</div>\";s:6:\"footer\";s:0:\"\";s:6:\"right1\";s:0:\"\";s:7:\"bottom3\";s:0:\"\";}",31,28,28,8,"Join your fellow kaleidoscopians at the 2014 Kaleidoscope Conference. Learn new techniques, attend workshops, share ideas with others, or just hang out!",a:0:{},8/9/14 0:00
|
||||
NULL,1,1,8/9/14 0:12,Kaleidoscope Conference 2014 v2,kaleidoscope-conference-2014,blank,en,"a:5:{s:4:\"top2\";s:151:\"<div><h1><span style=\"font-family:comic sans\"><span style=\"color:#0000FF\"><span dir=\"auto\">Kaleidoscope Conference 2014</span></span></span></h1></div>\";s:4:\"main\";s:153:\"<div>Don't be afraid to reach out to your inner kid once again! Let your creativity roll at the annual Kaleidoscope Conference. Register today!</div>\";s:6:\"footer\";s:0:\"\";s:6:\"right1\";s:55:\"<div><div>Sign up today!</div><div>{form=1}</div></div>\";s:7:\"bottom3\";s:0:\"\";}",95,85,85,4,"Join your fellow kaleidoscopians at the 2014 Kaleidoscope Conference. Learn new techniques, attend workshops, share ideas with others, or just hang out!","a:2:{s:6:\"weight\";i:50;s:14:\"winnerCriteria\";s:14:\"page.dwelltime\";}",8/9/14 0:00
|
||||
1,NULL,1,8/9/14 1:07,Kaleidoscope Conferencia 2014,kaleidoscope-conference-2014,blank,es_MX,"a:5:{s:4:\"top2\";s:151:\"<div><h1><span style=\"font-family:comic sans\"><span style=\"color:#0000FF\"><span dir=\"auto\">Kaleidoscope Conference 2014</span></span></span></h1></div>\";s:4:\"main\";s:44:\"<div>Sign up today!</div><div>{form=2}</div>\";s:6:\"footer\";s:0:\"\";s:6:\"right1\";s:0:\"\";s:7:\"bottom3\";s:0:\"\";}",1,1,1,9,"Únete a tus compañeros kaleidoscopians en la Conferencia de 2014 Kaleidoscope. Aprenda nuevas técnicas, asistir a talleres, compartir ideas con los demás, o simplemente pasar el rato!",a:0:{},NULL
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 103.
|
@@ -0,0 +1,131 @@
|
||||
page,ipAddress,dateHit,dateLeft,code,referer,url,userAgent,pageLanguage,browserLanguages,trackingId
|
||||
3,1,8/9/14 19:00,8/9/14 19:01,200,http://localhost/mautic/pages/view/3,http://localhost/mautic/p/page/es_MX/3:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,es_MX,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",H764T844
|
||||
1,38,8/10/14 0:22,8/10/14 0:22,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",5KPMMUCC
|
||||
1,36,8/10/14 0:22,8/10/14 0:22,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",5KPMMUCC
|
||||
1,15,8/10/14 0:22,8/10/14 0:22,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",E2Q6OBTT
|
||||
1,18,8/10/14 0:23,8/10/14 0:23,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",RZT87U00
|
||||
1,42,8/10/14 0:23,8/10/14 0:23,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",RZT87U00
|
||||
1,5,8/10/14 0:23,8/10/14 0:23,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",YOQLXM44
|
||||
1,1,8/10/14 0:23,8/10/14 0:23,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",OBKQCCRR
|
||||
1,39,8/10/14 0:23,8/10/14 0:23,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",OBKQCCRR
|
||||
1,42,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",SUDI7EZZ
|
||||
1,42,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",6QM3ZGEE
|
||||
1,32,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",JBW4CYRR
|
||||
1,37,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",IYPAQXX
|
||||
1,35,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",RDDQEK44
|
||||
1,14,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",3GLTTX00
|
||||
1,15,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",Z7H6S6CC
|
||||
1,32,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",ZIPRWGMM
|
||||
1,15,8/10/14 0:24,8/10/14 0:24,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",9WOPU3WW
|
||||
1,30,8/10/14 0:26,8/10/14 0:26,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",9QW2PE44
|
||||
1,2,8/10/14 0:26,8/10/14 0:26,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",KT7FHXSS
|
||||
1,20,8/10/14 0:26,8/10/14 0:26,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",D3O0F7NN
|
||||
1,43,8/10/14 0:27,8/10/14 0:27,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",QRT1O6VV
|
||||
1,4,8/10/14 0:27,8/10/14 0:27,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",CLRK3CMM
|
||||
1,43,8/10/14 0:27,8/10/14 0:27,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",4H7BVT22
|
||||
1,2,8/10/14 0:27,8/10/14 0:28,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",6F8ZXEE
|
||||
1,31,8/10/14 0:30,8/10/14 0:30,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",WYY3KF66
|
||||
2,46,8/10/14 0:30,8/10/14 0:30,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",HSZ4NWKK
|
||||
2,36,8/10/14 0:30,8/10/14 0:30,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",4M9RF144
|
||||
2,45,8/10/14 0:30,8/10/14 0:30,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",GIYIEJ22
|
||||
2,14,8/10/14 0:30,8/10/14 0:30,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",OPAIDMM
|
||||
2,35,8/10/14 0:30,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",P9JOSSVV
|
||||
2,33,8/10/14 0:30,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",Q8SLO5YY
|
||||
2,8,8/10/14 0:30,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",OGRXTRQQ
|
||||
2,42,8/10/14 0:30,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",1R45QTUU
|
||||
2,35,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",OB1QZ144
|
||||
2,47,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",UZ00ZR66
|
||||
2,31,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",KRQ2F4FF
|
||||
2,15,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",NVN2JEFF
|
||||
2,28,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",UJFR71NN
|
||||
2,47,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",CZ69ZOUU
|
||||
2,2,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",Z03WC1OO
|
||||
2,15,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",YLUNOS33
|
||||
2,21,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",4Q4L3EDD
|
||||
2,9,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",F73DD644
|
||||
2,33,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",RVOJXM00
|
||||
2,36,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",WYRHCFF
|
||||
2,30,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",PX57555
|
||||
2,44,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",0IBIS0GG
|
||||
2,28,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",LFOHPMNN
|
||||
2,9,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",74CM5J99
|
||||
2,9,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",4EZBX0GG
|
||||
2,19,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",MKEVKDZZ
|
||||
2,19,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",99LNEEKK
|
||||
2,35,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",QDPXP9
|
||||
2,18,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",ICW7BS77
|
||||
2,36,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",EFAHDC99
|
||||
2,24,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",LM4J9PII
|
||||
2,11,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",2S3J1K66
|
||||
2,34,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",3EXUCLEE
|
||||
2,37,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",W5TR0BRR
|
||||
2,31,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",3I4TC288
|
||||
2,44,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",IQLVMMUU
|
||||
2,25,8/10/14 0:31,8/10/14 0:31,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",XC5ZPDXX
|
||||
2,43,8/10/14 0:32,8/10/14 0:32,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",WNOL0UZZ
|
||||
2,39,8/10/14 0:32,8/10/14 0:32,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",KOFQ3044
|
||||
2,17,8/10/14 0:32,8/10/14 0:32,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",GI26RS66
|
||||
2,17,8/10/14 0:32,8/10/14 0:32,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",GQEN0GEE
|
||||
2,33,8/10/14 0:40,8/10/14 0:40,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",ONZRBVGG
|
||||
2,14,8/10/14 0:40,8/10/14 0:41,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",N4MLY555
|
||||
1,22,8/10/14 0:47,8/10/14 0:47,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",AGEYKUYY
|
||||
2,15,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",TOWNL9UU
|
||||
2,10,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",3SN3WPJJ
|
||||
2,4,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",RQY64ZHH
|
||||
2,41,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",O3BDIVWW
|
||||
2,40,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",G2JULRR
|
||||
2,27,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",9Q4LJOWW
|
||||
2,14,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",ZKDZ9N00
|
||||
2,37,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",JLPUNPII
|
||||
2,46,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",ATVT2KUU
|
||||
2,16,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",6L5GEWII
|
||||
2,43,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",C0EX4933
|
||||
2,17,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",1JWEEY
|
||||
2,6,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",HMN1FZ77
|
||||
2,29,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",4V37K977
|
||||
2,24,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",GBDDG0JJ
|
||||
2,35,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",8EJ7OW99
|
||||
2,1,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",Y21VCNLL
|
||||
2,1,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",4OOI0C66
|
||||
2,2,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",3XQD0E33
|
||||
2,6,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",W5PNQ9CC
|
||||
2,25,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",11BBNP
|
||||
2,5,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",3QVAHF11
|
||||
2,49,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",4DCW3PVV
|
||||
2,28,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",0EZB9OCC
|
||||
2,46,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",ODB8SKHH
|
||||
2,44,8/10/14 0:47,8/10/14 0:47,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",F744CMII
|
||||
1,32,8/10/14 1:08,8/10/14 1:08,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",ZGWS2Z33
|
||||
2,26,8/10/14 1:08,8/10/14 1:08,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",95XW2JVV
|
||||
2,34,8/10/14 1:11,8/10/14 1:11,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",C7EYGG66
|
||||
2,41,8/10/14 1:11,8/10/14 1:11,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",J6C2KJJ
|
||||
2,2,8/10/14 1:11,8/10/14 1:11,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",1US5SPBB
|
||||
2,39,8/10/14 1:12,8/10/14 1:12,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",CW6YD533
|
||||
2,38,8/10/14 1:12,8/10/14 1:12,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",FY9FEAWW
|
||||
2,24,8/10/14 1:12,8/10/14 1:12,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",Z4LXDL66
|
||||
2,3,8/10/14 1:12,8/10/14 1:12,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",IJPE4Z33
|
||||
2,45,8/10/14 1:12,8/10/14 1:12,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",3L76FSS
|
||||
2,13,8/10/14 1:12,8/10/14 1:12,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",UD2OGC44
|
||||
2,31,8/10/14 1:12,8/10/14 1:13,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",Z36Y96YY
|
||||
2,17,8/10/14 1:12,8/10/14 1:13,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",IX3S9766
|
||||
2,38,8/10/14 1:13,8/10/14 1:13,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",OFX17FUU
|
||||
2,42,8/10/14 1:13,8/10/14 1:13,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",7259A4YY
|
||||
2,10,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",TK9649UU
|
||||
2,25,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",9NZ8HLWW
|
||||
2,31,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",8N6RW177
|
||||
2,45,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",OC6LS7RR
|
||||
2,26,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",QTZ8XMM
|
||||
2,1,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",RC7GLHQQ
|
||||
2,16,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",MPFEO811
|
||||
2,29,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",PSOP6XOO
|
||||
2,36,8/10/14 1:15,8/10/14 1:15,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",7K6Z7LXX
|
||||
2,41,8/10/14 1:17,8/10/14 1:18,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",53e6bd3323eb9
|
||||
2,48,8/10/14 1:18,8/10/14 1:18,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",53e6bd3323eb9
|
||||
1,50,8/10/14 1:20,8/10/14 1:21,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",53e6bb48e5053
|
||||
1,18,8/10/14 1:20,8/10/14 1:20,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",53e6bb48e5053
|
||||
2,5,8/10/14 1:48,8/10/14 1:48,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.77.4 (KHTML, like Gecko) Version/7.0.5 Safari/537.77.4",en,"a:1:{i:0;s:5:\"en-US\";}",53e6bd3323eb9
|
||||
1,14,8/12/14 19:51,8/12/14 19:51,200,http://localhost/mautic/pages/view/1,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",53ea6d090aab5
|
||||
2,9,8/12/14 19:51,8/12/14 19:51,200,NULL,http://localhost/mautic/p/page/2:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",53ea6d090aab5
|
||||
2,7,8/12/14 20:02,8/12/14 20:02,200,NULL,http://localhost/mautic/p/page/2:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",53ea72c1c0232
|
||||
1,19,8/12/14 20:02,8/12/14 20:02,200,NULL,http://localhost/mautic/p/page/1:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",53ea72c1c0232
|
||||
2,35,8/12/14 20:02,8/12/14 20:02,200,NULL,http://localhost/mautic/p/page/2:kaleidoscope-conference-2014,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:31.0) Gecko/20100101 Firefox/31.0,en,"a:2:{i:0;s:5:\"en-US\";i:1;s:8:\"en;q=0.5\";}",53ea72c1c0232
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 243.
|
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticPageExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,844 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\EmailBundle\Entity\Email;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\LeadDevice;
|
||||
|
||||
class Hit
|
||||
{
|
||||
public const TABLE_NAME = 'page_hits';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateHit;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateLeft;
|
||||
|
||||
private ?Page $page = null;
|
||||
|
||||
/**
|
||||
* @var Redirect|null
|
||||
*/
|
||||
private $redirect;
|
||||
|
||||
/**
|
||||
* @var Email|null
|
||||
*/
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $country;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $region;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $city;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $isp;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $organization;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $code;
|
||||
|
||||
private $referer;
|
||||
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $urlTitle;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $userAgent;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $remoteHost;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $pageLanguage;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private $browserLanguages = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
**/
|
||||
private $trackingId;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $source;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $sourceId;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $query = [];
|
||||
|
||||
/**
|
||||
* @var LeadDevice|null
|
||||
*/
|
||||
private $device;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(HitRepository::class)
|
||||
->addIndex(['tracking_id'], 'page_hit_tracking_search')
|
||||
->addIndex(['code'], 'page_hit_code_search')
|
||||
->addIndex(['source', 'source_id'], 'page_hit_source_search')
|
||||
->addIndex(['date_hit', 'date_left'], 'date_hit_left_index')
|
||||
->addIndexWithOptions(['url'], 'page_hit_url', ['lengths' => [0 => 128]]);
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createField('dateHit', 'datetime')
|
||||
->columnName('date_hit')
|
||||
->build();
|
||||
|
||||
$builder->createField('dateLeft', 'datetime')
|
||||
->columnName('date_left')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('page', 'Page')
|
||||
->addJoinColumn('page_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('redirect', 'Redirect')
|
||||
->addJoinColumn('redirect_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('email', Email::class)
|
||||
->addJoinColumn('email_id', 'id', true, false, 'SET NULL')
|
||||
->build();
|
||||
|
||||
$builder->addLead(true, 'SET NULL');
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('country', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('region', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('city', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('isp', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('organization', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addField('code', 'integer');
|
||||
|
||||
$builder->createField('referer', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('url', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('urlTitle', 'string')
|
||||
->columnName('url_title')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('userAgent', 'text')
|
||||
->columnName('user_agent')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('remoteHost', 'string')
|
||||
->columnName('remote_host')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('pageLanguage', 'string')
|
||||
->columnName('page_language')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('browserLanguages', 'array')
|
||||
->columnName('browser_languages')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('trackingId', 'string')
|
||||
->columnName('tracking_id')
|
||||
->build();
|
||||
|
||||
$builder->createField('source', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('sourceId', 'integer')
|
||||
->columnName('source_id')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('query', 'array');
|
||||
|
||||
$builder->createManyToOne('device', LeadDevice::class)
|
||||
->addJoinColumn('device_id', 'id', true, false, 'SET NULL')
|
||||
->cascadePersist()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('hit')
|
||||
->addProperties(
|
||||
[
|
||||
'id',
|
||||
'dateHit',
|
||||
'dateLeft',
|
||||
'page',
|
||||
'redirect',
|
||||
'email',
|
||||
'lead',
|
||||
'ipAddress',
|
||||
'country',
|
||||
'region',
|
||||
'city',
|
||||
'isp',
|
||||
'organization',
|
||||
'code',
|
||||
'referer',
|
||||
'url',
|
||||
'urlTitle',
|
||||
'userAgent',
|
||||
'remoteHost',
|
||||
'pageLanguage',
|
||||
'browserLanguages',
|
||||
'trackingId',
|
||||
'source',
|
||||
'sourceId',
|
||||
'query',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateHit.
|
||||
*
|
||||
* @param \DateTime $dateHit
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setDateHit($dateHit)
|
||||
{
|
||||
$this->dateHit = $dateHit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateHit.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateHit()
|
||||
{
|
||||
return $this->dateHit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateLeft()
|
||||
{
|
||||
return $this->dateLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateLeft
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setDateLeft($dateLeft)
|
||||
{
|
||||
$this->dateLeft = $dateLeft;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set country.
|
||||
*
|
||||
* @param string $country
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setCountry($country)
|
||||
{
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCountry()
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set region.
|
||||
*
|
||||
* @param string $region
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setRegion($region)
|
||||
{
|
||||
$this->region = $region;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get region.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRegion()
|
||||
{
|
||||
return $this->region;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set city.
|
||||
*
|
||||
* @param string $city
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setCity($city)
|
||||
{
|
||||
$this->city = $city;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get city.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCity()
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isp.
|
||||
*
|
||||
* @param string $isp
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setIsp($isp)
|
||||
{
|
||||
$this->isp = $isp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isp.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIsp()
|
||||
{
|
||||
return $this->isp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set organization.
|
||||
*
|
||||
* @param string $organization
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setOrganization($organization)
|
||||
{
|
||||
$this->organization = $organization;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get organization.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOrganization()
|
||||
{
|
||||
return $this->organization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set code.
|
||||
*
|
||||
* @param int $code
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setCode($code)
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCode()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set referer.
|
||||
*
|
||||
* @param string $referer
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setReferer($referer)
|
||||
{
|
||||
$this->referer = $referer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get referer.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getReferer()
|
||||
{
|
||||
return $this->referer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set url.
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setUrl($url)
|
||||
{
|
||||
$this->url = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set url title.
|
||||
*
|
||||
* @param string $urlTitle
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setUrlTitle($urlTitle)
|
||||
{
|
||||
$urlTitle = mb_strlen($urlTitle) <= 191 ? $urlTitle : mb_substr($urlTitle, 0, 191);
|
||||
$this->urlTitle = $urlTitle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrlTitle()
|
||||
{
|
||||
return $this->urlTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userAgent.
|
||||
*
|
||||
* @param string $userAgent
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setUserAgent($userAgent)
|
||||
{
|
||||
$this->userAgent = $userAgent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userAgent.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent()
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remoteHost.
|
||||
*
|
||||
* @param string $remoteHost
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setRemoteHost($remoteHost)
|
||||
{
|
||||
$this->remoteHost = $remoteHost;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remoteHost.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteHost()
|
||||
{
|
||||
return $this->remoteHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page.
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setPage(?Page $page = null)
|
||||
{
|
||||
$this->page = $page;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?Page
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Hit
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ipAddress)
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress|null
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $trackingId
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setTrackingId($trackingId)
|
||||
{
|
||||
$this->trackingId = $trackingId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trackingId.
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTrackingId()
|
||||
{
|
||||
return $this->trackingId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set pageLanguage.
|
||||
*
|
||||
* @param string $pageLanguage
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setPageLanguage($pageLanguage)
|
||||
{
|
||||
$this->pageLanguage = $pageLanguage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pageLanguage.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPageLanguage()
|
||||
{
|
||||
return $this->pageLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set browserLanguages.
|
||||
*
|
||||
* @param array<string> $browserLanguages
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setBrowserLanguages($browserLanguages)
|
||||
{
|
||||
$this->browserLanguages = $browserLanguages;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browserLanguages.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getBrowserLanguages()
|
||||
{
|
||||
return $this->browserLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Hit
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSource()
|
||||
{
|
||||
return $this->source;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $source
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setSource($source)
|
||||
{
|
||||
$this->source = $source;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getSourceId()
|
||||
{
|
||||
return $this->sourceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $sourceId
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setSourceId($sourceId)
|
||||
{
|
||||
$this->sourceId = (int) $sourceId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Redirect
|
||||
*/
|
||||
public function getRedirect()
|
||||
{
|
||||
return $this->redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Hit
|
||||
*/
|
||||
public function setRedirect(Redirect $redirect)
|
||||
{
|
||||
$this->redirect = $redirect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $email
|
||||
*/
|
||||
public function setEmail(Email $email): void
|
||||
{
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $query
|
||||
*
|
||||
* @return Hit
|
||||
*/
|
||||
public function setQuery($query)
|
||||
{
|
||||
$this->query = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LeadDevice
|
||||
*/
|
||||
public function getDeviceStat()
|
||||
{
|
||||
return $this->device;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Hit
|
||||
*/
|
||||
public function setDeviceStat(LeadDevice $device)
|
||||
{
|
||||
$this->device = $device;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\Expression\CompositeExpression;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Hit>
|
||||
*/
|
||||
class HitRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Determine if the page hit is a unique.
|
||||
*
|
||||
* @param Page|Redirect $page
|
||||
* @param string $trackingId
|
||||
*/
|
||||
public function isUniquePageHit($page, $trackingId, ?Lead $lead = null): bool
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q2 = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q2->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'h');
|
||||
|
||||
// If we know the lead, use that to determine uniqueness
|
||||
if (null !== $lead && $lead->getId()) {
|
||||
$expr = CompositeExpression::and($q2->expr()->eq('h.lead_id', $lead->getId()));
|
||||
} else {
|
||||
$expr = CompositeExpression::and($q2->expr()->eq('h.tracking_id', ':id'));
|
||||
$q->setParameter('id', $trackingId);
|
||||
}
|
||||
|
||||
if ($page instanceof Page) {
|
||||
$expr = $expr->with(
|
||||
$q2->expr()->eq('h.page_id', $page->getId())
|
||||
);
|
||||
} elseif ($page instanceof Redirect) {
|
||||
$expr = $expr->with(
|
||||
$q2->expr()->eq('h.redirect_id', $page->getId())
|
||||
);
|
||||
}
|
||||
|
||||
$q2->where($expr);
|
||||
|
||||
$q->select('u.is_unique')
|
||||
->from(sprintf('(SELECT (NOT EXISTS (%s)) is_unique)', $q2->getSQL()), 'u');
|
||||
|
||||
return (bool) $q->executeQuery()->fetchOne();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's page hits.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLeadHits($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select('h.id as hitId, h.page_id, h.user_agent as userAgent, h.date_hit as dateHit, h.date_left as dateLeft, h.referer, h.source, h.source_id as sourceId, h.url, h.url_title as urlTitle, h.query, ds.client_info as clientInfo, ds.device, ds.device_os_name as deviceOsName, ds.device_brand as deviceBrand, ds.device_model as deviceModel, h.lead_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'h')
|
||||
->leftJoin('h', MAUTIC_TABLE_PREFIX.'pages', 'p', 'h.page_id = p.id');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where('h.lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->like('p.title', ':search')
|
||||
)->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
$query->leftjoin('h', MAUTIC_TABLE_PREFIX.'lead_devices', 'ds', 'ds.id = h.device_id');
|
||||
|
||||
if (isset($options['url']) && $options['url']) {
|
||||
$query->andWhere($query->expr()->eq('h.url', $query->expr()->literal($options['url'])));
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'p.title', 'h.date_hit', ['query'], ['dateHit', 'dateLeft'], null, 'h.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getHitCountForSource($source, $sourceId = null, $fromDate = null, $code = 200)
|
||||
{
|
||||
$query = $this->createQueryBuilder('h');
|
||||
$query->select('count(distinct(h.trackingId)) as hitCount');
|
||||
$query->andWhere($query->expr()->eq('h.source', $query->expr()->literal($source)));
|
||||
|
||||
if (null != $sourceId) {
|
||||
if (is_array($sourceId)) {
|
||||
$query->andWhere($query->expr()->in('h.sourceId', ':sourceIds'))
|
||||
->setParameter('sourceIds', $sourceId);
|
||||
} else {
|
||||
$query->andWhere('h.sourceId = :sourceId')
|
||||
->setParameter('sourceId', $sourceId);
|
||||
}
|
||||
}
|
||||
|
||||
if (null != $fromDate) {
|
||||
$query->andwhere($query->expr()->gte('h.dateHit', ':date'))
|
||||
->setParameter('date', $fromDate);
|
||||
}
|
||||
|
||||
$query->andWhere('h.code = :code')
|
||||
->setParameter('code', $code);
|
||||
|
||||
return $query->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of hits via an email clickthrough.
|
||||
*
|
||||
* @param int $code
|
||||
*/
|
||||
public function getEmailClickthroughHitCount($emailIds, ?\DateTime $fromDate = null, $code = 200): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
if (!is_array($emailIds)) {
|
||||
$emailIds = [$emailIds];
|
||||
}
|
||||
|
||||
$q->select('count(distinct(h.tracking_id)) as hit_count, h.email_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'h')
|
||||
->where($q->expr()->in('h.email_id', $emailIds))
|
||||
->groupBy('h.email_id');
|
||||
|
||||
if (null != $fromDate) {
|
||||
$dateHelper = new DateTimeHelper($fromDate);
|
||||
$q->andwhere($q->expr()->gte('h.date_hit', ':date'))
|
||||
->setParameter('date', $dateHelper->toUtcString());
|
||||
}
|
||||
|
||||
$q->andWhere($q->expr()->eq('h.code', (int) $code));
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$hits = [];
|
||||
foreach ($results as $r) {
|
||||
$hits[$r['email_id']] = $r['hit_count'];
|
||||
}
|
||||
|
||||
return $hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count returning IP addresses.
|
||||
*/
|
||||
public function countReturningIp(): int
|
||||
{
|
||||
$q = $this->createQueryBuilder('h');
|
||||
$q->select('COUNT(h.ipAddress) as returning')
|
||||
->groupBy('h.ipAddress')
|
||||
->having($q->expr()->gt('COUNT(h.ipAddress)', 1));
|
||||
$results = $q->getQuery()->getResult();
|
||||
|
||||
return count($results);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count email clickthrough.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function countEmailClickthrough()
|
||||
{
|
||||
$q = $this->createQueryBuilder('h');
|
||||
$q->select('COUNT(h.email) as clicks');
|
||||
$results = $q->getQuery()->getSingleResult();
|
||||
|
||||
return $results['clicks'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Count how many visitors hit some page in last X $seconds.
|
||||
*
|
||||
* @param int $seconds
|
||||
* @param bool $notLeft
|
||||
*/
|
||||
public function countVisitors($seconds = 60, $notLeft = false): int
|
||||
{
|
||||
$now = new \DateTime();
|
||||
$viewingTime = new \DateInterval('PT'.$seconds.'S');
|
||||
$now->sub($viewingTime);
|
||||
$query = $this->createQueryBuilder('h');
|
||||
|
||||
$query->select('count(h.code) as visitors');
|
||||
|
||||
if ($seconds) {
|
||||
$query->where($query->expr()->gte('h.dateHit', ':date'))
|
||||
->setParameter('date', $now);
|
||||
}
|
||||
|
||||
if ($notLeft) {
|
||||
$query->andWhere($query->expr()->isNull('h.dateLeft'));
|
||||
}
|
||||
|
||||
$result = $query->getQuery()->getSingleResult();
|
||||
|
||||
if (!isset($result['visitors'])) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) $result['visitors'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest hit.
|
||||
*
|
||||
* @param array $options
|
||||
*/
|
||||
public function getLatestHit($options): ?\DateTime
|
||||
{
|
||||
$sq = $this->_em->getConnection()->createQueryBuilder();
|
||||
$sq->select('h.date_hit latest_hit')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'h');
|
||||
|
||||
if (isset($options['leadId'])) {
|
||||
$sq->andWhere(
|
||||
$sq->expr()->eq('h.lead_id', $options['leadId'])
|
||||
);
|
||||
}
|
||||
if (isset($options['urls']) && $options['urls']) {
|
||||
$inUrls = (!is_array($options['urls'])) ? [$options['urls']] : $options['urls'];
|
||||
foreach ($inUrls as $k => $u) {
|
||||
$sq->andWhere($sq->expr()->like('h.url', ':url_'.$k))
|
||||
->setParameter('url_'.$k, $u);
|
||||
}
|
||||
}
|
||||
if (isset($options['second_to_last'])) {
|
||||
$sq->andWhere($sq->expr()->neq('h.id', $options['second_to_last']));
|
||||
} else {
|
||||
$sq->orderBy('h.date_hit', 'DESC limit 1');
|
||||
}
|
||||
$result = $sq->executeQuery()->fetchAssociative();
|
||||
|
||||
return $result ? new \DateTime($result['latest_hit'], new \DateTimeZone('UTC')) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of bounces.
|
||||
*
|
||||
* @param array|string $pageIds
|
||||
* @param bool $isVariantCheck
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getBounces($pageIds, ?\DateTime $fromDate = null, $isVariantCheck = false): array
|
||||
{
|
||||
$inOrEq = (!is_array($pageIds)) ? 'eq' : 'in';
|
||||
|
||||
$hitsColumn = ($isVariantCheck) ? 'variant_hits' : 'unique_hits';
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$pages = $q->select("p.id, p.$hitsColumn as totalHits, p.title")
|
||||
->from(MAUTIC_TABLE_PREFIX.'pages', 'p')
|
||||
->where($q->expr()->$inOrEq('p.id', $pageIds))
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
foreach ($pages as $p) {
|
||||
$return[$p['id']] = [
|
||||
'totalHits' => (int) $p['totalHits'],
|
||||
'bounces' => 0,
|
||||
'rate' => 0,
|
||||
'title' => $p['title'],
|
||||
];
|
||||
}
|
||||
|
||||
// Get the total number of bounces - simplified query for if date_left is null, it'll more than likely be a bounce or
|
||||
// else we would have recorded the date_left on a subsequent page hit
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$expr = $q->expr()->and(
|
||||
$q->expr()->$inOrEq('h.page_id', $pageIds),
|
||||
$q->expr()->eq('h.code', 200),
|
||||
$q->expr()->isNull('h.date_left')
|
||||
);
|
||||
|
||||
if (null !== $fromDate) {
|
||||
// make sure the date is UTC
|
||||
$dt = new DateTimeHelper($fromDate, 'Y-m-d H:i:s', 'local');
|
||||
$expr = $expr->with(
|
||||
$q->expr()->gte('h.date_hit', $q->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
|
||||
$q->select('count(*) as bounces, h.page_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'h')
|
||||
->where($expr)
|
||||
->groupBy('h.page_id');
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
foreach ($results as $p) {
|
||||
$return[$p['page_id']]['bounces'] = (int) $p['bounces'];
|
||||
$return[$p['page_id']]['rate'] = ($return[$p['page_id']]['totalHits']) ? round(
|
||||
($p['bounces'] / $return[$p['page_id']]['totalHits']) * 100,
|
||||
2
|
||||
) : 0;
|
||||
}
|
||||
|
||||
return (!is_array($pageIds)) ? $return[$pageIds] : $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of dwell time labels with ranges.
|
||||
*/
|
||||
public function getDwellTimeLabels(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'label' => '< 1m',
|
||||
'from' => 0,
|
||||
'till' => 60,
|
||||
],
|
||||
[
|
||||
'label' => '1 - 5m',
|
||||
'from' => 60,
|
||||
'till' => 300,
|
||||
],
|
||||
[
|
||||
'label' => '5 - 10m',
|
||||
'value' => 0,
|
||||
'from' => 300,
|
||||
'till' => 600,
|
||||
],
|
||||
[
|
||||
'label' => '> 10m',
|
||||
'from' => 600,
|
||||
'till' => 999999,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dwell times for bunch of pages.
|
||||
*/
|
||||
public function getDwellTimesForPages(array $pageIds, array $options): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'page_hits', 'ph')
|
||||
->leftJoin('ph', MAUTIC_TABLE_PREFIX.'pages', 'p', 'ph.page_id = p.id')
|
||||
->select('ph.page_id, ph.date_hit, ph.date_left, p.title')
|
||||
->orderBy('ph.date_hit', 'ASC')
|
||||
->andWhere(
|
||||
$q->expr()->and(
|
||||
$q->expr()->in('ph.page_id', $pageIds)
|
||||
)
|
||||
);
|
||||
|
||||
if (isset($options['fromDate'])) {
|
||||
// make sure the date is UTC
|
||||
$dt = new DateTimeHelper($options['fromDate']);
|
||||
$q->andWhere(
|
||||
$q->expr()->gte('ph.date_hit', $q->expr()->literal($dt->toUtcString()))
|
||||
);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
// loop to structure
|
||||
$times = [];
|
||||
$titles = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$dateHit = $r['date_hit'] ? new \DateTime($r['date_hit']) : 0;
|
||||
$dateLeft = $r['date_left'] ? new \DateTime($r['date_left']) : 0;
|
||||
|
||||
$titles[$r['page_id']] = $r['title'];
|
||||
$times[$r['page_id']][] = $dateLeft ? ($dateLeft->getTimestamp() - $dateHit->getTimestamp()) : 0;
|
||||
}
|
||||
|
||||
// now loop to create stats
|
||||
$stats = [];
|
||||
|
||||
foreach ($times as $pid => $time) {
|
||||
$stats[$pid] = $this->countStats($time);
|
||||
$stats[$pid]['title'] = $titles[$pid];
|
||||
}
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dwell times for bunch of URLs.
|
||||
*
|
||||
* @param string $url
|
||||
*/
|
||||
public function getDwellTimesForUrl($url, array $options): array
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->from(MAUTIC_TABLE_PREFIX.'page_hits', 'ph')
|
||||
->leftJoin('ph', MAUTIC_TABLE_PREFIX.'pages', 'p', 'ph.page_id = p.id')
|
||||
->select('ph.id, ph.page_id, ph.date_hit, ph.date_left, ph.tracking_id, ph.page_language, p.title')
|
||||
->orderBy('ph.date_hit', 'ASC')
|
||||
->andWhere($q->expr()->like('ph.url', ':url'))
|
||||
->setParameter('url', $url);
|
||||
|
||||
if (isset($options['leadId']) && $options['leadId']) {
|
||||
$q->andWhere(
|
||||
$q->expr()->eq('ph.lead_id', (int) $options['leadId'])
|
||||
);
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$times = [];
|
||||
|
||||
foreach ($results as $r) {
|
||||
$dateHit = $r['date_hit'] ? new \DateTime($r['date_hit']) : 0;
|
||||
$dateLeft = $r['date_left'] ? new \DateTime($r['date_left']) : 0;
|
||||
$times[] = $dateLeft ? ($dateLeft->getTimestamp() - $dateHit->getTimestamp()) : 0;
|
||||
}
|
||||
|
||||
return $this->countStats($times);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count stats from hit times.
|
||||
*
|
||||
* @param array $times
|
||||
*/
|
||||
public function countStats($times): array
|
||||
{
|
||||
return [
|
||||
'sum' => array_sum($times),
|
||||
'min' => count($times) ? min($times) : 0,
|
||||
'max' => count($times) ? max($times) : 0,
|
||||
'average' => count($times) ? round(array_sum($times) / count($times)) : 0,
|
||||
'count' => count($times),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a hit with the the time the user left.
|
||||
*
|
||||
* @param int $lastHitId
|
||||
*/
|
||||
public function updateHitDateLeft($lastHitId): void
|
||||
{
|
||||
$dt = new DateTimeHelper();
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'page_hits')
|
||||
->set('date_left', ':datetime')
|
||||
->where('id = '.(int) $lastHitId)
|
||||
->setParameter('datetime', $dt->toUtcString());
|
||||
$q->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of referers ordered by it's count.
|
||||
*
|
||||
* @param \Doctrine\DBAL\Query\QueryBuilder $query
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getReferers($query, $limit = 10, $offset = 0): array
|
||||
{
|
||||
$query->select('ph.referer, count(ph.referer) as sessions')
|
||||
->groupBy('ph.referer')
|
||||
->orderBy('sessions', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of referers ordered by it's count.
|
||||
*
|
||||
* @param \Doctrine\DBAL\Query\QueryBuilder $query
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
* @param string $column
|
||||
* @param string $as
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getMostVisited($query, $limit = 10, $offset = 0, $column = 'p.hits', $as = ''): array
|
||||
{
|
||||
if ($as) {
|
||||
$as = ' as "'.$as.'"';
|
||||
}
|
||||
|
||||
$query->select('p.title, p.id, '.$column.$as)
|
||||
->where('p.id IS NOT NULL')
|
||||
->groupBy('p.id, p.title, '.$column)
|
||||
->orderBy($column, 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
public function updateLeadByTrackingId($leadId, $newTrackingId, $oldTrackingId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'page_hits')
|
||||
->set('lead_id', (int) $leadId)
|
||||
->set('tracking_id', ':newTrackingId')
|
||||
->where(
|
||||
$q->expr()->eq('tracking_id', ':oldTrackingId')
|
||||
)
|
||||
->setParameters([
|
||||
'newTrackingId' => $newTrackingId,
|
||||
'oldTrackingId' => $oldTrackingId,
|
||||
])
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'page_hits')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
public function getLatestHitDateByLead(int $leadId, ?string $trackingId = null): ?\DateTime
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('MAX(date_hit)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits')
|
||||
->where('lead_id = :leadId')
|
||||
->setParameter('leadId', $leadId);
|
||||
|
||||
if (null != $trackingId) {
|
||||
$q->andWhere('tracking_id = :trackingId')
|
||||
->setParameter('trackingId', $trackingId);
|
||||
}
|
||||
|
||||
$result = $q->executeQuery()->fetchOne();
|
||||
|
||||
return $result ? new \DateTime($result, new \DateTimeZone('UTC')) : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,957 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CategoryBundle\Entity\Category;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\TranslationEntityInterface;
|
||||
use Mautic\CoreBundle\Entity\TranslationEntityTrait;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Mautic\CoreBundle\Entity\VariantEntityInterface;
|
||||
use Mautic\CoreBundle\Entity\VariantEntityTrait;
|
||||
use Mautic\CoreBundle\Validator\EntityEvent;
|
||||
use Mautic\ProjectBundle\Entity\ProjectTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Constraints\Callback;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('page:pages:viewown')"),
|
||||
new Post(security: "is_granted('page:pages:create')"),
|
||||
new Get(security: "is_granted('page:pages:viewown')"),
|
||||
new Put(security: "is_granted('page:pages:editown')"),
|
||||
new Patch(security: "is_granted('page:pages:editother')"),
|
||||
new Delete(security: "is_granted('page:pages:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['page:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['category', 'translationChildren'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['page:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
/**
|
||||
* @use TranslationEntityTrait<Page>
|
||||
* @use VariantEntityTrait<Page>
|
||||
*/
|
||||
class Page extends FormEntity implements TranslationEntityInterface, VariantEntityInterface, UuidInterface
|
||||
{
|
||||
use TranslationEntityTrait;
|
||||
use VariantEntityTrait;
|
||||
use UuidTrait;
|
||||
use ProjectTrait;
|
||||
public const ENTITY_NAME = 'page';
|
||||
|
||||
public const TABLE_NAME = 'pages';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['page:read', 'download:read', 'email:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $alias;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $template;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $customHtml;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $content = [];
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $publishUp;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $publishDown;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $hits = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $uniqueHits = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $variantHits = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $revision = 1;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $metaDescription;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $headScript;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $footerScript;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $redirectType;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $redirectUrl;
|
||||
|
||||
/**
|
||||
* @var Category|null
|
||||
**/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $category;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $isPreferenceCenter;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private $noIndex;
|
||||
|
||||
/**
|
||||
* Used to identify the page for the builder.
|
||||
*/
|
||||
private $sessionId;
|
||||
|
||||
private ?PageDraft $draft = null;
|
||||
|
||||
private bool $isCloned = false;
|
||||
|
||||
private ?int $cloneObjectId = null;
|
||||
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private ?bool $publicPreview = true;
|
||||
|
||||
#[Groups(['page:read', 'page:write', 'download:read', 'email:read'])]
|
||||
private bool $isDuplicate = false;
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
$this->cloneObjectId = (int) $this->id;
|
||||
$this->isCloned = true;
|
||||
$this->id = null;
|
||||
$this->clearTranslations();
|
||||
$this->clearVariants();
|
||||
$this->setDraft(null);
|
||||
|
||||
parent::__clone();
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->translationChildren = new \Doctrine\Common\Collections\ArrayCollection();
|
||||
$this->variantChildren = new \Doctrine\Common\Collections\ArrayCollection();
|
||||
$this->initializeProjects();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(PageRepository::class)
|
||||
->addIndex(['alias'], 'page_alias_search');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->addField('title', 'string');
|
||||
|
||||
$builder->addField('alias', 'string');
|
||||
|
||||
$builder->addNullableField('template', 'string');
|
||||
|
||||
$builder->createField('customHtml', 'text')
|
||||
->columnName('custom_html')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('content', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addPublishDates();
|
||||
|
||||
$builder->addField('hits', 'integer');
|
||||
|
||||
$builder->createField('uniqueHits', 'integer')
|
||||
->columnName('unique_hits')
|
||||
->build();
|
||||
|
||||
$builder->createField('variantHits', 'integer')
|
||||
->columnName('variant_hits')
|
||||
->build();
|
||||
|
||||
$builder->addField('revision', 'integer');
|
||||
|
||||
$builder->createField('metaDescription', 'string')
|
||||
->columnName('meta_description')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('headScript', 'text')
|
||||
->columnName('head_script')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('footerScript', 'text')
|
||||
->columnName('footer_script')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('redirectType', 'string')
|
||||
->columnName('redirect_type')
|
||||
->nullable()
|
||||
->length(100)
|
||||
->build();
|
||||
|
||||
$builder->createField('redirectUrl', 'string')
|
||||
->columnName('redirect_url')
|
||||
->nullable()
|
||||
->length(2048)
|
||||
->build();
|
||||
|
||||
$builder->addCategory();
|
||||
|
||||
$builder->createField('isPreferenceCenter', 'boolean')
|
||||
->columnName('is_preference_center')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('noIndex', 'boolean')
|
||||
->columnName('no_index')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createOneToOne('draft', PageDraft::class)
|
||||
->mappedBy('page')
|
||||
->fetchExtraLazy()
|
||||
->cascadeAll()
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('publicPreview', Types::BOOLEAN, 'public_preview');
|
||||
|
||||
self::addTranslationMetadata($builder, self::class);
|
||||
self::addVariantMetadata($builder, self::class);
|
||||
static::addUuidField($builder);
|
||||
self::addProjectsField($builder, 'page_projects_xref', 'page_id');
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('title', new NotBlank([
|
||||
'message' => 'mautic.core.title.required',
|
||||
]));
|
||||
|
||||
$metadata->addConstraint(new Callback(
|
||||
function (Page $page, ExecutionContextInterface $context): void {
|
||||
$type = $page->getRedirectType();
|
||||
if (!is_null($type)) {
|
||||
$validator = $context->getValidator();
|
||||
$violations = $validator->validate(
|
||||
$page->getRedirectUrl(),
|
||||
[
|
||||
new Assert\Url(),
|
||||
new NotBlank(['message' => 'mautic.core.value.required']),
|
||||
],
|
||||
);
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$context->buildViolation($violation->getMessage())
|
||||
->atPath('redirectUrl')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
|
||||
if ($page->isVariant()) {
|
||||
// Get a summation of weights
|
||||
$parent = $page->getVariantParent();
|
||||
$children = $parent ? $parent->getVariantChildren() : $page->getVariantChildren();
|
||||
|
||||
$total = 0;
|
||||
foreach ($children as $child) {
|
||||
$settings = $child->getVariantSettings();
|
||||
$total += (int) $settings['weight'];
|
||||
}
|
||||
|
||||
if ($total > 100) {
|
||||
$context->buildViolation('mautic.core.variant_weights_invalid')
|
||||
->atPath('variantSettings[weight]')
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
},
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new EntityEvent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('page')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'title',
|
||||
'alias',
|
||||
'category',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'language',
|
||||
'publishUp',
|
||||
'publishDown',
|
||||
'hits',
|
||||
'uniqueHits',
|
||||
'variantHits',
|
||||
'revision',
|
||||
'metaDescription',
|
||||
'redirectType',
|
||||
'redirectUrl',
|
||||
'isPreferenceCenter',
|
||||
'noIndex',
|
||||
'variantSettings',
|
||||
'variantStartDate',
|
||||
'variantParent',
|
||||
'variantChildren',
|
||||
'translationParent',
|
||||
'translationChildren',
|
||||
'template',
|
||||
'customHtml',
|
||||
]
|
||||
)
|
||||
->setMaxDepth(1, 'variantParent')
|
||||
->setMaxDepth(1, 'variantChildren')
|
||||
->setMaxDepth(1, 'translationParent')
|
||||
->setMaxDepth(1, 'translationChildren')
|
||||
->build();
|
||||
|
||||
self::addProjectsInLoadApiMetadata($metadata, 'page');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set title.
|
||||
*
|
||||
* @param string $title
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->isChanged('title', $title);
|
||||
$this->title = $title;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get title.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set alias.
|
||||
*
|
||||
* @param string $alias
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setAlias($alias)
|
||||
{
|
||||
$this->isChanged('alias', $alias);
|
||||
$this->alias = $alias;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get alias.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAlias()
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set content.
|
||||
*
|
||||
* @param array<string> $content
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setContent($content)
|
||||
{
|
||||
$this->isChanged('content', $content);
|
||||
$this->content = $content;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set publishUp.
|
||||
*
|
||||
* @param \DateTime $publishUp
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setPublishUp($publishUp)
|
||||
{
|
||||
$this->isChanged('publishUp', $publishUp);
|
||||
$this->publishUp = $publishUp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publishUp.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishUp()
|
||||
{
|
||||
return $this->publishUp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set publishDown.
|
||||
*
|
||||
* @param \DateTime $publishDown
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setPublishDown($publishDown)
|
||||
{
|
||||
$this->isChanged('publishDown', $publishDown);
|
||||
$this->publishDown = $publishDown;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get publishDown.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getPublishDown()
|
||||
{
|
||||
return $this->publishDown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hits.
|
||||
*
|
||||
* @param int $hits
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setHits($hits)
|
||||
{
|
||||
$this->hits = $hits;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hits.
|
||||
*
|
||||
* @param bool $includeVariants
|
||||
*
|
||||
* @return int|mixed
|
||||
*/
|
||||
public function getHits($includeVariants = false)
|
||||
{
|
||||
return ($includeVariants) ? $this->getAccumulativeVariantCount('getHits') : $this->hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set revision.
|
||||
*
|
||||
* @param int $revision
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setRevision($revision)
|
||||
{
|
||||
$this->revision = $revision;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get revision.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getRevision()
|
||||
{
|
||||
return $this->revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set metaDescription.
|
||||
*
|
||||
* @param string $metaDescription
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setMetaDescription($metaDescription)
|
||||
{
|
||||
$this->isChanged('metaDescription', $metaDescription);
|
||||
$this->metaDescription = $metaDescription;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metaDescription.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMetaDescription()
|
||||
{
|
||||
return $this->metaDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set headScript.
|
||||
*
|
||||
* @param string $headScript
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setHeadScript($headScript)
|
||||
{
|
||||
$this->headScript = $headScript;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get headScript.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getHeadScript()
|
||||
{
|
||||
return $this->headScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set footerScript.
|
||||
*
|
||||
* @param string $footerScript
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setFooterScript($footerScript)
|
||||
{
|
||||
$this->footerScript = $footerScript;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get footerScript.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFooterScript()
|
||||
{
|
||||
return $this->footerScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?string $redirectType
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setRedirectType($redirectType)
|
||||
{
|
||||
$this->isChanged('redirectType', $redirectType);
|
||||
$this->redirectType = $redirectType;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getRedirectType()
|
||||
{
|
||||
return $this->redirectType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set redirectUrl.
|
||||
*
|
||||
* @param string $redirectUrl
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setRedirectUrl($redirectUrl)
|
||||
{
|
||||
$this->isChanged('redirectUrl', $redirectUrl);
|
||||
$this->redirectUrl = $redirectUrl;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get redirectUrl.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRedirectUrl()
|
||||
{
|
||||
return $this->redirectUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set category.
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setCategory(?Category $category = null)
|
||||
{
|
||||
$this->isChanged('category', $category);
|
||||
$this->category = $category;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get category.
|
||||
*
|
||||
* @return Category
|
||||
*/
|
||||
public function getCategory()
|
||||
{
|
||||
return $this->category;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|null $isPreferenceCenter
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setIsPreferenceCenter($isPreferenceCenter)
|
||||
{
|
||||
$sanitizedValue = null === $isPreferenceCenter ? null : (bool) $isPreferenceCenter;
|
||||
$this->isChanged('isPreferenceCenter', $sanitizedValue);
|
||||
$this->isPreferenceCenter = $sanitizedValue;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getIsPreferenceCenter()
|
||||
{
|
||||
return $this->isPreferenceCenter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool|null $noIndex
|
||||
*/
|
||||
public function setNoIndex($noIndex): void
|
||||
{
|
||||
$sanitizedValue = null === $noIndex ? null : (bool) $noIndex;
|
||||
$this->isChanged('noIndex', $sanitizedValue);
|
||||
$this->noIndex = $sanitizedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getNoIndex()
|
||||
{
|
||||
return $this->noIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set sessionId.
|
||||
*
|
||||
* @param string $id
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setSessionId($id)
|
||||
{
|
||||
$this->sessionId = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get sessionId.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSessionId()
|
||||
{
|
||||
return $this->sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set template.
|
||||
*
|
||||
* @param string $template
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setTemplate($template)
|
||||
{
|
||||
$this->isChanged('template', $template);
|
||||
$this->template = $template;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTemplate()
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
$getter = 'get'.ucfirst($prop);
|
||||
$current = $this->$getter();
|
||||
|
||||
if ('translationParent' == $prop || 'variantParent' == $prop || 'category' == $prop) {
|
||||
$currentId = ($current) ? $current->getId() : '';
|
||||
$newId = ($val) ? $val->getId() : null;
|
||||
if ($currentId != $newId) {
|
||||
$this->changes[$prop] = [$currentId, $newId];
|
||||
}
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set uniqueHits.
|
||||
*
|
||||
* @param int $uniqueHits
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function setUniqueHits($uniqueHits)
|
||||
{
|
||||
$this->uniqueHits = $uniqueHits;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uniqueHits.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUniqueHits($includeVariants = false)
|
||||
{
|
||||
return ($includeVariants) ? $this->getAccumulativeVariantCount('getUniqueHits') : $this->uniqueHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $includeVariants
|
||||
*
|
||||
* @return int|mixed
|
||||
*/
|
||||
public function getVariantHits($includeVariants = false)
|
||||
{
|
||||
return ($includeVariants) ? $this->getAccumulativeVariantCount('getVariantHits') : $this->variantHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $variantHits
|
||||
*/
|
||||
public function setVariantHits($variantHits): void
|
||||
{
|
||||
$this->variantHits = $variantHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCustomHtml()
|
||||
{
|
||||
return $this->customHtml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $customHtml
|
||||
*/
|
||||
public function setCustomHtml($customHtml): void
|
||||
{
|
||||
$this->customHtml = $customHtml;
|
||||
}
|
||||
|
||||
public function hasDraft(): bool
|
||||
{
|
||||
return !is_null($this->getDraft());
|
||||
}
|
||||
|
||||
public function getDraftContent(): ?string
|
||||
{
|
||||
return $this->hasDraft() ? $this->getDraft()->getHtml() : null;
|
||||
}
|
||||
|
||||
public function getDraft(): ?PageDraft
|
||||
{
|
||||
return $this->draft;
|
||||
}
|
||||
|
||||
public function setDraft(?PageDraft $draft): void
|
||||
{
|
||||
$this->draft = $draft;
|
||||
}
|
||||
|
||||
public function getIsClone(): bool
|
||||
{
|
||||
return $this->isCloned;
|
||||
}
|
||||
|
||||
public function getCloneObjectId(): int
|
||||
{
|
||||
return $this->cloneObjectId;
|
||||
}
|
||||
|
||||
public function getPublicPreview(): bool
|
||||
{
|
||||
return $this->publicPreview;
|
||||
}
|
||||
|
||||
public function isPublicPreview(): bool
|
||||
{
|
||||
return $this->publicPreview;
|
||||
}
|
||||
|
||||
public function setPublicPreview(bool $publicPreview): self
|
||||
{
|
||||
$this->isChanged('publicPreview', $publicPreview);
|
||||
$this->publicPreview = $publicPreview;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isDuplicate(): bool
|
||||
{
|
||||
return $this->isDuplicate;
|
||||
}
|
||||
|
||||
public function setIsDuplicate(bool $isDuplicate): void
|
||||
{
|
||||
$this->isDuplicate = $isDuplicate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class PageDraft
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const TABLE_NAME = 'pages_draft';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public const REGEX_DECODE_AMPERSAND = '/((https?|ftps?):\/\/)([a-zA-Z0-9-\.{}]*[a-zA-Z0-9=}]*)(\??)([^\s\"\]]+)?/i';
|
||||
|
||||
private ?int $id = null;
|
||||
|
||||
public function __construct(
|
||||
private Page $page,
|
||||
private ?string $html = null,
|
||||
private ?string $template = null,
|
||||
private bool $publicPreview = true,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(PageDraftRepository::class)
|
||||
->addLifecycleEvent('cleanUrlsInContent', Events::preUpdate)
|
||||
->addLifecycleEvent('cleanUrlsInContent', Events::prePersist);
|
||||
|
||||
$builder->addId();
|
||||
$builder->addNullableField('html', Types::TEXT);
|
||||
$builder->addNullableField('template', Types::STRING);
|
||||
$builder->createField('publicPreview', Types::BOOLEAN)
|
||||
->columnName('public_preview')
|
||||
->nullable(false)
|
||||
->option('default', 1)
|
||||
->build();
|
||||
|
||||
$builder->createOneToOne('page', Page::class)
|
||||
->inversedBy('draft')
|
||||
->addJoinColumn('page_id', 'id', false)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle callback to clean URLs in the content.
|
||||
*/
|
||||
public function cleanUrlsInContent(): void
|
||||
{
|
||||
$this->html = $this->decodeAmpersands((string) $this->html);
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(?int $id): void
|
||||
{
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getPage(): Page
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function getHtml(): ?string
|
||||
{
|
||||
return $this->html;
|
||||
}
|
||||
|
||||
public function setPage(Page $page): void
|
||||
{
|
||||
$this->page = $page;
|
||||
}
|
||||
|
||||
public function setHtml(?string $html): void
|
||||
{
|
||||
$this->html = $html;
|
||||
}
|
||||
|
||||
public function getTemplate(): ?string
|
||||
{
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
public function setTemplate(?string $template): void
|
||||
{
|
||||
$this->template = $template;
|
||||
}
|
||||
|
||||
public function isPublicPreview(): bool
|
||||
{
|
||||
return (bool) $this->publicPreview;
|
||||
}
|
||||
|
||||
public function setPublicPreview(bool $publicPreview): void
|
||||
{
|
||||
$this->publicPreview = $publicPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check all links in content and decode &
|
||||
* This even works with double encoded ampersands.
|
||||
*/
|
||||
private function decodeAmpersands(string $content): string
|
||||
{
|
||||
if (!preg_match_all(self::REGEX_DECODE_AMPERSAND, $content, $matches)) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
foreach ($matches[0] as $url) {
|
||||
$newUrl = $url;
|
||||
while (str_contains($newUrl, '&')) {
|
||||
$newUrl = str_replace('&', '&', $newUrl);
|
||||
}
|
||||
$content = str_replace($url, $newUrl, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
class PageDraftRepository extends CommonRepository
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\ProjectBundle\Entity\ProjectRepositoryTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Page>
|
||||
*/
|
||||
class PageRepository extends CommonRepository
|
||||
{
|
||||
use ProjectRepositoryTrait;
|
||||
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$select = ['p'];
|
||||
|
||||
if (!empty($args['submissionCount'])) {
|
||||
// use a subquery to get a count of submissions otherwise doctrine will not pull all of the results
|
||||
$sq = $this->_em->createQueryBuilder()
|
||||
->select('count(fs.id)')
|
||||
->from(\Mautic\FormBundle\Entity\Submission::class, 'fs')
|
||||
->where('fs.page = p');
|
||||
|
||||
$select[] = '('.$sq->getDql().') as submission_count';
|
||||
}
|
||||
|
||||
$q = $this->createQueryBuilder('p')
|
||||
->select($select)
|
||||
->leftJoin('p.category', 'c');
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $alias
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function checkPageUniqueAlias($alias, $ignoreIds = [])
|
||||
{
|
||||
$q = $this->createQueryBuilder('e')
|
||||
->select('count(e.id) as alias_count')
|
||||
->where('e.alias = :alias');
|
||||
$q->setParameter('alias', $alias);
|
||||
|
||||
if (!empty($ignoreIds)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->notIn('e.id', ':ignoreIds')
|
||||
)
|
||||
->setParameter('ignoreIds', $ignoreIds);
|
||||
}
|
||||
|
||||
$results = $q->getQuery()->getSingleResult();
|
||||
|
||||
return $results['alias_count'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param bool $viewOther
|
||||
* @param string|bool $topLevel
|
||||
* @param array $ignoreIds
|
||||
* @param array $extraColumns
|
||||
* @param bool $publishedOnly
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPageList($search = '', $limit = 10, $start = 0, $viewOther = false, $topLevel = false, $ignoreIds = [], $extraColumns = [], $publishedOnly = false)
|
||||
{
|
||||
$q = $this->createQueryBuilder('p');
|
||||
$q->select(sprintf('partial p.{id, title, language, alias %s}', empty($extraColumns) ? '' : ','.implode(',', $extraColumns)));
|
||||
|
||||
if (!empty($search)) {
|
||||
$q->andWhere($q->expr()->like('p.title', ':search'))
|
||||
->setParameter('search', "{$search}%");
|
||||
}
|
||||
|
||||
if (!$viewOther) {
|
||||
$q->andWhere($q->expr()->eq('p.createdBy', ':id'))
|
||||
->setParameter('id', $this->currentUser->getId());
|
||||
}
|
||||
|
||||
if ('translation' == $topLevel) {
|
||||
// only get top level pages
|
||||
$q->andWhere($q->expr()->isNull('p.translationParent'));
|
||||
} elseif ('variant' == $topLevel) {
|
||||
$q->andWhere($q->expr()->isNull('p.variantParent'));
|
||||
}
|
||||
|
||||
if (!empty($ignoreIds)) {
|
||||
$q->andWhere($q->expr()->notIn('p.id', ':pageIds'))
|
||||
->setParameter('pageIds', $ignoreIds);
|
||||
}
|
||||
|
||||
if ($publishedOnly) {
|
||||
$q->andWhere($q->expr()->eq('p.isPublished', 1));
|
||||
}
|
||||
|
||||
$q->orderBy('p.title');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'p.title',
|
||||
'p.alias',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
[$expr, $parameters] = $this->addStandardSearchCommandWhereClause($q, $filter);
|
||||
if ($expr) {
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.page.searchcommand.isexpired'):
|
||||
case $this->translator->trans('mautic.page.searchcommand.isexpired', [], null, 'en_US'):
|
||||
$expr = sprintf(
|
||||
"(p.isPublished = :%1\$s AND p.publishDown IS NOT NULL AND p.publishDown <> '' AND p.publishDown < CURRENT_TIMESTAMP())",
|
||||
$unique
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.page.searchcommand.ispending'):
|
||||
case $this->translator->trans('mautic.page.searchcommand.ispending', [], null, 'en_US'):
|
||||
$expr = sprintf(
|
||||
"(p.isPublished = :%1\$s AND p.publishUp IS NOT NULL AND p.publishUp <> '' AND p.publishUp > CURRENT_TIMESTAMP())",
|
||||
$unique
|
||||
);
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.lang'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.lang', [], null, 'en_US'):
|
||||
$langUnique = $this->generateRandomParameterName();
|
||||
$langValue = $filter->string.'_%';
|
||||
$forceParameters = [
|
||||
$langUnique => $langValue,
|
||||
$unique => $filter->string,
|
||||
];
|
||||
$expr = '('.$q->expr()->eq('p.language', ":$unique").' OR '.$q->expr()->like('p.language', ":$langUnique").')';
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.page.searchcommand.isprefcenter'):
|
||||
case $this->translator->trans('mautic.page.searchcommand.isprefcenter', [], null, 'en_US'):
|
||||
$expr = $q->expr()->eq('p.isPreferenceCenter', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.project.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.project.searchcommand.name', [], null, 'en_US'):
|
||||
return $this->handleProjectFilter(
|
||||
$this->_em->getConnection()->createQueryBuilder(),
|
||||
'page_id',
|
||||
'page_projects_xref',
|
||||
$this->getTableAlias(),
|
||||
$filter->string,
|
||||
$filter->not
|
||||
);
|
||||
}
|
||||
|
||||
if ($expr && $filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.core.searchcommand.isuncategorized',
|
||||
'mautic.core.searchcommand.ismine',
|
||||
'mautic.page.searchcommand.isexpired',
|
||||
'mautic.page.searchcommand.ispending',
|
||||
'mautic.core.searchcommand.category',
|
||||
'mautic.core.searchcommand.lang',
|
||||
'mautic.page.searchcommand.isprefcenter',
|
||||
'mautic.project.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['p.title', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'p';
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets variant_start_date and variant_hits.
|
||||
*/
|
||||
public function resetVariants($relatedIds, $date): void
|
||||
{
|
||||
if (!is_array($relatedIds)) {
|
||||
$relatedIds = [(int) $relatedIds];
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$qb->update(MAUTIC_TABLE_PREFIX.'pages')
|
||||
->set('variant_hits', 0)
|
||||
->set('variant_start_date', ':date')
|
||||
->setParameter('date', $date)
|
||||
->where(
|
||||
$qb->expr()->in('id', $relatedIds)
|
||||
)
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Up the hit count.
|
||||
*
|
||||
* @param int $increaseBy
|
||||
* @param bool|false $unique
|
||||
* @param bool|false $variant
|
||||
*/
|
||||
public function upHitCount($id, $increaseBy = 1, $unique = false, $variant = false): void
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'pages')
|
||||
->set('hits', 'hits + '.(int) $increaseBy)
|
||||
->where('id = '.(int) $id);
|
||||
|
||||
if ($unique) {
|
||||
$q->set('unique_hits', 'unique_hits + '.(int) $increaseBy);
|
||||
}
|
||||
|
||||
if ($variant) {
|
||||
$q->set('variant_hits', 'variant_hits + '.(int) $increaseBy);
|
||||
}
|
||||
|
||||
$q->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
|
||||
class Redirect extends FormEntity
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $redirectId;
|
||||
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $hits = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $uniqueHits = 0;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int, Trackable>
|
||||
*/
|
||||
private $trackables;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->trackables = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('page_redirects')
|
||||
->setCustomRepositoryClass(RedirectRepository::class);
|
||||
|
||||
$builder->addBigIntIdField();
|
||||
|
||||
$builder->createField('redirectId', 'string')
|
||||
->columnName('redirect_id')
|
||||
->length(25)
|
||||
->build();
|
||||
|
||||
$builder->addField('url', 'text');
|
||||
|
||||
$builder->addField('hits', 'integer');
|
||||
|
||||
$builder->createField('uniqueHits', 'integer')
|
||||
->columnName('unique_hits')
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('trackables', 'Trackable')
|
||||
->mappedBy('redirect')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('redirect')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'redirectId',
|
||||
'url',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'hits',
|
||||
'uniqueHits',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return (int) $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getRedirectId()
|
||||
{
|
||||
return $this->redirectId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $redirectId
|
||||
*/
|
||||
public function setRedirectId($redirectId = null): void
|
||||
{
|
||||
if (null === $redirectId) {
|
||||
$redirectId = substr(hash('sha1', uniqid(mt_rand())), 0, 25);
|
||||
}
|
||||
$this->redirectId = $redirectId;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return trim($this->url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
*/
|
||||
public function setUrl($url): void
|
||||
{
|
||||
$this->url = trim($url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hits.
|
||||
*
|
||||
* @param int $hits
|
||||
*/
|
||||
public function setHits($hits): Redirect
|
||||
{
|
||||
$this->hits = $hits;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hits.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getHits()
|
||||
{
|
||||
return $this->hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set uniqueHits.
|
||||
*
|
||||
* @param int $uniqueHits
|
||||
*/
|
||||
public function setUniqueHits($uniqueHits): Redirect
|
||||
{
|
||||
$this->uniqueHits = $uniqueHits;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get uniqueHits.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUniqueHits()
|
||||
{
|
||||
return $this->uniqueHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ArrayCollection
|
||||
*/
|
||||
public function getTrackableList()
|
||||
{
|
||||
return $this->trackables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ArrayCollection $trackables
|
||||
*
|
||||
* @return Redirect
|
||||
*/
|
||||
public function setTrackables($trackables)
|
||||
{
|
||||
$this->trackables = $trackables;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Redirect>
|
||||
*/
|
||||
class RedirectRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function findByUrls(array $urls)
|
||||
{
|
||||
$q = $this->createQueryBuilder('r');
|
||||
|
||||
$expr = $q->expr()->andX(
|
||||
$q->expr()->in('r.url', ':urls')
|
||||
);
|
||||
|
||||
$q->where($expr)
|
||||
->setParameter('urls', $urls);
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Up the hit count.
|
||||
*
|
||||
* @param int $increaseBy
|
||||
* @param bool|false $unique
|
||||
*/
|
||||
public function upHitCount($id, $increaseBy = 1, $unique = false): void
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'page_redirects')
|
||||
->set('hits', 'hits + '.(int) $increaseBy)
|
||||
->where('id = '.(int) $id);
|
||||
|
||||
if ($unique) {
|
||||
$q->set('unique_hits', 'unique_hits + '.(int) $increaseBy);
|
||||
}
|
||||
|
||||
$q->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $limit
|
||||
* @param int|null $createdByUserId
|
||||
* @param int|null $companyId
|
||||
* @param int|null $campaignId
|
||||
* @param int|null $segmentId
|
||||
*/
|
||||
public function getMostHitEmailRedirects(
|
||||
$limit,
|
||||
\DateTime $dateFrom,
|
||||
\DateTime $dateTo,
|
||||
$createdByUserId = null,
|
||||
$companyId = null,
|
||||
$campaignId = null,
|
||||
$segmentId = null,
|
||||
): array {
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$q->addSelect('pr.url')
|
||||
->addSelect('count(ph.id) as hits')
|
||||
->addSelect('count(distinct ph.tracking_id) as unique_hits')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_hits', 'ph')
|
||||
->join('ph', MAUTIC_TABLE_PREFIX.'page_redirects', 'pr', 'pr.id = ph.redirect_id')
|
||||
->join('ph', MAUTIC_TABLE_PREFIX.'email_stats', 'es', 'ph.source = \'email\' and ph.source_id = es.email_id and ph.lead_id = es.lead_id')
|
||||
->join('es', MAUTIC_TABLE_PREFIX.'emails', 'e', 'es.email_id = e.id')
|
||||
->addSelect('e.id AS email_id')
|
||||
->addSelect('e.name AS email_name');
|
||||
|
||||
if (null !== $createdByUserId) {
|
||||
$q->andWhere('e.created_by = :userId')
|
||||
->setParameter('userId', $createdByUserId);
|
||||
}
|
||||
|
||||
$q->andWhere('ph.date_hit BETWEEN :dateFrom AND :dateTo')
|
||||
->setParameter('dateFrom', $dateFrom->format('Y-m-d H:i:s'))
|
||||
->setParameter('dateTo', $dateTo->format('Y-m-d H:i:s'));
|
||||
|
||||
$q->leftJoin('es', MAUTIC_TABLE_PREFIX.'campaign_events', 'ce', 'es.source = "campaign.event" and es.source_id = ce.id')
|
||||
->leftJoin('ce', MAUTIC_TABLE_PREFIX.'campaigns', 'campaign', 'ce.campaign_id = campaign.id')
|
||||
->addSelect('campaign.id AS campaign_id')
|
||||
->addSelect('campaign.name AS campaign_name');
|
||||
|
||||
if (null !== $campaignId) {
|
||||
$q->andWhere('ce.campaign_id = :campaignId')
|
||||
->setParameter('campaignId', $campaignId);
|
||||
}
|
||||
|
||||
if (!empty($companyId)) {
|
||||
$sb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$sb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'companies_leads', 'cl')
|
||||
->where(
|
||||
$sb->expr()->and(
|
||||
$sb->expr()->eq('cl.company_id', ':companyId'),
|
||||
$sb->expr()->eq('cl.lead_id', 'ph.lead_id')
|
||||
)
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('EXISTS (%s)', $sb->getSQL())
|
||||
)
|
||||
->setParameter('companyId', $companyId);
|
||||
}
|
||||
|
||||
if (null !== $segmentId) {
|
||||
$sb = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$sb->select('null')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'lll')
|
||||
->where(
|
||||
$sb->expr()->and(
|
||||
$sb->expr()->eq('lll.leadlist_id', ':segmentId'),
|
||||
$sb->expr()->eq('lll.lead_id', 'ph.lead_id'),
|
||||
$sb->expr()->eq('lll.manually_removed', 0)
|
||||
)
|
||||
);
|
||||
|
||||
$q->andWhere(
|
||||
sprintf('EXISTS (%s)', $sb->getSQL())
|
||||
)
|
||||
->setParameter('segmentId', $segmentId);
|
||||
}
|
||||
|
||||
$q->groupBy('pr.id, pr.url, e.id, e.name, campaign.id, campaign.name');
|
||||
|
||||
$q->setMaxResults($limit);
|
||||
|
||||
$q->orderBy('hits', 'DESC');
|
||||
|
||||
return $q->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class Trackable
|
||||
{
|
||||
/**
|
||||
* @var Redirect
|
||||
*/
|
||||
private $redirect;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $channelId;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $hits = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $uniqueHits = 0;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('channel_url_trackables')
|
||||
->setCustomRepositoryClass(TrackableRepository::class)
|
||||
->addIndex(['channel', 'channel_id'], 'channel_url_trackable_search');
|
||||
|
||||
$builder->createManyToOne('redirect', Redirect::class)
|
||||
->addJoinColumn('redirect_id', 'id', true, false, 'CASCADE')
|
||||
->cascadePersist()
|
||||
->inversedBy('trackables')
|
||||
->isPrimaryKey()
|
||||
->build();
|
||||
|
||||
$builder->createField('channelId', 'integer')
|
||||
->columnName('channel_id')
|
||||
->makePrimaryKey()
|
||||
->build();
|
||||
|
||||
$builder->addField('channel', 'string');
|
||||
|
||||
$builder->addField('hits', 'integer');
|
||||
|
||||
$builder->addNamedField('uniqueHits', 'integer', 'unique_hits');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('trackable')
|
||||
->addListProperties(
|
||||
[
|
||||
'redirect',
|
||||
'channelId',
|
||||
'channel',
|
||||
'hits',
|
||||
'uniqueHits',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Redirect
|
||||
*/
|
||||
public function getRedirect()
|
||||
{
|
||||
return $this->redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Trackable
|
||||
*/
|
||||
public function setRedirect(Redirect $redirect)
|
||||
{
|
||||
$this->redirect = $redirect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return Trackable
|
||||
*/
|
||||
public function setChannel($channel)
|
||||
{
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $channelId
|
||||
*
|
||||
* @return Trackable
|
||||
*/
|
||||
public function setChannelId($channelId)
|
||||
{
|
||||
$this->channelId = $channelId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getHits()
|
||||
{
|
||||
return $this->hits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $hits
|
||||
*
|
||||
* @return Trackable
|
||||
*/
|
||||
public function setHits($hits)
|
||||
{
|
||||
$this->hits = $hits;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getUniqueHits()
|
||||
{
|
||||
return $this->uniqueHits;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $uniqueHits
|
||||
*
|
||||
* @return Trackable
|
||||
*/
|
||||
public function setUniqueHits($uniqueHits)
|
||||
{
|
||||
$this->uniqueHits = $uniqueHits;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Trackable>
|
||||
*/
|
||||
class TrackableRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Find redirects that are trackable.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function findByChannel($channel, $channelId): array
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$tableAlias = $this->getTableAlias();
|
||||
|
||||
return $q->select('r.redirect_id, r.url, r.id, '.$tableAlias.'.hits, '.$tableAlias.'.unique_hits')
|
||||
->from(MAUTIC_TABLE_PREFIX.'page_redirects', 'r')
|
||||
->innerJoin('r', MAUTIC_TABLE_PREFIX.'channel_url_trackables', $tableAlias,
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('r.id', 't.redirect_id'),
|
||||
$q->expr()->eq('t.channel', ':channel'),
|
||||
$q->expr()->eq('t.channel_id', (int) $channelId)
|
||||
)
|
||||
)
|
||||
->setParameter('channel', $channel)
|
||||
->orderBy('r.url')
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Trackable by Redirect URL.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findByUrl($url, $channel, $channelId)
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$q = $this->createQueryBuilder($alias)
|
||||
->innerJoin("$alias.redirect", 'r');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq("$alias.channel", ':channel'),
|
||||
$q->expr()->eq("$alias.channelId", (int) $channelId),
|
||||
$q->expr()->eq('r.url', ':url')
|
||||
)
|
||||
)
|
||||
->setParameter('url', $url)
|
||||
->setParameter('channel', $channel);
|
||||
|
||||
$result = $q->getQuery()->getResult();
|
||||
|
||||
return ($result) ? $result[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of Trackable entities by Redirect URLs.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findByUrls(array $urls, $channel, $channelId)
|
||||
{
|
||||
$alias = $this->getTableAlias();
|
||||
$q = $this->createQueryBuilder($alias)
|
||||
->innerJoin("$alias.redirect", 'r');
|
||||
|
||||
$q->where(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq("$alias.channel", ':channel'),
|
||||
$q->expr()->eq("$alias.channelId", (int) $channelId),
|
||||
$q->expr()->in('r.url', ':urls')
|
||||
)
|
||||
)
|
||||
->setParameter('urls', $urls)
|
||||
->setParameter('channel', $channel);
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Up the hit count.
|
||||
*
|
||||
* @param int $increaseBy
|
||||
* @param bool $unique
|
||||
*/
|
||||
public function upHitCount($redirectId, $channel, $channelId, $increaseBy = 1, $unique = false): void
|
||||
{
|
||||
$q = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'channel_url_trackables')
|
||||
->set('hits', 'hits + '.(int) $increaseBy)
|
||||
->where(
|
||||
$q->expr()->and(
|
||||
$q->expr()->eq('redirect_id', (int) $redirectId),
|
||||
$q->expr()->eq('channel', ':channel'),
|
||||
$q->expr()->eq('channel_id', (int) $channelId)
|
||||
)
|
||||
)
|
||||
->setParameter('channel', $channel);
|
||||
|
||||
if ($unique) {
|
||||
$q->set('unique_hits', 'unique_hits + '.(int) $increaseBy);
|
||||
}
|
||||
|
||||
$q->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hit count.
|
||||
*
|
||||
* @param bool $combined
|
||||
* @param string $countColumn
|
||||
*
|
||||
* @return array|int
|
||||
*/
|
||||
public function getCount($channel, $channelIds, $listId, ?ChartQuery $chartQuery = null, $combined = false, $countColumn = 'ph.id')
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder()
|
||||
->select('count('.$countColumn.') as click_count')
|
||||
->from(MAUTIC_TABLE_PREFIX.'channel_url_trackables', 'cut')
|
||||
->innerJoin('cut', MAUTIC_TABLE_PREFIX.'page_hits', 'ph', 'ph.redirect_id = cut.redirect_id AND ph.source = cut.channel AND ph.source_id = cut.channel_id');
|
||||
|
||||
$q->where(
|
||||
'cut.channel = :channel'
|
||||
)->setParameter('channel', $channel);
|
||||
|
||||
if ($channelIds) {
|
||||
if (!is_array($channelIds)) {
|
||||
$channelIds = [(int) $channelIds];
|
||||
}
|
||||
$q->andWhere(
|
||||
$q->expr()->in('cut.channel_id', $channelIds)
|
||||
);
|
||||
}
|
||||
|
||||
if ($listId) {
|
||||
if (!$combined) {
|
||||
$q->innerJoin('ph', MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'cs', 'cs.lead_id = ph.lead_id');
|
||||
|
||||
if (true === $listId) {
|
||||
$q->addSelect('cs.leadlist_id')
|
||||
->groupBy('cs.leadlist_id');
|
||||
} elseif (is_array($listId)) {
|
||||
$q->andWhere(
|
||||
$q->expr()->in('cs.leadlist_id', array_map('intval', $listId))
|
||||
);
|
||||
|
||||
$q->addSelect('cs.leadlist_id')
|
||||
->groupBy('cs.leadlist_id');
|
||||
} else {
|
||||
$q->andWhere('cs.leadlist_id = :list_id')
|
||||
->setParameter('list_id', $listId);
|
||||
}
|
||||
} else {
|
||||
$subQ = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
$subQ->select('distinct(list.lead_id)')
|
||||
->from(MAUTIC_TABLE_PREFIX.'lead_lists_leads', 'list')
|
||||
->andWhere(
|
||||
$q->expr()->in('list.leadlist_id', array_map('intval', $listId))
|
||||
);
|
||||
|
||||
$q->innerJoin('ph', sprintf('(%s)', $subQ->getSQL()), 'cs', 'cs.lead_id = ph.lead_id');
|
||||
}
|
||||
}
|
||||
|
||||
if ($chartQuery) {
|
||||
$chartQuery->applyDateFilters($q, 'date_hit', 'ph');
|
||||
}
|
||||
|
||||
$results = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
if ((true === $listId || is_array($listId)) && !$combined) {
|
||||
// Return array of results
|
||||
$byList = [];
|
||||
foreach ($results as $result) {
|
||||
$byList[$result['leadlist_id']] = $result['click_count'];
|
||||
}
|
||||
|
||||
return $byList;
|
||||
}
|
||||
|
||||
return (isset($results[0])) ? $results[0]['click_count'] : 0;
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 't';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\IpAddress;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
|
||||
class VideoHit
|
||||
{
|
||||
public const TABLE_NAME = 'video_hits';
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $guid;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateHit;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
private $dateLeft;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $timeWatched;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $duration;
|
||||
|
||||
/**
|
||||
* @var Redirect
|
||||
*/
|
||||
private $redirect;
|
||||
|
||||
/**
|
||||
* @var Lead|null
|
||||
*/
|
||||
private $lead;
|
||||
|
||||
/**
|
||||
* @var IpAddress|null
|
||||
*/
|
||||
private $ipAddress;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $country;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $region;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $city;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $isp;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $organization;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $code;
|
||||
|
||||
private $referer;
|
||||
|
||||
private $url;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $userAgent;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $remoteHost;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $pageLanguage;
|
||||
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
private $browserLanguages = [];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $channel;
|
||||
|
||||
/**
|
||||
* @var int|null
|
||||
*/
|
||||
private $channelId;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $query = [];
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(self::TABLE_NAME)
|
||||
->setCustomRepositoryClass(VideoHitRepository::class)
|
||||
->addIndex(['date_hit'], 'video_date_hit')
|
||||
->addIndex(['channel', 'channel_id'], 'video_channel_search')
|
||||
->addIndex(['guid', 'lead_id'], 'video_guid_lead_search');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->createField('dateHit', 'datetime')
|
||||
->columnName('date_hit')
|
||||
->build();
|
||||
|
||||
$builder->createField('dateLeft', 'datetime')
|
||||
->columnName('date_left')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addLead(true, 'SET NULL');
|
||||
|
||||
$builder->addIpAddress(true);
|
||||
|
||||
$builder->createField('country', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('region', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('city', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('isp', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('organization', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addField('code', 'integer');
|
||||
|
||||
$builder->createField('referer', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('url', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('userAgent', 'text')
|
||||
->columnName('user_agent')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('remoteHost', 'string')
|
||||
->columnName('remote_host')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('guid', 'string')
|
||||
->columnName('guid')
|
||||
->build();
|
||||
|
||||
$builder->createField('pageLanguage', 'string')
|
||||
->columnName('page_language')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('browserLanguages', 'array')
|
||||
->columnName('browser_languages')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('channel', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('channelId', 'integer')
|
||||
->columnName('channel_id')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('timeWatched', 'integer')
|
||||
->columnName('time_watched')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('duration', 'integer')
|
||||
->columnName('duration')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->addNullableField('query', 'array');
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('hit')
|
||||
->addProperties(
|
||||
[
|
||||
'dateHit',
|
||||
'dateLeft',
|
||||
'lead',
|
||||
'ipAddress',
|
||||
'country',
|
||||
'region',
|
||||
'city',
|
||||
'isp',
|
||||
'code',
|
||||
'referer',
|
||||
'url',
|
||||
'urlTitle',
|
||||
'userAgent',
|
||||
'remoteHost',
|
||||
'pageLanguage',
|
||||
'browserLanguages',
|
||||
'source',
|
||||
'sourceId',
|
||||
'query',
|
||||
'timeWatched',
|
||||
'guid',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set dateHit.
|
||||
*
|
||||
* @param \DateTime $dateHit
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setDateHit($dateHit)
|
||||
{
|
||||
$this->dateHit = $dateHit;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get dateHit.
|
||||
*
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateHit()
|
||||
{
|
||||
return $this->dateHit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface
|
||||
*/
|
||||
public function getDateLeft()
|
||||
{
|
||||
return $this->dateLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime $dateLeft
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setDateLeft($dateLeft)
|
||||
{
|
||||
$this->dateLeft = $dateLeft;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set country.
|
||||
*
|
||||
* @param string $country
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setCountry($country)
|
||||
{
|
||||
$this->country = $country;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get country.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCountry()
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set region.
|
||||
*
|
||||
* @param string $region
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setRegion($region)
|
||||
{
|
||||
$this->region = $region;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get region.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRegion()
|
||||
{
|
||||
return $this->region;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set city.
|
||||
*
|
||||
* @param string $city
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setCity($city)
|
||||
{
|
||||
$this->city = $city;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get city.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCity()
|
||||
{
|
||||
return $this->city;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isp.
|
||||
*
|
||||
* @param string $isp
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setIsp($isp)
|
||||
{
|
||||
$this->isp = $isp;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isp.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIsp()
|
||||
{
|
||||
return $this->isp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set organization.
|
||||
*
|
||||
* @param string $organization
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setOrganization($organization)
|
||||
{
|
||||
$this->organization = $organization;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get organization.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOrganization()
|
||||
{
|
||||
return $this->organization;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set code.
|
||||
*
|
||||
* @param int $code
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setCode($code)
|
||||
{
|
||||
$this->code = $code;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get code.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCode()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set referer.
|
||||
*
|
||||
* @param string $referer
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setReferer($referer)
|
||||
{
|
||||
$this->referer = $referer;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get referer.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getReferer()
|
||||
{
|
||||
return $this->referer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set url.
|
||||
*
|
||||
* @param string $url
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setUrl($url)
|
||||
{
|
||||
$this->url = $url;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set userAgent.
|
||||
*
|
||||
* @param string $userAgent
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setUserAgent($userAgent)
|
||||
{
|
||||
$this->userAgent = $userAgent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get userAgent.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUserAgent()
|
||||
{
|
||||
return $this->userAgent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set remoteHost.
|
||||
*
|
||||
* @param string $remoteHost
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setRemoteHost($remoteHost)
|
||||
{
|
||||
$this->remoteHost = $remoteHost;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remoteHost.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRemoteHost()
|
||||
{
|
||||
return $this->remoteHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setIpAddress(IpAddress $ipAddress)
|
||||
{
|
||||
$this->ipAddress = $ipAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IpAddress
|
||||
*/
|
||||
public function getIpAddress()
|
||||
{
|
||||
return $this->ipAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set pageLanguage.
|
||||
*
|
||||
* @param string $pageLanguage
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setPageLanguage($pageLanguage)
|
||||
{
|
||||
$this->pageLanguage = $pageLanguage;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pageLanguage.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPageLanguage()
|
||||
{
|
||||
return $this->pageLanguage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set browserLanguages.
|
||||
*
|
||||
* @param array<string> $browserLanguages
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setBrowserLanguages($browserLanguages)
|
||||
{
|
||||
$this->browserLanguages = $browserLanguages;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get browserLanguages.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function getBrowserLanguages()
|
||||
{
|
||||
return $this->browserLanguages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setLead(Lead $lead)
|
||||
{
|
||||
$this->lead = $lead;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getChannel()
|
||||
{
|
||||
return $this->channel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $channel
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setChannel($channel)
|
||||
{
|
||||
$this->channel = $channel;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getChannelId()
|
||||
{
|
||||
return $this->channelId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $channelId
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setChannelId($channelId)
|
||||
{
|
||||
$this->channelId = (int) $channelId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Redirect
|
||||
*/
|
||||
public function getRedirect()
|
||||
{
|
||||
return $this->redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setRedirect(Redirect $redirect)
|
||||
{
|
||||
$this->redirect = $redirect;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getQuery()
|
||||
{
|
||||
return $this->query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $query
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setQuery($query)
|
||||
{
|
||||
$this->query = $query;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getTimeWatched()
|
||||
{
|
||||
return $this->timeWatched;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setTimeWatched($timeWatched)
|
||||
{
|
||||
$this->timeWatched = $timeWatched;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getGuid()
|
||||
{
|
||||
return $this->guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guid
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setGuid($guid)
|
||||
{
|
||||
$this->guid = $guid;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getDuration()
|
||||
{
|
||||
return $this->duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $duration
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function setDuration($duration)
|
||||
{
|
||||
$this->duration = $duration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Entity\TimelineTrait;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<VideoHit>
|
||||
*/
|
||||
class VideoHitRepository extends CommonRepository
|
||||
{
|
||||
use TimelineTrait;
|
||||
|
||||
/**
|
||||
* Get video hit info for lead timeline.
|
||||
*
|
||||
* @param int|null $leadId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTimelineStats($leadId = null, array $options = [])
|
||||
{
|
||||
$query = $this->getEntityManager()->getConnection()->createQueryBuilder();
|
||||
|
||||
$query->select('h.id, h.url, h.date_hit, h.time_watched, h.duration, h.referer, h.user_agent')
|
||||
->from(MAUTIC_TABLE_PREFIX.'video_hits', 'h');
|
||||
|
||||
if ($leadId) {
|
||||
$query->where($query->expr()->eq('h.lead_id', (int) $leadId));
|
||||
}
|
||||
|
||||
if (isset($options['search']) && $options['search']) {
|
||||
$query->andWhere(
|
||||
$query->expr()->like('h.url', ':search')
|
||||
)->setParameter('search', '%'.$options['search'].'%');
|
||||
}
|
||||
|
||||
return $this->getTimelineResults($query, $options, 'h.url', 'h.date_hit', [], ['date_hit'], null, 'h.id');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guid
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function getHitForLeadByGuid(Lead $lead, $guid)
|
||||
{
|
||||
$result = $this->findOneBy(['guid' => $guid, 'lead' => $lead]);
|
||||
|
||||
return $result ?: new VideoHit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lead's page hits.
|
||||
*
|
||||
* @param int $leadId
|
||||
*
|
||||
* @return array
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getLeadHits($leadId, array $options = [])
|
||||
{
|
||||
$query = $this->createQueryBuilder('h');
|
||||
$query->select('h.userAgent, h.dateHit, h.dateLeft, h.referer, h.channel, h.channelId, h.url, h.duration, h.query, h.timeWatched')
|
||||
->where('h.lead = :leadId')
|
||||
->setParameter('leadId', (int) $leadId);
|
||||
|
||||
if (isset($options['url']) && $options['url']) {
|
||||
$query->andWhere($query->expr()->eq('h.url', $query->expr()->literal($options['url'])));
|
||||
}
|
||||
|
||||
return $query->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Count stats from hit times.
|
||||
*
|
||||
* @param array $times
|
||||
*/
|
||||
public function countStats($times): array
|
||||
{
|
||||
return [
|
||||
'sum' => array_sum($times),
|
||||
'min' => count($times) ? min($times) : 0,
|
||||
'max' => count($times) ? max($times) : 0,
|
||||
'average' => count($times) ? round(array_sum($times) / count($times)) : 0,
|
||||
'count' => count($times),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of referers ordered by it's count.
|
||||
*
|
||||
* @param \Doctrine\DBAL\Query\QueryBuilder $query
|
||||
* @param int $limit
|
||||
* @param int $offset
|
||||
*
|
||||
* @throws \Doctrine\ORM\NoResultException
|
||||
* @throws \Doctrine\ORM\NonUniqueResultException
|
||||
*/
|
||||
public function getReferers($query, $limit = 10, $offset = 0): array
|
||||
{
|
||||
$query->select('h.referer, count(h.referer) as sessions')
|
||||
->groupBy('h.referer')
|
||||
->orderBy('sessions', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $query->executeQuery()->fetchAllAssociative();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates lead ID (e.g. after a lead merge).
|
||||
*/
|
||||
public function updateLead($fromLeadId, $toLeadId): void
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
$q->update(MAUTIC_TABLE_PREFIX.'video_hits')
|
||||
->set('lead_id', (int) $toLeadId)
|
||||
->where('lead_id = '.(int) $fromLeadId)
|
||||
->executeStatement();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\BuilderEvent;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
|
||||
class PageBuilderEvent extends BuilderEvent
|
||||
{
|
||||
/**
|
||||
* @return Page|null
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class PageDisplayEvent extends Event
|
||||
{
|
||||
/**
|
||||
* Preferred lead to be used in listeners.
|
||||
*/
|
||||
private ?Lead $lead = null;
|
||||
|
||||
public function __construct(
|
||||
private string $content,
|
||||
private Page $page,
|
||||
private array $params = [],
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Page entity.
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page content.
|
||||
*/
|
||||
public function getContent(): string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set page content.
|
||||
*
|
||||
* @param string $content
|
||||
*/
|
||||
public function setContent($content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get params.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParams()
|
||||
{
|
||||
return $this->params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set params.
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
public function setParams($params): void
|
||||
{
|
||||
$this->params = $params;
|
||||
}
|
||||
|
||||
public function getLead(): ?Lead
|
||||
{
|
||||
return $this->lead;
|
||||
}
|
||||
|
||||
public function setLead(Lead $lead): void
|
||||
{
|
||||
$this->lead = $lead;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
|
||||
final class PageEditSubmitEvent extends CommonEvent
|
||||
{
|
||||
public function __construct(
|
||||
private Page $previousPage,
|
||||
private Page $currentPage,
|
||||
private bool $saveAndClose,
|
||||
private bool $apply,
|
||||
private bool $saveAsDraft,
|
||||
private bool $applyDraft,
|
||||
private bool $discardDraft,
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPreviousPage(): Page
|
||||
{
|
||||
return $this->previousPage;
|
||||
}
|
||||
|
||||
public function getCurrentPage(): Page
|
||||
{
|
||||
return $this->currentPage;
|
||||
}
|
||||
|
||||
public function isSaveAndClose(): bool
|
||||
{
|
||||
return $this->saveAndClose;
|
||||
}
|
||||
|
||||
public function isApply(): bool
|
||||
{
|
||||
return $this->apply;
|
||||
}
|
||||
|
||||
public function isSaveAsDraft(): bool
|
||||
{
|
||||
return $this->saveAsDraft;
|
||||
}
|
||||
|
||||
public function isApplyDraft(): bool
|
||||
{
|
||||
return $this->applyDraft;
|
||||
}
|
||||
|
||||
public function isDiscardDraft(): bool
|
||||
{
|
||||
return $this->discardDraft;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
|
||||
class PageEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(Page $page, $isNew = false)
|
||||
{
|
||||
$this->entity = $page;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Page entity.
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Page entity.
|
||||
*/
|
||||
public function setPage(Page $page): void
|
||||
{
|
||||
$this->entity = $page;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
|
||||
class PageHitEvent extends CommonEvent
|
||||
{
|
||||
protected ?Page $page = null;
|
||||
|
||||
/**
|
||||
* @param mixed[] $clickthroughData
|
||||
* @param bool $unique
|
||||
*/
|
||||
public function __construct(
|
||||
Hit $hit,
|
||||
protected $request,
|
||||
protected $code,
|
||||
protected $clickthroughData = [],
|
||||
protected $unique = false,
|
||||
) {
|
||||
$this->entity = $hit;
|
||||
$this->page = $hit->getPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Page entity.
|
||||
*
|
||||
* @return Page
|
||||
*/
|
||||
public function getPage()
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTML code.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCode()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Hit
|
||||
*/
|
||||
public function getHit()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getClickthroughData()
|
||||
{
|
||||
return $this->clickthroughData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this page hit is unique.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isUnique()
|
||||
{
|
||||
return $this->unique;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
|
||||
class RedirectGenerationEvent extends CommonEvent
|
||||
{
|
||||
public function __construct(
|
||||
private Redirect $redirect,
|
||||
private array $clickthrough,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or overwrite a value in the clickthrough.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function setInClickthrough($key, $value): void
|
||||
{
|
||||
$this->clickthrough[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the redirect from the event.
|
||||
*
|
||||
* @return Redirect
|
||||
*/
|
||||
public function getRedirect()
|
||||
{
|
||||
return $this->redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the modified clickthrough from the event.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getClickthrough()
|
||||
{
|
||||
return $this->clickthrough;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class TrackingEvent extends Event
|
||||
{
|
||||
private ParameterBag $response;
|
||||
|
||||
public function __construct(
|
||||
private Lead $contact,
|
||||
private Request $request,
|
||||
array $mtcSessionResponses,
|
||||
) {
|
||||
$this->response = new ParameterBag($mtcSessionResponses);
|
||||
}
|
||||
|
||||
public function getContact(): Lead
|
||||
{
|
||||
return $this->contact;
|
||||
}
|
||||
|
||||
public function getRequest(): Request
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function getResponse(): ParameterBag
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class UntrackableUrlsEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $doNotTrack = [
|
||||
'{webview_url}',
|
||||
'{resubscribe_url}',
|
||||
'{unsubscribe_url}',
|
||||
'{dnc_url}',
|
||||
'{trackable=(.*?)}',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param mixed $content
|
||||
*/
|
||||
public function __construct(
|
||||
private $content,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* set a URL or token to not convert to trackables.
|
||||
*/
|
||||
public function addNonTrackable($url): void
|
||||
{
|
||||
$this->doNotTrack[] = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of non-trackables.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getDoNotTrackList(): array
|
||||
{
|
||||
return $this->doNotTrack;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getContent()
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\PageBundle\Entity\VideoHit;
|
||||
|
||||
class VideoHitEvent extends CommonEvent
|
||||
{
|
||||
public function __construct(
|
||||
VideoHit $hit,
|
||||
protected $request,
|
||||
protected $code,
|
||||
) {
|
||||
$this->entity = $hit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page request.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get HTML code.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCode()
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function getHit()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\BuildJsEvent;
|
||||
use Mautic\PageBundle\Helper\TrackingHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class BuildJsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TrackingHelper $trackingHelper,
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::BUILD_MAUTIC_JS => [
|
||||
// onBuildJs must always needs to be last to ensure setup before delivering the event
|
||||
['onBuildJs', -255],
|
||||
['onBuildJsForTrackingEvent', 256],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function onBuildJs(BuildJsEvent $event): void
|
||||
{
|
||||
$pageTrackingUrl = $this->router->generate('mautic_page_tracker', [], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
// Determine if this is https
|
||||
$parts = parse_url($pageTrackingUrl);
|
||||
$scheme = $parts['scheme'];
|
||||
$pageTrackingUrl = str_replace(['http://', 'https://'], '', $pageTrackingUrl);
|
||||
|
||||
$pageTrackingCORSUrl = str_replace(
|
||||
['http://', 'https://'],
|
||||
'',
|
||||
$this->router->generate('mautic_page_tracker_cors', [], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
);
|
||||
$contactIdUrl = str_replace(
|
||||
['http://', 'https://'],
|
||||
'',
|
||||
$this->router->generate('mautic_page_tracker_getcontact', [], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
);
|
||||
|
||||
$js = <<<JS
|
||||
(function(m, l, n, d) {
|
||||
m.pageTrackingUrl = (l.protocol == 'https:' ? 'https:' : '{$scheme}:') + '//{$pageTrackingUrl}';
|
||||
m.pageTrackingCORSUrl = (l.protocol == 'https:' ? 'https:' : '{$scheme}:') + '//{$pageTrackingCORSUrl}';
|
||||
m.contactIdUrl = (l.protocol == 'https:' ? 'https:' : '{$scheme}:') + '//{$contactIdUrl}';
|
||||
|
||||
m.getOs = function() {
|
||||
var OSName="Unknown OS";
|
||||
|
||||
if (navigator.appVersion.indexOf("Win")!=-1) OSName="Windows";
|
||||
if (navigator.appVersion.indexOf("Mac")!=-1) OSName="MacOS";
|
||||
if (navigator.appVersion.indexOf("X11")!=-1) OSName="UNIX";
|
||||
if (navigator.appVersion.indexOf("Linux")!=-1) OSName="Linux";
|
||||
|
||||
return OSName;
|
||||
}
|
||||
|
||||
m.deliverPageEvent = function(event, params) {
|
||||
if (!m.firstDeliveryMade && params['counter'] > 0) {
|
||||
// Wait for the first delivery to complete so that the tracking information is set
|
||||
setTimeout(function () {
|
||||
m.deliverPageEvent(event, params);
|
||||
}, 5);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Pre delivery events always take all known params and should use them in the request
|
||||
if (m.preEventDeliveryQueue.length && m.beforeFirstDeliveryMade === false) {
|
||||
for(var i = 0; i < m.preEventDeliveryQueue.length; i++) {
|
||||
m.preEventDeliveryQueue[i](params);
|
||||
}
|
||||
|
||||
// In case the first delivery set sid, append it
|
||||
params = m.appendTrackedContact(params);
|
||||
|
||||
m.beforeFirstDeliveryMade = true;
|
||||
}
|
||||
|
||||
MauticJS.makeCORSRequest('POST', m.pageTrackingCORSUrl, params,
|
||||
function(response) {
|
||||
MauticJS.dispatchEvent('mauticPageEventDelivered', {'event': event, 'params': params, 'response': response});
|
||||
},
|
||||
function() {
|
||||
// CORS failed so load an image
|
||||
m.buildTrackingImage(event, params);
|
||||
m.firstDeliveryMade = true;
|
||||
});
|
||||
}
|
||||
|
||||
m.buildTrackingImage = function(pageview, params) {
|
||||
delete m.trackingPixel;
|
||||
m.trackingPixel = new Image();
|
||||
|
||||
if (typeof pageview[3] === 'object') {
|
||||
var events = ['onabort', 'onerror', 'onload'];
|
||||
for (var i = 0; i < events.length; i++) {
|
||||
var e = events[i];
|
||||
if (typeof pageview[3][e] === 'function') {
|
||||
m.trackingPixel[e] = pageview[3][e];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.trackingPixel.onload = function(e) {
|
||||
MauticJS.dispatchEvent('mauticPageEventDelivered', {'event': pageview, 'params': params, 'image': true});
|
||||
};
|
||||
|
||||
m.trackingPixel.src = m.pageTrackingUrl + '?' + m.serialize(params);
|
||||
}
|
||||
|
||||
m.pageViewCounter = 0;
|
||||
m.sendPageview = function(pageview) {
|
||||
var queue = [];
|
||||
|
||||
if (!pageview) {
|
||||
if (typeof m.getInput === 'function') {
|
||||
queue = m.getInput('send', 'pageview');
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
queue.push(pageview);
|
||||
}
|
||||
|
||||
if (queue) {
|
||||
for (var i=0; i<queue.length; i++) {
|
||||
var event = queue[i];
|
||||
|
||||
var params = {
|
||||
page_title: d.title,
|
||||
page_language: n.language,
|
||||
preferred_locale: (n.language).replace('-', '_'),
|
||||
page_referrer: (d.referrer) ? d.referrer.split('/')[2] : '',
|
||||
page_url: l.href,
|
||||
counter: m.pageViewCounter,
|
||||
timezone_offset: new Date().getTimezoneOffset(),
|
||||
resolution: window.screen.width + 'x' + window.screen.height,
|
||||
platform: m.getOs(),
|
||||
do_not_track: navigator.doNotTrack == 1
|
||||
};
|
||||
|
||||
if (window.Intl && window.Intl.DateTimeFormat) {
|
||||
params.timezone = new window.Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
|
||||
params = MauticJS.appendTrackedContact(params);
|
||||
|
||||
// Merge user defined tracking pixel parameters.
|
||||
if (typeof event[2] === 'object') {
|
||||
for (var attr in event[2]) {
|
||||
params[attr] = event[2][attr];
|
||||
}
|
||||
}
|
||||
|
||||
m.deliverPageEvent(event, params);
|
||||
|
||||
m.pageViewCounter++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process pageviews after mtc.js loaded
|
||||
m.sendPageview();
|
||||
|
||||
// Process pageviews after new are added
|
||||
document.addEventListener('eventAddedToMauticQueue', function(e) {
|
||||
if (MauticJS.ensureEventContext(e, 'send', 'pageview')) {
|
||||
m.sendPageview(e.detail);
|
||||
}
|
||||
});
|
||||
})(MauticJS, location, navigator, document);
|
||||
JS;
|
||||
|
||||
$event->appendJs($js, 'Mautic Tracking Pixel');
|
||||
}
|
||||
|
||||
public function onBuildJsForTrackingEvent(BuildJsEvent $event): void
|
||||
{
|
||||
$js = '';
|
||||
|
||||
$lead = $this->trackingHelper->getLead();
|
||||
|
||||
if ($id = $this->trackingHelper->displayInitCode('google_analytics')) {
|
||||
$gtagSettings = [];
|
||||
|
||||
if ($this->trackingHelper->getAnonymizeIp()) {
|
||||
$gtagSettings['anonymize_ip'] = true;
|
||||
}
|
||||
|
||||
if ($lead && $lead->getId()) {
|
||||
$gtagSettings['user_id'] = $lead->getId();
|
||||
}
|
||||
|
||||
if (count($gtagSettings) > 0) {
|
||||
$gtagSettings = ', '.json_encode($gtagSettings);
|
||||
} else {
|
||||
$gtagSettings = '';
|
||||
}
|
||||
|
||||
$js .= <<<JS
|
||||
a = document.createElement('script');
|
||||
a.async = 1;
|
||||
a.src = 'https://www.googletagmanager.com/gtag/js?id={$id}';
|
||||
document.getElementsByTagName('head')[0].appendChild(a);
|
||||
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
gtag('config', '{$id}'{$gtagSettings});
|
||||
JS;
|
||||
}
|
||||
|
||||
if ($id = $this->trackingHelper->displayInitCode('facebook_pixel')) {
|
||||
$customMatch = [];
|
||||
if ($lead && $lead->getId()) {
|
||||
$fieldsToMatch = [
|
||||
'fn' => 'firstname',
|
||||
'ln' => 'lastname',
|
||||
'em' => 'email',
|
||||
'ph' => 'phone',
|
||||
'ct' => 'city',
|
||||
'st' => 'state',
|
||||
'zp' => 'zipcode',
|
||||
];
|
||||
foreach ($fieldsToMatch as $key => $fieldToMatch) {
|
||||
$par = 'get'.ucfirst($fieldToMatch);
|
||||
if ($value = $lead->{$par}()) {
|
||||
$customMatch[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
$customMatch = json_encode($customMatch);
|
||||
|
||||
$js .= <<<JS
|
||||
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
|
||||
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
|
||||
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
|
||||
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
|
||||
document,'script','https://connect.facebook.net/en_US/fbevents.js');
|
||||
fbq('init', '{$id}'); // Insert your pixel ID here.
|
||||
fbq('track', 'PageView', {$customMatch});
|
||||
JS;
|
||||
}
|
||||
$js .= <<<'JS_WRAP'
|
||||
MauticJS.mtcEventSet=false;
|
||||
document.addEventListener('mauticPageEventDelivered', function(e) {
|
||||
var detail = e.detail;
|
||||
if (!MauticJS.mtcEventSet && detail.response && detail.response.events) {
|
||||
MauticJS.setTrackedEvents(detail.response.events);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
MauticJS.setTrackedEvents = function(events) {
|
||||
MauticJS.mtcEventSet=true;
|
||||
if (typeof fbq !== 'undefined' && typeof events.facebook_pixel !== 'undefined') {
|
||||
var e = events.facebook_pixel;
|
||||
for(var i = 0; i < e.length; i++) {
|
||||
if(typeof e[i]['action'] !== 'undefined' && typeof e[i]['label'] !== 'undefined' )
|
||||
fbq('trackCustom', e[i]['action'], {
|
||||
eventLabel: e[i]['label']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof ga !== 'undefined' && typeof events.google_analytics !== 'undefined') {
|
||||
var e = events.google_analytics;
|
||||
for(var i = 0; i < e.length; i++) {
|
||||
if(typeof e[i]['action'] !== 'undefined' && typeof e[i]['label'] !== 'undefined' ) {
|
||||
ga('send', {
|
||||
hitType: 'event',
|
||||
eventCategory: e[i]['category'],
|
||||
eventAction: e[i]['action'],
|
||||
eventLabel: e[i]['label'],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof events.focus_item !== 'undefined') {
|
||||
var e = events.focus_item;
|
||||
for(var i = 0; i < e.length; i++) {
|
||||
if(typeof e[i]['id'] !== 'undefined' && typeof e[i]['js'] !== 'undefined' ){
|
||||
MauticJS.insertScript(e[i]['js']);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
JS_WRAP;
|
||||
$event->appendJs($js, 'Mautic 3rd party tracking pixels');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,449 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mautic\CoreBundle\Helper\BuilderTokenHelperFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\EmailBundle\EmailEvents;
|
||||
use Mautic\EmailBundle\Event\EmailBuilderEvent;
|
||||
use Mautic\EmailBundle\Event\EmailSendEvent;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Event as Events;
|
||||
use Mautic\PageBundle\Helper\TokenHelper;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use Twig\Environment;
|
||||
|
||||
final class BuilderSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private const pageTokenRegex = '{pagelink=(.*?)}';
|
||||
|
||||
private const dwcTokenRegex = '{dwc=(.*?)}';
|
||||
|
||||
private const langBarRegex = '{langbar}';
|
||||
|
||||
private const shareButtonsRegex = '{sharebuttons}';
|
||||
|
||||
private const titleRegex = '{pagetitle}';
|
||||
|
||||
private const descriptionRegex = '{pagemetadescription}';
|
||||
|
||||
public const brandName = '{brand=name}';
|
||||
|
||||
public const segmentListRegex = '{segmentlist}';
|
||||
|
||||
public const categoryListRegex = '{categorylist}';
|
||||
|
||||
public const channelfrequency = '{channelfrequency}';
|
||||
|
||||
public const preferredchannel = '{preferredchannel}';
|
||||
|
||||
public const saveprefsRegex = '{saveprefsbutton}';
|
||||
|
||||
public const successmessage = '{successmessage}';
|
||||
|
||||
public const identifierToken = '{leadidentifier}';
|
||||
|
||||
public const saveButtonContainerClass = 'prefs-saveprefs';
|
||||
|
||||
public const firstSlotAttribute = ' data-prefs-center-first="1"';
|
||||
|
||||
/**
|
||||
* @var array<string,string>
|
||||
*/
|
||||
private array $renderedContentCache = [];
|
||||
|
||||
public function __construct(private TokenHelper $tokenHelper, private IntegrationHelper $integrationHelper, private PageModel $pageModel, private BuilderTokenHelperFactory $builderTokenHelperFactory, private TranslatorInterface $translator, private Connection $connection, private Environment $twig, private CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PageEvents::PAGE_ON_DISPLAY => ['onPageDisplay', 0],
|
||||
PageEvents::PAGE_ON_BUILD => ['onPageBuild', 0],
|
||||
EmailEvents::EMAIL_ON_BUILD => ['onEmailBuild', 0],
|
||||
EmailEvents::EMAIL_ON_SEND => ['onEmailGenerate', 0],
|
||||
EmailEvents::EMAIL_ON_DISPLAY => ['onEmailGenerate', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onEmailBuild(EmailBuilderEvent $event): void
|
||||
{
|
||||
if ($event->tokensRequested([static::pageTokenRegex])) {
|
||||
$tokenHelper = $this->builderTokenHelperFactory->getBuilderTokenHelper('page');
|
||||
$event->addTokensFromHelper($tokenHelper, static::pageTokenRegex, 'title', 'id', true);
|
||||
}
|
||||
}
|
||||
|
||||
public function onEmailGenerate(EmailSendEvent $event): void
|
||||
{
|
||||
$content = $event->getContent();
|
||||
$plainText = $event->getPlainText();
|
||||
$clickthrough = $event->shouldAppendClickthrough() ? $event->generateClickthrough() : [];
|
||||
$tokens = $this->tokenHelper->findPageTokens($content.$plainText, $clickthrough);
|
||||
|
||||
$event->addTokens($tokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add forms to available page tokens.
|
||||
*/
|
||||
public function onPageBuild(Events\PageBuilderEvent $event): void
|
||||
{
|
||||
$tokenHelper = $this->builderTokenHelperFactory->getBuilderTokenHelper('page');
|
||||
|
||||
if ($event->abTestWinnerCriteriaRequested()) {
|
||||
// add AB Test Winner Criteria
|
||||
$bounceRate = [
|
||||
'group' => 'mautic.page.abtest.criteria',
|
||||
'label' => 'mautic.page.abtest.criteria.bounce',
|
||||
'event' => PageEvents::ON_DETERMINE_BOUNCE_RATE_WINNER,
|
||||
];
|
||||
$event->addAbTestWinnerCriteria('page.bouncerate', $bounceRate);
|
||||
|
||||
$dwellTime = [
|
||||
'group' => 'mautic.page.abtest.criteria',
|
||||
'label' => 'mautic.page.abtest.criteria.dwelltime',
|
||||
'event' => PageEvents::ON_DETERMINE_DWELL_TIME_WINNER,
|
||||
];
|
||||
$event->addAbTestWinnerCriteria('page.dwelltime', $dwellTime);
|
||||
}
|
||||
|
||||
if ($event->tokensRequested([static::pageTokenRegex, static::dwcTokenRegex])) {
|
||||
$event->addTokensFromHelper($tokenHelper, static::pageTokenRegex, 'title', 'id', true);
|
||||
|
||||
// add only filter based dwc tokens
|
||||
$dwcTokenHelper = $this->builderTokenHelperFactory->getBuilderTokenHelper('dynamicContent', 'dynamiccontent:dynamiccontents');
|
||||
$expr = $this->connection->createExpressionBuilder()->and('e.is_campaign_based <> 1 and e.slot_name is not null');
|
||||
$tokens = $dwcTokenHelper->getTokens(
|
||||
static::dwcTokenRegex,
|
||||
'',
|
||||
'name',
|
||||
'slot_name',
|
||||
$expr
|
||||
);
|
||||
$event->addTokens(is_array($tokens) ? $tokens : []);
|
||||
|
||||
$event->addTokens(
|
||||
$event->filterTokens(
|
||||
[
|
||||
static::langBarRegex => $this->translator->trans('mautic.page.token.lang'),
|
||||
static::shareButtonsRegex => $this->translator->trans('mautic.page.token.share'),
|
||||
static::titleRegex => $this->translator->trans('mautic.core.title'),
|
||||
static::brandName => $this->translator->trans('mautic.core.token.brand_name'),
|
||||
static::descriptionRegex => $this->translator->trans('mautic.page.form.metadescription'),
|
||||
static::segmentListRegex => $this->translator->trans('mautic.page.form.segmentlist'),
|
||||
static::categoryListRegex => $this->translator->trans('mautic.page.form.categorylist'),
|
||||
static::preferredchannel => $this->translator->trans('mautic.page.form.preferredchannel'),
|
||||
static::channelfrequency => $this->translator->trans('mautic.page.form.channelfrequency'),
|
||||
static::saveprefsRegex => $this->translator->trans('mautic.page.form.saveprefs'),
|
||||
static::successmessage => $this->translator->trans('mautic.page.form.successmessage'),
|
||||
static::identifierToken => $this->translator->trans('mautic.page.form.leadidentifier'),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function onPageDisplay(Events\PageDisplayEvent $event): void
|
||||
{
|
||||
if (empty($content = $event->getContent())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$page = $event->getPage();
|
||||
$params = $event->getParams();
|
||||
$content = $this->replaceCommonTokens($content, $page);
|
||||
|
||||
if ($page->getIsPreferenceCenter()) {
|
||||
$content = $this->handlePreferenceCenterReplacements($content, $params);
|
||||
}
|
||||
|
||||
if ($tokens = $this->tokenHelper->findPageTokens($content, ['source' => ['page', $page->getId()]])) {
|
||||
$content = str_ireplace(array_keys($tokens), $tokens, $content);
|
||||
}
|
||||
|
||||
$headCloseScripts = $page->getHeadScript();
|
||||
if ($headCloseScripts) {
|
||||
$content = str_ireplace('</head>', $headCloseScripts."\n</head>", $content);
|
||||
}
|
||||
|
||||
$bodyCloseScripts = $page->getFooterScript();
|
||||
if ($bodyCloseScripts) {
|
||||
$content = str_ireplace('</body>', $bodyCloseScripts."\n</body>", $content);
|
||||
}
|
||||
|
||||
$event->setContent($content);
|
||||
}
|
||||
|
||||
private function replaceCommonTokens(string $content, Page $page): string
|
||||
{
|
||||
return str_ireplace([
|
||||
static::langBarRegex,
|
||||
static::shareButtonsRegex,
|
||||
static::titleRegex,
|
||||
static::brandName,
|
||||
static::descriptionRegex,
|
||||
static::successmessage,
|
||||
], [
|
||||
str_contains($content, static::langBarRegex) ? $this->renderLanguageBar($page) : '',
|
||||
str_contains($content, static::shareButtonsRegex) ? $this->renderSocialShareButtons() : '',
|
||||
str_contains($content, static::titleRegex) ? $page->getTitle() : '',
|
||||
str_contains($content, static::brandName) ? $this->coreParametersHelper->get('brand_name') : '',
|
||||
str_contains($content, static::descriptionRegex) ? $page->getMetaDescription() : '',
|
||||
str_contains($content, static::successmessage) ? $this->renderSuccessMessage() : '',
|
||||
], $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
*/
|
||||
private function handlePreferenceCenterReplacements(string $content, array $params): string
|
||||
{
|
||||
$xpath = $this->createDOMXPathForContent($content);
|
||||
|
||||
$content = $this->replacePreferenceCenterTokens($xpath->document->saveHTML(), $params);
|
||||
|
||||
return $this->wrapPreferenceCenterInFormTag($content, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $params
|
||||
*/
|
||||
private function replacePreferenceCenterTokens(string $content, array $params): string
|
||||
{
|
||||
return str_ireplace([
|
||||
static::segmentListRegex,
|
||||
static::categoryListRegex,
|
||||
static::preferredchannel,
|
||||
static::channelfrequency,
|
||||
static::saveprefsRegex,
|
||||
], [
|
||||
str_contains($content, static::segmentListRegex) ? $this->renderSegmentList($params) : '',
|
||||
str_contains($content, static::categoryListRegex) ? $this->renderCategoryList($params) : '',
|
||||
str_contains($content, static::preferredchannel) ? $this->renderPreferredChannel($params) : '',
|
||||
str_contains($content, static::channelfrequency) ? $this->renderChannelFrequency($params) : '',
|
||||
str_contains($content, static::saveprefsRegex) ? $this->renderSavePrefs($params) : '',
|
||||
], $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $templateParams
|
||||
*/
|
||||
private function renderTemplate(string $templateName, array $templateParams, string $wrapperTemplate = '', string ...$wrapperTemplateValues): string
|
||||
{
|
||||
if (!empty($this->renderedContentCache[$templateName])) {
|
||||
return $this->renderedContentCache[$templateName];
|
||||
}
|
||||
|
||||
$content = trim($this->twig->render($templateName, $templateParams));
|
||||
|
||||
if ($wrapperTemplate) {
|
||||
// If the content is not empty, ensure that the $wrapperTemplate contains a place to put it.
|
||||
if (!empty($content) && !str_contains($wrapperTemplate, '{templateContent}')) {
|
||||
throw new \InvalidArgumentException('Your $wrapperTemplate must contain the string {templateContent} where you want to insert the rendered template content.');
|
||||
}
|
||||
|
||||
$content = str_replace('{templateContent}', $content, sprintf($wrapperTemplate, ...$wrapperTemplateValues));
|
||||
}
|
||||
|
||||
return $this->renderedContentCache[$templateName] = $content;
|
||||
}
|
||||
|
||||
private function renderSocialShareButtons(): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticPage/SubscribedEvents/PageToken/sharebtn_css.html.twig',
|
||||
[],
|
||||
'<div class="share-buttons">%s</div>',
|
||||
implode('', $this->integrationHelper->getShareButtons())
|
||||
);
|
||||
}
|
||||
|
||||
private function renderSegmentList(array $params): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticCore/Slots/segmentlist.html.twig',
|
||||
$params,
|
||||
'<div class="pref-segmentlist"%s>{templateContent}</div>',
|
||||
static::firstSlotAttribute
|
||||
);
|
||||
}
|
||||
|
||||
private function renderCategoryList(array $params): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticCore/Slots/categorylist.html.twig',
|
||||
$params,
|
||||
'<div class="pref-categorylist"%s>{templateContent}</div>',
|
||||
static::firstSlotAttribute
|
||||
);
|
||||
}
|
||||
|
||||
private function renderPreferredChannel(array $params): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticCore/Slots/preferredchannel.html.twig',
|
||||
$params,
|
||||
'<div class="pref-preferredchannel">{templateContent}</div>'
|
||||
);
|
||||
}
|
||||
|
||||
private function renderChannelFrequency(array $params): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticCore/Slots/channelfrequency.html.twig',
|
||||
$params,
|
||||
'<div class="pref-channelfrequency">{templateContent}</div>'
|
||||
);
|
||||
}
|
||||
|
||||
private function renderSavePrefs(array $params): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticCore/Slots/saveprefsbutton.html.twig',
|
||||
$params,
|
||||
'<div class="%s"%s>{templateContent}</div>',
|
||||
static::saveButtonContainerClass,
|
||||
static::firstSlotAttribute
|
||||
);
|
||||
}
|
||||
|
||||
private function renderSuccessMessage(): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticCore/Slots/successmessage.html.twig',
|
||||
[],
|
||||
'<div class="pref-successmessage">{templateContent}</div>'
|
||||
);
|
||||
}
|
||||
|
||||
private function renderLanguageBar(Page $page): string
|
||||
{
|
||||
return $this->renderTemplate(
|
||||
'@MauticPage/SubscribedEvents/PageToken/langbar.html.twig',
|
||||
['pages' => $this->getRelatedPagesForLanguageBar($page)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,mixed[]>
|
||||
*/
|
||||
private function getRelatedPagesForLanguageBar(Page $page): array
|
||||
{
|
||||
$related = [];
|
||||
$parent = $page->getTranslationParent();
|
||||
$children = $page->getTranslationChildren();
|
||||
|
||||
if (empty($parent) && empty($children)) {
|
||||
return $related;
|
||||
}
|
||||
|
||||
// If this page has a parent, then fetch the children from the parent
|
||||
if (!empty($parent)) {
|
||||
$children = $parent->getTranslationChildren();
|
||||
} else {
|
||||
// Otherwise this is the parent page.
|
||||
$parent = $page;
|
||||
}
|
||||
|
||||
if (empty($children)) {
|
||||
return $related;
|
||||
}
|
||||
|
||||
if ($parent instanceof Page) {
|
||||
$related[$parent->getId()] = $this->buildRelatedArrayForPage($parent);
|
||||
}
|
||||
|
||||
foreach ($children as $child) {
|
||||
$related[$child->getId()] = $this->buildRelatedArrayForPage($child);
|
||||
}
|
||||
|
||||
uasort($related, fn ($a, $b): int => strnatcasecmp($a['lang'], $b['lang']));
|
||||
|
||||
return $related;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,string>
|
||||
*/
|
||||
private function buildRelatedArrayForPage(Page $page): array
|
||||
{
|
||||
$language = $page->getLanguage();
|
||||
$translated = $this->translator->trans('mautic.page.lang.'.$language);
|
||||
|
||||
if ($translated == 'mautic.page.lang.'.$language) {
|
||||
$translated = $language;
|
||||
}
|
||||
|
||||
return [
|
||||
'lang' => $translated,
|
||||
// Add ntrd to not auto redirect to another language
|
||||
'url' => $this->pageModel->generateUrl($page, false).'?ntrd=1',
|
||||
];
|
||||
}
|
||||
|
||||
private function createDOMXPathForContent(string $content): \DOMXPath
|
||||
{
|
||||
$domDocument = new \DOMDocument('1.0', 'utf-8');
|
||||
$domDocument->loadHTML(mb_encode_numericentity($content, [0x80, 0x10FFFF, 0, 0xFFFFF], 'UTF-8'), LIBXML_NOERROR);
|
||||
|
||||
return new \DOMXPath($domDocument);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $params
|
||||
*/
|
||||
private function wrapPreferenceCenterInFormTag(string $content, array $params): string
|
||||
{
|
||||
if (!isset($params['startform']) || !str_contains($content, 'data-prefs-center')) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$xpath = $this->createDOMXPathForContent($content);
|
||||
$node = $this->getFirstNodeThatContainsAPreferenceCenterToken($xpath);
|
||||
|
||||
if (null === $node) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$parentNode = $this->getFirstParentNodeThatContainsAllFormInputs($node);
|
||||
|
||||
$parentNode->insertBefore(new \DOMElement('startform'), $parentNode->firstChild);
|
||||
$parentNode->appendChild(new \DOMElement('endform'));
|
||||
|
||||
return str_replace(['<startform></startform>', '<endform></endform>'], [$params['startform'], '</form>'], $xpath->document->saveHTML());
|
||||
}
|
||||
|
||||
private function getFirstNodeThatContainsAPreferenceCenterToken(\DOMXPath $xpath): ?\DOMNode
|
||||
{
|
||||
$nodeList = $xpath->query('//*[@data-prefs-center-first="1"]');
|
||||
|
||||
if (false !== $nodeList) {
|
||||
return $nodeList->item(0);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getFirstParentNodeThatContainsAllFormInputs(\DOMNode $node): \DOMNode
|
||||
{
|
||||
$content = implode('', array_map([$node->ownerDocument, 'saveHTML'], iterator_to_array($node->childNodes)));
|
||||
|
||||
// Check if the save button exists in the content. If not, try again with the parentNode.
|
||||
if (!str_contains($content, static::saveButtonContainerClass)) {
|
||||
if (null === $node->parentNode) {
|
||||
throw new \RuntimeException("Can't get parent node of #document. Did you forget to insert a save button in your preference center form?");
|
||||
}
|
||||
|
||||
return $this->getFirstParentNodeThatContainsAllFormInputs($node->parentNode);
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CampaignBundle\CampaignEvents;
|
||||
use Mautic\CampaignBundle\Event\CampaignBuilderEvent;
|
||||
use Mautic\CampaignBundle\Event\CampaignExecutionEvent;
|
||||
use Mautic\CampaignBundle\Executioner\RealTimeExecutioner;
|
||||
use Mautic\LeadBundle\Form\Type\CampaignEventLeadDeviceType;
|
||||
use Mautic\LeadBundle\Model\LeadModel;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Event\PageHitEvent;
|
||||
use Mautic\PageBundle\Form\Type\CampaignEventPageHitType;
|
||||
use Mautic\PageBundle\Form\Type\TrackingPixelSendType;
|
||||
use Mautic\PageBundle\Helper\TrackingHelper;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class CampaignSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private LeadModel $leadModel,
|
||||
private TrackingHelper $trackingHelper,
|
||||
private RealTimeExecutioner $realTimeExecutioner,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CampaignEvents::CAMPAIGN_ON_BUILD => ['onCampaignBuild', 0],
|
||||
PageEvents::PAGE_ON_HIT => ['onPageHit', 0],
|
||||
PageEvents::ON_CAMPAIGN_TRIGGER_DECISION => [
|
||||
['onCampaignTriggerDecision', 0],
|
||||
['onCampaignTriggerDecisionDeviceHit', 1],
|
||||
],
|
||||
PageEvents::ON_CAMPAIGN_TRIGGER_ACTION => ['onCampaignTriggerAction', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event triggers and actions.
|
||||
*/
|
||||
public function onCampaignBuild(CampaignBuilderEvent $event): void
|
||||
{
|
||||
// Add trigger
|
||||
$pageHitTrigger = [
|
||||
'label' => 'mautic.page.campaign.event.pagehit',
|
||||
'description' => 'mautic.page.campaign.event.pagehit_descr',
|
||||
'formType' => CampaignEventPageHitType::class,
|
||||
'eventName' => PageEvents::ON_CAMPAIGN_TRIGGER_DECISION,
|
||||
'channel' => 'page',
|
||||
'channelIdField' => 'pages',
|
||||
];
|
||||
$event->addDecision('page.pagehit', $pageHitTrigger);
|
||||
|
||||
// Add trigger
|
||||
$deviceHitTrigger = [
|
||||
'label' => 'mautic.page.campaign.event.devicehit',
|
||||
'description' => 'mautic.page.campaign.event.devicehit_descr',
|
||||
'formType' => CampaignEventLeadDeviceType::class,
|
||||
'eventName' => PageEvents::ON_CAMPAIGN_TRIGGER_DECISION,
|
||||
'channel' => 'page',
|
||||
'channelIdField' => 'pages',
|
||||
];
|
||||
$event->addDecision('page.devicehit', $deviceHitTrigger);
|
||||
|
||||
$trackingServices = $this->trackingHelper->getEnabledServices();
|
||||
if (!empty($trackingServices)) {
|
||||
$action = [
|
||||
'label' => 'mautic.page.tracking.pixel.event.send',
|
||||
'description' => 'mautic.page.tracking.pixel.event.send_desc',
|
||||
'eventName' => PageEvents::ON_CAMPAIGN_TRIGGER_ACTION,
|
||||
'formType' => TrackingPixelSendType::class,
|
||||
'connectionRestrictions' => [
|
||||
'anchor' => [
|
||||
'decision.inaction',
|
||||
],
|
||||
'source' => [
|
||||
'decision' => [
|
||||
'page.pagehit',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
$event->addAction('tracking.pixel.send', $action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger actions for page hits.
|
||||
*/
|
||||
public function onPageHit(PageHitEvent $event): void
|
||||
{
|
||||
$hit = $event->getHit();
|
||||
$channel = 'page';
|
||||
$channelId = null;
|
||||
if ($redirect = $hit->getRedirect()) {
|
||||
$channel = 'page.redirect';
|
||||
$channelId = $redirect->getId();
|
||||
} elseif ($page = $hit->getPage()) {
|
||||
$channelId = $page->getId();
|
||||
}
|
||||
$this->realTimeExecutioner->execute('page.pagehit', $hit, $channel, $channelId);
|
||||
$this->realTimeExecutioner->execute('page.devicehit', $hit, $channel, $channelId);
|
||||
}
|
||||
|
||||
public function onCampaignTriggerDecisionDeviceHit(CampaignExecutionEvent $event)
|
||||
{
|
||||
$eventDetails = $event->getEventDetails();
|
||||
$config = $event->getConfig();
|
||||
$lead = $event->getLead();
|
||||
|
||||
if (!$event->checkContext('page.devicehit')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$deviceRepo = $this->leadModel->getDeviceRepository();
|
||||
$result = false;
|
||||
|
||||
$deviceId = $eventDetails->getDeviceStat() ? $eventDetails->getDeviceStat()->getId() : null;
|
||||
$deviceType = $config['device_type'];
|
||||
$deviceBrands = $config['device_brand'];
|
||||
$deviceOs = $config['device_os'];
|
||||
|
||||
if (!empty($deviceType)) {
|
||||
$result = false;
|
||||
if (!empty($deviceRepo->getDevice($lead, $deviceType, null, null, null, $deviceId))) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($deviceBrands)) {
|
||||
$result = false;
|
||||
if (!empty($deviceRepo->getDevice($lead, null, $deviceBrands, null, null, $deviceId))) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($deviceOs)) {
|
||||
$result = false;
|
||||
if (!empty($deviceRepo->getDevice($lead, null, null, null, $deviceOs, $deviceId))) {
|
||||
$result = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $event->setResult($result);
|
||||
}
|
||||
|
||||
public function onCampaignTriggerDecision(CampaignExecutionEvent $event)
|
||||
{
|
||||
$eventDetails = $event->getEventDetails();
|
||||
$config = $event->getConfig();
|
||||
|
||||
if (!$event->checkContext('page.pagehit')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (null == $eventDetails) {
|
||||
return true;
|
||||
}
|
||||
$pageHit = $eventDetails->getPage();
|
||||
|
||||
// Check Landing Pages
|
||||
if ($pageHit instanceof Page) {
|
||||
[$parent, $children] = $pageHit->getVariants();
|
||||
// use the parent (self or configured parent)
|
||||
$pageHitId = $parent->getId();
|
||||
} else {
|
||||
$pageHitId = 0;
|
||||
}
|
||||
|
||||
$limitToPages = $config['pages'] ?? [];
|
||||
|
||||
$urlMatches = [];
|
||||
|
||||
// Check Landing Pages URL or Tracing Pixel URL
|
||||
if (isset($config['url']) && $config['url']) {
|
||||
$pageUrl = html_entity_decode($eventDetails->getUrl());
|
||||
$limitToUrls = explode(',', $config['url']);
|
||||
|
||||
foreach ($limitToUrls as $url) {
|
||||
$url = html_entity_decode(trim($url));
|
||||
$urlMatches[$url] = fnmatch($url, $pageUrl);
|
||||
}
|
||||
}
|
||||
|
||||
$refererMatches = [];
|
||||
|
||||
// Check Landing Pages URL or Tracing Pixel URL
|
||||
if (isset($config['referer']) && $config['referer']) {
|
||||
$refererUrl = html_entity_decode($eventDetails->getReferer());
|
||||
$limitToReferers = explode(',', $config['referer']);
|
||||
|
||||
foreach ($limitToReferers as $referer) {
|
||||
$referer = html_entity_decode(trim($referer));
|
||||
$refererMatches[$referer] = fnmatch($referer, $refererUrl);
|
||||
}
|
||||
}
|
||||
|
||||
// **Page hit is true if:**
|
||||
// 1. no landing page is set and no URL rule is set
|
||||
$applyToAny = (empty($config['url']) && empty($config['referer']) && empty($limitToPages));
|
||||
|
||||
// 2. some landing pages are set and page ID match
|
||||
$langingPageIsHit = (!empty($limitToPages) && in_array($pageHitId, $limitToPages));
|
||||
|
||||
// 3. URL rule is set and match with URL hit
|
||||
$urlIsHit = (!empty($config['url']) && in_array(true, $urlMatches));
|
||||
|
||||
// 3. URL rule is set and match with URL hit
|
||||
$refererIsHit = (!empty($config['referer']) && in_array(true, $refererMatches));
|
||||
|
||||
if ($applyToAny || $langingPageIsHit || $urlIsHit || $refererIsHit) {
|
||||
return $event->setResult(true);
|
||||
}
|
||||
|
||||
return $event->setResult(false);
|
||||
}
|
||||
|
||||
public function onCampaignTriggerAction(CampaignExecutionEvent $event)
|
||||
{
|
||||
$config = $event->getConfig();
|
||||
if (empty($config['services'])) {
|
||||
return $event->setResult(false);
|
||||
}
|
||||
|
||||
$values = [];
|
||||
foreach ($config['services'] as $service) {
|
||||
$values[$service][] = ['category' => $config['category'], 'action' => $config['action'], 'label' => $config['label']];
|
||||
}
|
||||
$this->trackingHelper->updateCacheItem($values);
|
||||
|
||||
return $event->setResult(true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
|
||||
use Mautic\ConfigBundle\Event\ConfigEvent;
|
||||
use Mautic\PageBundle\Form\Type\ConfigTrackingPageType;
|
||||
use Mautic\PageBundle\Form\Type\ConfigType;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ConfigSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ConfigEvents::CONFIG_ON_GENERATE => [
|
||||
['onConfigGenerate', 0],
|
||||
['onConfigGenerateTracking', 0],
|
||||
],
|
||||
ConfigEvents::CONFIG_PRE_SAVE => ['onConfigSave', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onConfigGenerate(ConfigBuilderEvent $event): void
|
||||
{
|
||||
$event->addForm([
|
||||
'bundle' => 'PageBundle',
|
||||
'formAlias' => 'pageconfig',
|
||||
'formType' => ConfigType::class,
|
||||
'formTheme' => '@MauticPage/FormTheme/Config/_config_pageconfig_widget.html.twig',
|
||||
// parameters must be defined directly in case there are 2 config forms per bundle.
|
||||
// $event->getParametersFromConfig('MauticPageBundle') would return all params for PageBundle
|
||||
// and trackingconfig form would overwrote values in the pageconfig form. See #5559.
|
||||
'parameters' => [
|
||||
'cat_in_page_url' => false,
|
||||
'google_analytics' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function onConfigGenerateTracking(ConfigBuilderEvent $event): void
|
||||
{
|
||||
$event->addForm([
|
||||
'bundle' => 'PageBundle',
|
||||
'formAlias' => 'trackingconfig',
|
||||
'formType' => ConfigTrackingPageType::class,
|
||||
'formTheme' => '@MauticPage/FormTheme/Config/_config_trackingconfig_widget.html.twig',
|
||||
// parameters defined this way because of the reason as above.
|
||||
'parameters' => [
|
||||
'anonymize_ip' => false,
|
||||
'track_contact_by_ip' => false,
|
||||
'facebook_pixel_id' => null,
|
||||
'facebook_pixel_trackingpage_enabled' => false,
|
||||
'facebook_pixel_landingpage_enabled' => false,
|
||||
'google_analytics_id' => null,
|
||||
'google_analytics_trackingpage_enabled' => false,
|
||||
'google_analytics_landingpage_enabled' => false,
|
||||
'google_analytics_anonymize_ip' => false,
|
||||
'do_not_track_404_anonymous' => false,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function onConfigSave(ConfigEvent $event): void
|
||||
{
|
||||
$values = $event->getConfig();
|
||||
|
||||
if (!empty($values['pageconfig']['google_analytics'])) {
|
||||
$values['pageconfig']['google_analytics'] = htmlspecialchars($values['pageconfig']['google_analytics']);
|
||||
$event->setConfig($values);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\DashboardBundle\Event\WidgetDetailEvent;
|
||||
use Mautic\DashboardBundle\EventListener\DashboardSubscriber as MainDashboardSubscriber;
|
||||
use Mautic\PageBundle\Form\Type\DashboardHitsInTimeWidgetType;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class DashboardSubscriber extends MainDashboardSubscriber
|
||||
{
|
||||
/**
|
||||
* Define the name of the bundle/category of the widget(s).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bundle = 'page';
|
||||
|
||||
/**
|
||||
* Define the widget(s).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $types = [
|
||||
'page.hits.in.time' => [
|
||||
'formAlias' => DashboardHitsInTimeWidgetType::class,
|
||||
],
|
||||
'unique.vs.returning.leads' => [],
|
||||
'dwell.times' => [],
|
||||
'popular.pages' => [],
|
||||
'created.pages' => [],
|
||||
'device.granularity' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Define permissions to see those widgets.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $permissions = [
|
||||
'page:pages:viewown',
|
||||
'page:pages:viewother',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
protected PageModel $pageModel,
|
||||
protected RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a widget detail when needed.
|
||||
*/
|
||||
public function onWidgetDetailGenerate(WidgetDetailEvent $event): void
|
||||
{
|
||||
$this->checkPermissions($event);
|
||||
$canViewOthers = $event->hasPermission('page:pages:viewother');
|
||||
|
||||
if ('page.hits.in.time' == $event->getType()) {
|
||||
$widget = $event->getWidget();
|
||||
$params = $widget->getParams();
|
||||
|
||||
if (isset($params['flag'])) {
|
||||
$params['filter']['flag'] = $params['flag'];
|
||||
}
|
||||
|
||||
if (!$event->isCached()) {
|
||||
$event->setTemplateData([
|
||||
'chartType' => 'line',
|
||||
'chartHeight' => $widget->getHeight() - 80,
|
||||
'chartData' => $this->pageModel->getHitsLineChartData(
|
||||
$params['timeUnit'],
|
||||
$params['dateFrom'],
|
||||
$params['dateTo'],
|
||||
$params['dateFormat'],
|
||||
$params['filter'],
|
||||
$canViewOthers
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('unique.vs.returning.leads' == $event->getType()) {
|
||||
if (!$event->isCached()) {
|
||||
$params = $event->getWidget()->getParams();
|
||||
$event->setTemplateData([
|
||||
'chartType' => 'pie',
|
||||
'chartHeight' => $event->getWidget()->getHeight() - 80,
|
||||
'chartData' => $this->pageModel->getUniqueVsReturningPieChartData($params['dateFrom'], $params['dateTo'], $canViewOthers),
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('dwell.times' == $event->getType()) {
|
||||
if (!$event->isCached()) {
|
||||
$params = $event->getWidget()->getParams();
|
||||
$event->setTemplateData([
|
||||
'chartType' => 'pie',
|
||||
'chartHeight' => $event->getWidget()->getHeight() - 80,
|
||||
'chartData' => $this->pageModel->getDwellTimesPieChartData($params['dateFrom'], $params['dateTo'], [], $canViewOthers),
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('popular.pages' == $event->getType()) {
|
||||
if (!$event->isCached()) {
|
||||
$params = $event->getWidget()->getParams();
|
||||
|
||||
if (empty($params['limit'])) {
|
||||
// Count the pages limit from the widget height
|
||||
$limit = round((($event->getWidget()->getHeight() - 80) / 35) - 1);
|
||||
} else {
|
||||
$limit = $params['limit'];
|
||||
}
|
||||
|
||||
$pages = $this->pageModel->getPopularPages($limit, $params['dateFrom'], $params['dateTo'], [], $canViewOthers);
|
||||
$items = [];
|
||||
|
||||
// Build table rows with links
|
||||
foreach ($pages as &$page) {
|
||||
$pageUrl = $this->router->generate('mautic_page_action', ['objectAction' => 'view', 'objectId' => $page['id']]);
|
||||
$row = [
|
||||
[
|
||||
'value' => $page['title'],
|
||||
'type' => 'link',
|
||||
'link' => $pageUrl,
|
||||
],
|
||||
[
|
||||
'value' => $page['hits'],
|
||||
],
|
||||
];
|
||||
$items[] = $row;
|
||||
}
|
||||
|
||||
$event->setTemplateData([
|
||||
'headItems' => [
|
||||
'mautic.dashboard.label.title',
|
||||
'mautic.dashboard.label.hits',
|
||||
],
|
||||
'bodyItems' => $items,
|
||||
'raw' => $pages,
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/table.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('created.pages' == $event->getType()) {
|
||||
if (!$event->isCached()) {
|
||||
$params = $event->getWidget()->getParams();
|
||||
|
||||
if (empty($params['limit'])) {
|
||||
// Count the pages limit from the widget height
|
||||
$limit = round((($event->getWidget()->getHeight() - 80) / 35) - 1);
|
||||
} else {
|
||||
$limit = $params['limit'];
|
||||
}
|
||||
|
||||
$pages = $this->pageModel->getPageList($limit, $params['dateFrom'], $params['dateTo'], [], $canViewOthers);
|
||||
$items = [];
|
||||
|
||||
// Build table rows with links
|
||||
foreach ($pages as &$page) {
|
||||
$pageUrl = $this->router->generate('mautic_page_action', ['objectAction' => 'view', 'objectId' => $page['id']]);
|
||||
$row = [
|
||||
[
|
||||
'value' => $page['name'],
|
||||
'type' => 'link',
|
||||
'link' => $pageUrl,
|
||||
],
|
||||
];
|
||||
$items[] = $row;
|
||||
}
|
||||
|
||||
$event->setTemplateData([
|
||||
'headItems' => [
|
||||
'mautic.dashboard.label.title',
|
||||
],
|
||||
'bodyItems' => $items,
|
||||
'raw' => $pages,
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/table.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
|
||||
if ('device.granularity' == $event->getType()) {
|
||||
$widget = $event->getWidget();
|
||||
$params = $widget->getParams();
|
||||
|
||||
if (!$event->isCached()) {
|
||||
$event->setTemplateData([
|
||||
'chartType' => 'pie',
|
||||
'chartHeight' => $widget->getHeight() - 80,
|
||||
'chartData' => $this->pageModel->getDeviceGranularityData(
|
||||
$params['dateFrom'],
|
||||
$params['dateTo'],
|
||||
[],
|
||||
$canViewOthers
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
$event->setTemplate('@MauticCore/Helper/chart.html.twig');
|
||||
$event->stopPropagation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\DateTime\DateTimeToken;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Event\PageBuilderEvent;
|
||||
use Mautic\PageBundle\Event\PageDisplayEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DateTimeTokenSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private DateTimeToken $dateTokenHelper,
|
||||
private CorePermissions $security,
|
||||
private ContactTracker $contactTracker,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PageEvents::PAGE_ON_BUILD => ['onPageBuild', 0],
|
||||
PageEvents::PAGE_ON_DISPLAY => ['onPageDisplay', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onPageBuild(PageBuilderEvent $event): void
|
||||
{
|
||||
$event->addToken('{today}', $this->translator->trans('mautic.email.token.today'));
|
||||
}
|
||||
|
||||
public function onPageDisplay(PageDisplayEvent $event): void
|
||||
{
|
||||
$content = $event->getContent();
|
||||
$contact = $this->security->isAnonymous() ? $this->contactTracker->getContact() : null;
|
||||
|
||||
$tokenList = $this->dateTokenHelper->getTokens($content, $contact ? $contact->getTimezone() : null);
|
||||
$event->setContent(str_replace(array_keys($tokenList), $tokenList, $content));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Event\DetermineWinnerEvent;
|
||||
use Mautic\PageBundle\Entity\HitRepository;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class DetermineWinnerSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private HitRepository $hitRepository,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PageEvents::ON_DETERMINE_BOUNCE_RATE_WINNER => ['onDetermineBounceRateWinner', 0],
|
||||
PageEvents::ON_DETERMINE_DWELL_TIME_WINNER => ['onDetermineDwellTimeWinner', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the winner of A/B test based on bounce rates.
|
||||
*/
|
||||
public function onDetermineBounceRateWinner(DetermineWinnerEvent $event): void
|
||||
{
|
||||
// find the hits that did not go any further
|
||||
$parent = $event->getParameters()['parent'];
|
||||
$children = $event->getParameters()['children'];
|
||||
$pageIds = $parent->getRelatedEntityIds();
|
||||
$startDate = $parent->getVariantStartDate();
|
||||
|
||||
if (null != $startDate && !empty($pageIds)) {
|
||||
// get their bounce rates
|
||||
$counts = $this->hitRepository->getBounces($pageIds, $startDate, true);
|
||||
if ($counts) {
|
||||
// Group by translation
|
||||
$combined = [
|
||||
$parent->getId() => $counts[$parent->getId()],
|
||||
];
|
||||
|
||||
if ($parent->hasTranslations()) {
|
||||
$translations = $parent->getTranslationChildren()->getKeys();
|
||||
|
||||
foreach ($translations as $translation) {
|
||||
$combined[$parent->getId()]['bounces'] += $counts[$translation]['bounces'];
|
||||
$combined[$parent->getId()]['totalHits'] += $counts[$translation]['totalHits'];
|
||||
$combined[$parent->getId()]['rate'] = ($combined[$parent->getId()]['totalHits']) ? round(
|
||||
($combined[$parent->getId()]['bounces'] / $combined[$parent->getId()]['totalHits']) * 100,
|
||||
2
|
||||
) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($children as $child) {
|
||||
$combined[$child->getId()] = $counts[$child->getId()];
|
||||
|
||||
if ($child->hasTranslations()) {
|
||||
$translations = $child->getTranslationChildren()->getKeys();
|
||||
foreach ($translations as $translation) {
|
||||
$combined[$child->getId()]['bounces'] += $counts[$translation]['bounces'];
|
||||
$combined[$child->getId()]['totalHits'] += $counts[$translation]['totalHits'];
|
||||
$combined[$child->getId()]['rate'] = ($combined[$child->getId()]['totalHits']) ? round(
|
||||
($combined[$child->getId()]['bounces'] / $combined[$child->getId()]['totalHits']) * 100,
|
||||
2
|
||||
) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
unset($counts);
|
||||
|
||||
// let's arrange by rate
|
||||
$rates = [];
|
||||
$support['data'] = [];
|
||||
$support['labels'] = [];
|
||||
$bounceLabel = $this->translator->trans('mautic.page.abtest.label.bounces');
|
||||
|
||||
foreach ($combined as $pid => $stats) {
|
||||
$rates[$pid] = $stats['rate'];
|
||||
$support['data'][$bounceLabel][] = $rates[$pid];
|
||||
$support['labels'][] = $pid.':'.$stats['title'];
|
||||
}
|
||||
|
||||
$min = min($rates);
|
||||
$support['step_width'] = (ceil($min / 10) * 10);
|
||||
|
||||
$winners = ($min >= 0) ? array_keys($rates, $min) : [];
|
||||
|
||||
$event->setAbTestResults([
|
||||
'winners' => $winners,
|
||||
'support' => $support,
|
||||
'basedOn' => 'page.bouncerate',
|
||||
'supportTemplate' => '@MauticPage/SubscribedEvents/AbTest/bargraph.html.twig',
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$event->setAbTestResults([
|
||||
'winners' => [],
|
||||
'support' => [],
|
||||
'basedOn' => 'page.bouncerate',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the winner of A/B test based on dwell time rates.
|
||||
*/
|
||||
public function onDetermineDwellTimeWinner(DetermineWinnerEvent $event): void
|
||||
{
|
||||
// find the hits that did not go any further
|
||||
$parent = $event->getParameters()['parent'];
|
||||
$pageIds = $parent->getRelatedEntityIds();
|
||||
$startDate = $parent->getVariantStartDate();
|
||||
|
||||
if (null != $startDate && !empty($pageIds)) {
|
||||
// get their bounce rates
|
||||
$counts = $this->hitRepository->getDwellTimesForPages($pageIds, ['fromDate' => $startDate]);
|
||||
$support = [];
|
||||
|
||||
if ($counts) {
|
||||
// in order to get a fair grade, we have to compare the averages here since a page that is only shown
|
||||
// 25% of the time will have a significantly lower sum than a page shown 75% of the time
|
||||
$avgs = [];
|
||||
$support['data'] = [];
|
||||
$support['labels'] = [];
|
||||
foreach ($counts as $pid => $stats) {
|
||||
$avgs[$pid] = $stats['average'];
|
||||
$support['data'][$this->translator->trans('mautic.page.abtest.label.dewlltime.average')][] = $stats['average'];
|
||||
$support['labels'][] = $pid.':'.$stats['title'];
|
||||
}
|
||||
|
||||
// set max for scales
|
||||
$max = max($avgs);
|
||||
$support['step_width'] = (ceil($max / 10) * 10);
|
||||
|
||||
// get the page ids with the greatest average dwell time
|
||||
$winners = ($max > 0) ? array_keys($avgs, $max) : [];
|
||||
|
||||
$event->setAbTestResults([
|
||||
'winners' => $winners,
|
||||
'support' => $support,
|
||||
'basedOn' => 'page.dwelltime',
|
||||
'supportTemplate' => '@MauticPage/SubscribedEvents/AbTest/bargraph.html.twig',
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$event->setAbTestResults([
|
||||
'winners' => [],
|
||||
'support' => [],
|
||||
'basedOn' => 'page.dwelltime',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\EventListener\ChannelTrait;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\LeadBundle\Event\LeadChangeEvent;
|
||||
use Mautic\LeadBundle\Event\LeadMergeEvent;
|
||||
use Mautic\LeadBundle\Event\LeadTimelineEvent;
|
||||
use Mautic\LeadBundle\LeadEvents;
|
||||
use Mautic\LeadBundle\Model\ChannelTimelineInterface;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Mautic\PageBundle\Model\VideoModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class LeadSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
use ChannelTrait;
|
||||
|
||||
/**
|
||||
* @var RouterInterface
|
||||
*/
|
||||
private $router;
|
||||
|
||||
/**
|
||||
* @param ModelFactory<object> $modelFactory
|
||||
*/
|
||||
public function __construct(
|
||||
private PageModel $pageModel,
|
||||
private VideoModel $pageVideoModel,
|
||||
private TranslatorInterface $translator,
|
||||
RouterInterface $router,
|
||||
ModelFactory $modelFactory,
|
||||
) {
|
||||
$this->router = $router;
|
||||
|
||||
$this->setModelFactory($modelFactory);
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
LeadEvents::TIMELINE_ON_GENERATE => [
|
||||
['onTimelineGenerate', 0],
|
||||
['onTimelineGenerateVideo', 0],
|
||||
],
|
||||
LeadEvents::CURRENT_LEAD_CHANGED => ['onLeadChange', 0],
|
||||
LeadEvents::LEAD_POST_MERGE => ['onLeadMerge', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile events for the lead timeline.
|
||||
*/
|
||||
public function onTimelineGenerate(LeadTimelineEvent $event): void
|
||||
{
|
||||
// Set available event types
|
||||
$eventTypeKey = 'page.hit';
|
||||
$eventTypeName = $this->translator->trans('mautic.page.event.hit');
|
||||
$event->addEventType($eventTypeKey, $eventTypeName);
|
||||
$event->addSerializerGroup(['pageList', 'hitDetails']);
|
||||
|
||||
if (!$event->isApplicable($eventTypeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hits = $this->pageModel->getHitRepository()->getLeadHits(
|
||||
$event->getLeadId(),
|
||||
$event->getQueryOptions()
|
||||
);
|
||||
|
||||
// Add to counter
|
||||
$event->addToCounter($eventTypeKey, $hits);
|
||||
|
||||
if (!$event->isEngagementCount()) {
|
||||
// Add the hits to the event array
|
||||
foreach ($hits['results'] as $hit) {
|
||||
$template = '@MauticPage/SubscribedEvents/Timeline/index.html.twig';
|
||||
$icon = 'ri-link';
|
||||
|
||||
if (!empty($hit['source'])) {
|
||||
if ($channelModel = $this->getChannelModel($hit['source'])) {
|
||||
if ($channelModel instanceof ChannelTimelineInterface) {
|
||||
if ($overrideTemplate = $channelModel->getChannelTimelineTemplate($eventTypeKey, $hit)) {
|
||||
$template = $overrideTemplate;
|
||||
}
|
||||
|
||||
if ($overrideEventTypeName = $channelModel->getChannelTimelineLabel($eventTypeKey, $hit)) {
|
||||
$eventTypeName = $overrideEventTypeName;
|
||||
}
|
||||
|
||||
if ($overrideIcon = $channelModel->getChannelTimelineIcon($eventTypeKey, $hit)) {
|
||||
$icon = $overrideIcon;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($hit['sourceId'])) {
|
||||
if ($source = $this->getChannelEntityName($hit['source'], $hit['sourceId'], true)) {
|
||||
$hit['sourceName'] = $source['name'];
|
||||
$hit['sourceRoute'] = $source['url'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($hit['page_id'])) {
|
||||
$page = $this->pageModel->getEntity($hit['page_id']);
|
||||
$eventLabel = [
|
||||
'label' => $page->getTitle(),
|
||||
'href' => $this->router->generate('mautic_page_action', ['objectAction' => 'view', 'objectId' => $hit['page_id']]),
|
||||
];
|
||||
} else {
|
||||
$eventLabel = [
|
||||
'label' => $hit['urlTitle'] ?? $hit['url'],
|
||||
'href' => $hit['url'],
|
||||
'isExternal' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$contactId = $hit['lead_id'];
|
||||
unset($hit['lead_id']);
|
||||
|
||||
$event->addEvent(
|
||||
[
|
||||
'event' => $eventTypeKey,
|
||||
'eventId' => $hit['hitId'],
|
||||
'eventLabel' => $eventLabel,
|
||||
'eventType' => $eventTypeName,
|
||||
'timestamp' => $hit['dateHit'],
|
||||
'extra' => [
|
||||
'hit' => $hit,
|
||||
],
|
||||
'contentTemplate' => $template,
|
||||
'icon' => $icon,
|
||||
'contactId' => $contactId,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile events for the lead timeline.
|
||||
*/
|
||||
public function onTimelineGenerateVideo(LeadTimelineEvent $event): void
|
||||
{
|
||||
// Set available event types
|
||||
$eventTypeKey = 'page.videohit';
|
||||
$eventTypeName = $this->translator->trans('mautic.page.event.videohit');
|
||||
$event->addEventType($eventTypeKey, $eventTypeName);
|
||||
$event->addSerializerGroup(['pageList', 'hitDetails']);
|
||||
|
||||
if (!$event->isApplicable($eventTypeKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hits = $this->pageVideoModel->getHitRepository()->getTimelineStats(
|
||||
$event->getLeadId(),
|
||||
$event->getQueryOptions()
|
||||
);
|
||||
|
||||
$event->addToCounter($eventTypeKey, $hits);
|
||||
|
||||
if (!$event->isEngagementCount()) {
|
||||
// Add the hits to the event array
|
||||
foreach ($hits['results'] as $hit) {
|
||||
$template = '@MauticPage/SubscribedEvents/Timeline/videohit.html.twig';
|
||||
$eventLabel = $eventTypeName;
|
||||
|
||||
$event->addEvent(
|
||||
[
|
||||
'event' => $eventTypeKey,
|
||||
'eventLabel' => $eventLabel,
|
||||
'eventType' => $eventTypeName,
|
||||
'timestamp' => $hit['date_hit'],
|
||||
'extra' => [
|
||||
'hit' => $hit,
|
||||
],
|
||||
'contentTemplate' => $template,
|
||||
'icon' => 'ri-vidicon-2-line',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onLeadChange(LeadChangeEvent $event): void
|
||||
{
|
||||
$this->pageModel->getHitRepository()->updateLeadByTrackingId(
|
||||
$event->getNewLead()->getId(),
|
||||
$event->getNewTrackingId(),
|
||||
$event->getOldTrackingId()
|
||||
);
|
||||
}
|
||||
|
||||
public function onLeadMerge(LeadMergeEvent $event): void
|
||||
{
|
||||
$this->pageModel->getHitRepository()->updateLead(
|
||||
$event->getLoser()->getId(),
|
||||
$event->getVictor()->getId()
|
||||
);
|
||||
|
||||
$this->pageVideoModel->getHitRepository()->updateLead(
|
||||
$event->getLoser()->getId(),
|
||||
$event->getVictor()->getId()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\Event\MaintenanceEvent;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class MaintenanceSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $db,
|
||||
private TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::MAINTENANCE_CLEANUP_DATA => ['onDataCleanup', 10], // Cleanup before visitors are processed
|
||||
];
|
||||
}
|
||||
|
||||
public function onDataCleanup(MaintenanceEvent $event): void
|
||||
{
|
||||
$this->cleanData($event, 'page_hits');
|
||||
$this->cleanData($event, 'lead_utmtags');
|
||||
}
|
||||
|
||||
private function cleanData(MaintenanceEvent $event, $table): void
|
||||
{
|
||||
$qb = $this->db->createQueryBuilder()
|
||||
->setParameter('date', $event->getDate()->format('Y-m-d H:i:s'));
|
||||
|
||||
if ($event->isDryRun()) {
|
||||
$qb->select('count(*) as records')
|
||||
->from(MAUTIC_TABLE_PREFIX.$table, 'h')
|
||||
->join('h', MAUTIC_TABLE_PREFIX.'leads', 'l', 'h.lead_id = l.id')
|
||||
->where($qb->expr()->lte('l.last_active', ':date'));
|
||||
|
||||
if (false === $event->isGdpr()) {
|
||||
$qb->andWhere($qb->expr()->isNull('l.date_identified'));
|
||||
} else {
|
||||
$qb->orWhere(
|
||||
$qb->expr()->and(
|
||||
$qb->expr()->lte('l.date_added', ':date2'),
|
||||
$qb->expr()->isNull('l.last_active')
|
||||
));
|
||||
$qb->setParameter('date2', $event->getDate()->format('Y-m-d H:i:s'));
|
||||
}
|
||||
|
||||
$rows = $qb->executeQuery()->fetchOne();
|
||||
} else {
|
||||
$subQb = $this->db->createQueryBuilder();
|
||||
$subQb->select('id')->from(MAUTIC_TABLE_PREFIX.'leads', 'l')
|
||||
->where($qb->expr()->lte('l.last_active', ':date'));
|
||||
|
||||
if (false === $event->isGdpr()) {
|
||||
$subQb->andWhere($qb->expr()->isNull('l.date_identified'));
|
||||
} else {
|
||||
$subQb->orWhere(
|
||||
$subQb->expr()->and(
|
||||
$subQb->expr()->lte('l.date_added', ':date2'),
|
||||
$subQb->expr()->isNull('l.last_active')
|
||||
));
|
||||
$subQb->setParameter('date2', $event->getDate()->format('Y-m-d H:i:s'));
|
||||
}
|
||||
$rows = 0;
|
||||
$loop = 0;
|
||||
$subQb->setParameter('date', $event->getDate()->format('Y-m-d H:i:s'));
|
||||
while (true) {
|
||||
$subQb->setMaxResults(10000)->setFirstResult($loop * 10000);
|
||||
|
||||
$leadsIds = array_column($subQb->executeQuery()->fetchAllAssociative(), 'id');
|
||||
|
||||
if (0 === sizeof($leadsIds)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$rows += $qb->delete(MAUTIC_TABLE_PREFIX.$table)
|
||||
->where(
|
||||
$qb->expr()->in(
|
||||
'lead_id', $leadsIds
|
||||
)
|
||||
)
|
||||
->executeStatement();
|
||||
++$loop;
|
||||
}
|
||||
}
|
||||
$event->setStat($this->translator->trans('mautic.maintenance.'.$table), $rows, $qb->getSQL(), $qb->getParameters());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CoreBundle\Event\EntityExportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportAnalyzeEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportEvent;
|
||||
use Mautic\CoreBundle\Event\EntityImportUndoEvent;
|
||||
use Mautic\CoreBundle\EventListener\ImportExportTrait;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
|
||||
final class PageImportExportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
use ImportExportTrait;
|
||||
|
||||
public function __construct(
|
||||
private PageModel $pageModel,
|
||||
private EntityManagerInterface $entityManager,
|
||||
private AuditLogModel $auditLogModel,
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private DenormalizerInterface $serializer,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
EntityExportEvent::class => ['onPageExport', 0],
|
||||
EntityImportEvent::class => ['onPageImport', 0],
|
||||
EntityImportUndoEvent::class => ['onUndoImport', 0],
|
||||
EntityImportAnalyzeEvent::class => ['onDuplicationCheck', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onPageExport(EntityExportEvent $event): void
|
||||
{
|
||||
if (Page::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pageId = $event->getEntityId();
|
||||
$page = $this->pageModel->getEntity($pageId);
|
||||
if (!$page) {
|
||||
return;
|
||||
}
|
||||
|
||||
$pageData = [
|
||||
'id' => $page->getId(),
|
||||
'is_published' => $page->isPublished(),
|
||||
'title' => $page->getTitle(),
|
||||
'alias' => $page->getAlias(),
|
||||
'template' => $page->getTemplate(),
|
||||
'custom_html' => $page->getCustomHtml(),
|
||||
'content' => $page->getContent(),
|
||||
'publish_up' => $page->getPublishUp() ? $page->getPublishUp()->format(DATE_ATOM) : null,
|
||||
'publish_down' => $page->getPublishDown() ? $page->getPublishDown()->format(DATE_ATOM) : null,
|
||||
'hits' => $page->getHits(),
|
||||
'unique_hits' => $page->getUniqueHits(),
|
||||
'variant_hits' => $page->getVariantHits(),
|
||||
'revision' => $page->getRevision(),
|
||||
'meta_description' => $page->getMetaDescription(),
|
||||
'head_script' => $page->getHeadScript(),
|
||||
'footer_script' => $page->getFooterScript(),
|
||||
'redirect_type' => $page->getRedirectType(),
|
||||
'redirect_url' => $page->getRedirectUrl(),
|
||||
'is_preference_center' => $page->getIsPreferenceCenter(),
|
||||
'no_index' => $page->getNoIndex(),
|
||||
'lang' => $page->getLanguage(),
|
||||
'variant_settings' => $page->getVariantSettings(),
|
||||
'uuid' => $page->getUuid(),
|
||||
];
|
||||
|
||||
$event->addEntity(Page::ENTITY_NAME, $pageData);
|
||||
$this->logAction('export', $page->getId(), $pageData);
|
||||
}
|
||||
|
||||
public function onPageImport(EntityImportEvent $event): void
|
||||
{
|
||||
if (Page::ENTITY_NAME !== $event->getEntityName() || !$event->getEntityData()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = [
|
||||
EntityImportEvent::NEW => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
EntityImportEvent::UPDATE => ['names' => [], 'ids' => [], 'count' => 0],
|
||||
];
|
||||
|
||||
foreach ($event->getEntityData() as $element) {
|
||||
$object = $this->entityManager->getRepository(Page::class)->findOneBy(['uuid' => $element['uuid']]);
|
||||
$isNew = !$object;
|
||||
|
||||
$object ??= new Page();
|
||||
$this->serializer->denormalize(
|
||||
$element,
|
||||
Page::class,
|
||||
null,
|
||||
['object_to_populate' => $object]
|
||||
);
|
||||
$this->pageModel->saveEntity($object);
|
||||
|
||||
$event->addEntityIdMap((int) $element['id'], $object->getId());
|
||||
|
||||
$status = $isNew ? EntityImportEvent::NEW : EntityImportEvent::UPDATE;
|
||||
$stats[$status]['names'][] = $object->getTitle();
|
||||
$stats[$status]['ids'][] = $object->getId();
|
||||
++$stats[$status]['count'];
|
||||
|
||||
$this->logAction('import', $object->getId(), $element);
|
||||
}
|
||||
|
||||
foreach ($stats as $status => $info) {
|
||||
if ($info['count'] > 0) {
|
||||
$event->setStatus($status, [Page::ENTITY_NAME => $info]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function onUndoImport(EntityImportUndoEvent $event): void
|
||||
{
|
||||
if (Page::ENTITY_NAME !== $event->getEntityName()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$summary = $event->getSummary();
|
||||
|
||||
if (!isset($summary['ids']) || empty($summary['ids'])) {
|
||||
return;
|
||||
}
|
||||
foreach ($summary['ids'] as $id) {
|
||||
$entity = $this->entityManager->getRepository(Page::class)->find($id);
|
||||
|
||||
if ($entity) {
|
||||
$this->entityManager->remove($entity);
|
||||
$this->logAction('undo_import', $id, ['deletedEntity' => Page::class]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function onDuplicationCheck(EntityImportAnalyzeEvent $event): void
|
||||
{
|
||||
$this->performDuplicationCheck(
|
||||
$event,
|
||||
Page::ENTITY_NAME,
|
||||
Page::class,
|
||||
'title',
|
||||
$this->entityManager
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $details
|
||||
*/
|
||||
private function logAction(string $action, int $objectId, array $details): void
|
||||
{
|
||||
$this->auditLogModel->writeToLog([
|
||||
'bundle' => 'page',
|
||||
'object' => 'page',
|
||||
'objectId' => $objectId,
|
||||
'action' => $action,
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\LanguageHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\CoreBundle\Twig\Helper\AssetsHelper;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Event as Events;
|
||||
use Mautic\PageBundle\Event\PageEditSubmitEvent;
|
||||
use Mautic\PageBundle\Event\PageEvent;
|
||||
use Mautic\PageBundle\Model\PageDraftModel;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class PageSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private AssetsHelper $assetsHelper,
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private AuditLogModel $auditLogModel,
|
||||
private LanguageHelper $languageHelper,
|
||||
private PageModel $pageModel,
|
||||
private PageDraftModel $pageDraftModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PageEvents::PAGE_POST_SAVE => ['onPagePostSave', 0],
|
||||
PageEvents::PAGE_POST_DELETE => ['onPageDelete', 0],
|
||||
PageEvents::PAGE_ON_DISPLAY => ['onPageDisplay', -255], // We want this to run last
|
||||
PageEditSubmitEvent::class => ['managePageDraft'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an entry to the audit log.
|
||||
*/
|
||||
public function onPagePostSave(PageEvent $event): void
|
||||
{
|
||||
$page = $event->getPage();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'page',
|
||||
'object' => 'page',
|
||||
'objectId' => $page->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
if (!array_key_exists($page->getLanguage(), $this->languageHelper->getSupportedLanguages())) {
|
||||
$this->languageHelper->extractLanguagePackage($page->getLanguage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a delete entry to the audit log.
|
||||
*/
|
||||
public function onPageDelete(PageEvent $event): void
|
||||
{
|
||||
$page = $event->getPage();
|
||||
$log = [
|
||||
'bundle' => 'page',
|
||||
'object' => 'page',
|
||||
'objectId' => $page->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $page->getTitle()],
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow event listeners to add scripts to
|
||||
* - </head> : onPageDisplay_headClose
|
||||
* - <body> : onPageDisplay_bodyOpen
|
||||
* - </body> : onPageDisplay_bodyClose.
|
||||
*/
|
||||
public function onPageDisplay(Events\PageDisplayEvent $event): void
|
||||
{
|
||||
$content = $event->getContent();
|
||||
|
||||
// Get scripts to insert before </head>
|
||||
ob_start();
|
||||
$this->assetsHelper->outputScripts('onPageDisplay_headClose');
|
||||
$headCloseScripts = ob_get_clean();
|
||||
|
||||
if ($headCloseScripts) {
|
||||
$content = str_ireplace('</head>', $headCloseScripts."\n</head>", $content);
|
||||
}
|
||||
|
||||
// Get scripts to insert after <body>
|
||||
ob_start();
|
||||
$this->assetsHelper->outputScripts('onPageDisplay_bodyOpen');
|
||||
$bodyOpenScripts = ob_get_clean();
|
||||
|
||||
if ($bodyOpenScripts) {
|
||||
preg_match('/(<body[^>]*>)/i', $content, $matches);
|
||||
|
||||
$content = str_ireplace($matches[0], $matches[0]."\n".$bodyOpenScripts, $content);
|
||||
}
|
||||
|
||||
// Get scripts to insert before </body>
|
||||
ob_start();
|
||||
$this->assetsHelper->outputScripts('onPageDisplay_bodyClose');
|
||||
$bodyCloseScripts = ob_get_clean();
|
||||
|
||||
if ($bodyCloseScripts) {
|
||||
$content = str_ireplace('</body>', $bodyCloseScripts."\n</body>", $content);
|
||||
}
|
||||
|
||||
// Get scripts to insert before a custom tag
|
||||
$params = $event->getParams();
|
||||
if (count($params) > 0) {
|
||||
if (isset($params['custom_tag']) && $customTag = $params['custom_tag']) {
|
||||
ob_start();
|
||||
$this->assetsHelper->outputScripts('customTag');
|
||||
$bodyCustomTag = ob_get_clean();
|
||||
|
||||
if ($bodyCustomTag) {
|
||||
$content = str_ireplace($customTag, $bodyCustomTag."\n".$customTag, $content);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$event->setContent($content);
|
||||
}
|
||||
|
||||
public function managePageDraft(PageEditSubmitEvent $event): void
|
||||
{
|
||||
$livePage = $event->getPreviousPage();
|
||||
$editedPage = $event->getCurrentPage();
|
||||
|
||||
if (
|
||||
($event->isSaveAndClose() || $event->isApply())
|
||||
&& $editedPage->hasDraft()
|
||||
) {
|
||||
$pageDraft = $editedPage->getDraft();
|
||||
$pageDraft->setHtml($editedPage->getCustomHtml());
|
||||
$pageDraft->setTemplate($editedPage->getTemplate());
|
||||
$editedPage->setCustomHtml($livePage->getCustomHtml());
|
||||
$editedPage->setTemplate($livePage->getTemplate());
|
||||
$this->pageDraftModel->saveDraft($pageDraft);
|
||||
$this->pageModel->saveEntity($editedPage);
|
||||
}
|
||||
|
||||
if ($event->isSaveAsDraft()) {
|
||||
$pageDraft = $this
|
||||
->pageDraftModel
|
||||
->createDraft($editedPage, $editedPage->getCustomHtml(), $editedPage->getTemplate());
|
||||
|
||||
$editedPage->setCustomHtml($livePage->getCustomHtml());
|
||||
$editedPage->setTemplate($livePage->getTemplate());
|
||||
$editedPage->setDraft($pageDraft);
|
||||
$this->pageModel->saveEntity($editedPage);
|
||||
}
|
||||
|
||||
if ($event->isDiscardDraft()) {
|
||||
$this->revertPageModifications($livePage, $editedPage);
|
||||
$this->pageDraftModel->deleteDraft($editedPage);
|
||||
$editedPage->setDraft(null);
|
||||
$this->pageModel->saveEntity($editedPage);
|
||||
}
|
||||
|
||||
if ($event->isApplyDraft()) {
|
||||
$this->pageDraftModel->deleteDraft($editedPage);
|
||||
$editedPage->setDraft(null);
|
||||
}
|
||||
}
|
||||
|
||||
public function deletePageDraft(PageEvent $event): void
|
||||
{
|
||||
try {
|
||||
$this->pageDraftModel->deleteDraft($event->getPage());
|
||||
} catch (NotFoundHttpException) {
|
||||
// No associated draft found for deletion. We have nothing to do here. Return.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private function revertPageModifications(Page $livePage, Page $editedPage): void
|
||||
{
|
||||
$livePageReflection = new \ReflectionObject($livePage);
|
||||
$editedPageReflection = new \ReflectionObject($editedPage);
|
||||
foreach ($livePageReflection->getProperties() as $property) {
|
||||
if ('id' == $property->getName()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$property->setAccessible(true);
|
||||
$name = $property->getName();
|
||||
$value = $property->getValue($livePage);
|
||||
$editedPageProperty = $editedPageReflection->getProperty($name);
|
||||
$editedPageProperty->setAccessible(true);
|
||||
$editedPageProperty->setValue($editedPage, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\PageBundle\Event as Events;
|
||||
use Mautic\PageBundle\Form\Type\PointActionPageHitType;
|
||||
use Mautic\PageBundle\Form\Type\PointActionUrlHitType;
|
||||
use Mautic\PageBundle\Helper\PointActionHelper;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Mautic\PointBundle\Event\PointBuilderEvent;
|
||||
use Mautic\PointBundle\Model\PointModel;
|
||||
use Mautic\PointBundle\PointEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class PointSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PointModel $pointModel,
|
||||
private PointActionHelper $pointActionHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PointEvents::POINT_ON_BUILD => ['onPointBuild', 0],
|
||||
PageEvents::PAGE_ON_HIT => ['onPageHit', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onPointBuild(PointBuilderEvent $event): void
|
||||
{
|
||||
$action = [
|
||||
'group' => 'mautic.page.point.action',
|
||||
'label' => 'mautic.page.point.action.pagehit',
|
||||
'description' => 'mautic.page.point.action.pagehit_descr',
|
||||
'callback' => [PointActionHelper::class, 'validatePageHit'],
|
||||
'formType' => PointActionPageHitType::class,
|
||||
];
|
||||
|
||||
$event->addAction('page.hit', $action);
|
||||
|
||||
$action = [
|
||||
'group' => 'mautic.page.point.action',
|
||||
'label' => 'mautic.page.point.action.urlhit',
|
||||
'description' => 'mautic.page.point.action.urlhit_descr',
|
||||
'callback' => [$this->pointActionHelper, 'validateUrlHit'],
|
||||
'formType' => PointActionUrlHitType::class,
|
||||
'formTheme' => '@MauticPage/FormTheme/Point/pointaction_urlhit_widget.html.twig',
|
||||
];
|
||||
|
||||
$event->addAction('url.hit', $action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger point actions for page hits.
|
||||
*/
|
||||
public function onPageHit(Events\PageHitEvent $event): void
|
||||
{
|
||||
if ($event->getPage()) {
|
||||
// Mautic Landing Page was hit
|
||||
$this->pointModel->triggerAction('page.hit', $event->getHit(), null, $event->getLead());
|
||||
} else {
|
||||
// Mautic Tracking Pixel was hit
|
||||
$this->pointModel->triggerAction('url.hit', $event->getHit(), null, $event->getLead());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,587 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\Chart\ChartQuery;
|
||||
use Mautic\CoreBundle\Helper\Chart\LineChart;
|
||||
use Mautic\CoreBundle\Helper\Chart\PieChart;
|
||||
use Mautic\LeadBundle\Model\CompanyReportData;
|
||||
use Mautic\LeadBundle\Report\DncReportService;
|
||||
use Mautic\PageBundle\Entity\HitRepository;
|
||||
use Mautic\ReportBundle\Event\ReportBuilderEvent;
|
||||
use Mautic\ReportBundle\Event\ReportDataEvent;
|
||||
use Mautic\ReportBundle\Event\ReportGeneratorEvent;
|
||||
use Mautic\ReportBundle\Event\ReportGraphEvent;
|
||||
use Mautic\ReportBundle\ReportEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ReportSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public const CONTEXT_PAGES = 'pages';
|
||||
|
||||
public const CONTEXT_PAGE_HITS = 'page.hits';
|
||||
|
||||
public const CONTEXT_VIDEO_HITS = 'video.hits';
|
||||
|
||||
public function __construct(
|
||||
private CompanyReportData $companyReportData,
|
||||
private HitRepository $hitRepository,
|
||||
private TranslatorInterface $translator,
|
||||
private DncReportService $dncReportService,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ReportEvents::REPORT_ON_BUILD => ['onReportBuilder', 0],
|
||||
ReportEvents::REPORT_ON_GENERATE => ['onReportGenerate', 0],
|
||||
ReportEvents::REPORT_ON_GRAPH_GENERATE => ['onReportGraphGenerate', 0],
|
||||
ReportEvents::REPORT_ON_DISPLAY => ['onReportDisplay', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add available tables and columns to the report builder lookup.
|
||||
*/
|
||||
public function onReportBuilder(ReportBuilderEvent $event): void
|
||||
{
|
||||
if (!$event->checkContext([self::CONTEXT_PAGES, self::CONTEXT_PAGE_HITS, self::CONTEXT_VIDEO_HITS])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$prefix = 'p.';
|
||||
$translationPrefix = 'tp.';
|
||||
$variantPrefix = 'vp.';
|
||||
|
||||
$columns = [
|
||||
$prefix.'title' => [
|
||||
'label' => 'mautic.core.title',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'alias' => [
|
||||
'label' => 'mautic.core.alias',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'revision' => [
|
||||
'label' => 'mautic.page.report.revision',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'hits' => [
|
||||
'label' => 'mautic.page.field.hits',
|
||||
'type' => 'int',
|
||||
],
|
||||
$prefix.'unique_hits' => [
|
||||
'label' => 'mautic.page.field.unique_hits',
|
||||
'type' => 'int',
|
||||
],
|
||||
$translationPrefix.'id' => [
|
||||
'label' => 'mautic.page.report.translation_parent_id',
|
||||
'type' => 'int',
|
||||
],
|
||||
$translationPrefix.'title' => [
|
||||
'label' => 'mautic.page.report.translation_parent_title',
|
||||
'type' => 'string',
|
||||
],
|
||||
$variantPrefix.'id' => [
|
||||
'label' => 'mautic.page.report.variant_parent_id',
|
||||
'type' => 'string',
|
||||
],
|
||||
$variantPrefix.'title' => [
|
||||
'label' => 'mautic.page.report.variant_parent_title',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'lang' => [
|
||||
'label' => 'mautic.core.language',
|
||||
'type' => 'string',
|
||||
],
|
||||
$prefix.'variant_start_date' => [
|
||||
'label' => 'mautic.page.report.variant_start_date',
|
||||
'type' => 'datetime',
|
||||
'groupByFormula' => 'DATE('.$prefix.'variant_start_date)',
|
||||
],
|
||||
$prefix.'variant_hits' => [
|
||||
'label' => 'mautic.page.report.variant_hits',
|
||||
'type' => 'int',
|
||||
],
|
||||
];
|
||||
$columns = array_merge(
|
||||
$columns,
|
||||
$event->getStandardColumns('p.', ['name', 'description'], 'mautic_page_action'),
|
||||
$event->getCategoryColumns()
|
||||
);
|
||||
$data = [
|
||||
'display_name' => 'mautic.page.pages',
|
||||
'columns' => $columns,
|
||||
];
|
||||
$event->addTable(self::CONTEXT_PAGES, $data);
|
||||
|
||||
if ($event->checkContext(self::CONTEXT_PAGE_HITS)) {
|
||||
$hitPrefix = 'ph.';
|
||||
$redirectHit = 'r.';
|
||||
$hitColumns = [
|
||||
$hitPrefix.'id' => [
|
||||
'label' => 'mautic.page.report.hits.id',
|
||||
'type' => 'int',
|
||||
],
|
||||
$hitPrefix.'date_hit' => [
|
||||
'label' => 'mautic.page.report.hits.date_hit',
|
||||
'type' => 'datetime',
|
||||
'groupByFormula' => 'DATE('.$hitPrefix.'date_hit)',
|
||||
],
|
||||
$hitPrefix.'date_left' => [
|
||||
'label' => 'mautic.page.report.hits.date_left',
|
||||
'type' => 'datetime',
|
||||
'groupByFormula' => 'DATE('.$hitPrefix.'date_left)',
|
||||
],
|
||||
$hitPrefix.'time_spent' => [
|
||||
'label' => 'mautic.page.report.hits.time_spent',
|
||||
'type' => 'string',
|
||||
'formula' => 'IF('.$hitPrefix.'date_left IS NOT NULL, SEC_TO_TIME(TIMESTAMPDIFF(SECOND, '.$hitPrefix.'date_hit, '.$hitPrefix.'date_left)), \'\')',
|
||||
],
|
||||
$hitPrefix.'country' => [
|
||||
'label' => 'mautic.page.report.hits.country',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'region' => [
|
||||
'label' => 'mautic.page.report.hits.region',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'city' => [
|
||||
'label' => 'mautic.page.report.hits.city',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'isp' => [
|
||||
'label' => 'mautic.page.report.hits.isp',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'organization' => [
|
||||
'label' => 'mautic.page.report.hits.organization',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'code' => [
|
||||
'label' => 'mautic.page.report.hits.code',
|
||||
'type' => 'int',
|
||||
],
|
||||
$hitPrefix.'referer' => [
|
||||
'label' => 'mautic.page.report.hits.referer',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'url' => [
|
||||
'label' => 'mautic.page.report.hits.url',
|
||||
'type' => 'url',
|
||||
],
|
||||
$hitPrefix.'url_title' => [
|
||||
'label' => 'mautic.page.report.hits.url_title',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'user_agent' => [
|
||||
'label' => 'mautic.page.report.hits.user_agent',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'remote_host' => [
|
||||
'label' => 'mautic.page.report.hits.remote_host',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'browser_languages' => [
|
||||
'label' => 'mautic.page.report.hits.browser_languages',
|
||||
'type' => 'array',
|
||||
],
|
||||
$hitPrefix.'source' => [
|
||||
'label' => 'mautic.report.field.source',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'source_id' => [
|
||||
'label' => 'mautic.report.field.source_id',
|
||||
'type' => 'int',
|
||||
],
|
||||
$redirectHit.'url' => [
|
||||
'label' => 'mautic.page.report.hits.redirect_url',
|
||||
'type' => 'url',
|
||||
],
|
||||
$redirectHit.'hits' => [
|
||||
'label' => 'mautic.page.report.hits.redirect_hit_count',
|
||||
'type' => 'int',
|
||||
],
|
||||
$redirectHit.'unique_hits' => [
|
||||
'label' => 'mautic.page.report.hits.redirect_unique_hits',
|
||||
'type' => 'string',
|
||||
],
|
||||
'ds.device' => [
|
||||
'label' => 'mautic.lead.device',
|
||||
'type' => 'string',
|
||||
],
|
||||
'ds.device_brand' => [
|
||||
'label' => 'mautic.lead.device_brand',
|
||||
'type' => 'string',
|
||||
],
|
||||
'ds.device_model' => [
|
||||
'label' => 'mautic.lead.device_model',
|
||||
'type' => 'string',
|
||||
],
|
||||
'ds.device_os_name' => [
|
||||
'label' => 'mautic.lead.device_os_name',
|
||||
'type' => 'string',
|
||||
],
|
||||
'ds.device_os_shortname' => [
|
||||
'label' => 'mautic.lead.device_os_shortname',
|
||||
'type' => 'string',
|
||||
],
|
||||
'ds.device_os_version' => [
|
||||
'label' => 'mautic.lead.device_os_version',
|
||||
'type' => 'string',
|
||||
],
|
||||
'ds.device_os_platform' => [
|
||||
'label' => 'mautic.lead.device_os_platform',
|
||||
'type' => 'string',
|
||||
],
|
||||
];
|
||||
|
||||
$companyColumns = $this->companyReportData->getCompanyData();
|
||||
|
||||
$commonColumnsAndFilters = array_merge(
|
||||
$columns,
|
||||
$hitColumns,
|
||||
$event->getCampaignByChannelColumns(),
|
||||
$event->getLeadColumns(),
|
||||
$event->getIpColumn(),
|
||||
$companyColumns
|
||||
);
|
||||
|
||||
$pageHitsColumns = array_merge($commonColumnsAndFilters, $this->dncReportService->getDncColumns());
|
||||
$pageHitsFilters = array_merge($commonColumnsAndFilters, $this->dncReportService->getDncFilters());
|
||||
|
||||
$data = [
|
||||
'display_name' => 'mautic.page.hits',
|
||||
'columns' => $pageHitsColumns,
|
||||
'filters' => $pageHitsFilters,
|
||||
];
|
||||
$event->addTable(self::CONTEXT_PAGE_HITS, $data, self::CONTEXT_PAGES);
|
||||
|
||||
// Register graphs
|
||||
$context = self::CONTEXT_PAGE_HITS;
|
||||
$event->addGraph($context, 'line', 'mautic.page.graph.line.hits');
|
||||
$event->addGraph($context, 'line', 'mautic.page.graph.line.time.on.site');
|
||||
$event->addGraph($context, 'pie', 'mautic.page.graph.pie.time.on.site', ['translate' => false]);
|
||||
$event->addGraph($context, 'pie', 'mautic.page.graph.pie.new.vs.returning');
|
||||
$event->addGraph($context, 'pie', 'mautic.page.graph.pie.devices');
|
||||
$event->addGraph($context, 'pie', 'mautic.page.graph.pie.languages', ['translate' => false]);
|
||||
$event->addGraph($context, 'table', 'mautic.page.table.referrers');
|
||||
$event->addGraph($context, 'table', 'mautic.page.table.most.visited');
|
||||
$event->addGraph($context, 'table', 'mautic.page.table.most.visited.unique');
|
||||
}
|
||||
if ($event->checkContext(self::CONTEXT_VIDEO_HITS)) {
|
||||
$hitPrefix = 'vh.';
|
||||
$hitColumns = [
|
||||
$hitPrefix.'id' => [
|
||||
'label' => 'mautic.core.id',
|
||||
'type' => 'int',
|
||||
],
|
||||
$hitPrefix.'date_hit' => [
|
||||
'label' => 'mautic.page.report.hits.date_hit',
|
||||
'type' => 'datetime',
|
||||
'groupByFormula' => 'DATE('.$hitPrefix.'date_hit)',
|
||||
],
|
||||
$hitPrefix.'country' => [
|
||||
'label' => 'mautic.page.report.hits.country',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'region' => [
|
||||
'label' => 'mautic.page.report.hits.region',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'city' => [
|
||||
'label' => 'mautic.page.report.hits.city',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'isp' => [
|
||||
'label' => 'mautic.page.report.hits.isp',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'organization' => [
|
||||
'label' => 'mautic.page.report.hits.organization',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'code' => [
|
||||
'label' => 'mautic.page.report.hits.code',
|
||||
'type' => 'int',
|
||||
],
|
||||
$hitPrefix.'referer' => [
|
||||
'label' => 'mautic.page.report.hits.referer',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'url' => [
|
||||
'label' => 'mautic.page.report.hits.url',
|
||||
'type' => 'url',
|
||||
],
|
||||
$hitPrefix.'user_agent' => [
|
||||
'label' => 'mautic.page.report.hits.user_agent',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'remote_host' => [
|
||||
'label' => 'mautic.page.report.hits.remote_host',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'browser_languages' => [
|
||||
'label' => 'mautic.page.report.hits.browser_languages',
|
||||
'type' => 'array',
|
||||
],
|
||||
$hitPrefix.'channel' => [
|
||||
'label' => 'mautic.report.field.source',
|
||||
'type' => 'string',
|
||||
],
|
||||
$hitPrefix.'channel_id' => [
|
||||
'label' => 'mautic.report.field.source_id',
|
||||
'type' => 'int',
|
||||
],
|
||||
'time_watched' => [
|
||||
'label' => 'mautic.page.report.hits.time_watched',
|
||||
'type' => 'string',
|
||||
'formula' => 'if('.$hitPrefix.'duration = 0,\'-\',SEC_TO_TIME('.$hitPrefix.'time_watched))',
|
||||
],
|
||||
'duration' => [
|
||||
'label' => 'mautic.page.report.hits.duration',
|
||||
'type' => 'string',
|
||||
'formula' => 'if('.$hitPrefix.'duration = 0,\'-\',SEC_TO_TIME('.$hitPrefix.'duration))',
|
||||
],
|
||||
];
|
||||
|
||||
$data = [
|
||||
'display_name' => 'mautic.'.self::CONTEXT_VIDEO_HITS,
|
||||
'columns' => array_merge($hitColumns, $event->getLeadColumns(), $event->getIpColumn()),
|
||||
];
|
||||
$event->addTable(self::CONTEXT_VIDEO_HITS, $data, 'videos');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the QueryBuilder object to generate reports from.
|
||||
*/
|
||||
public function onReportGenerate(ReportGeneratorEvent $event): void
|
||||
{
|
||||
$context = $event->getContext();
|
||||
$qb = $event->getQueryBuilder();
|
||||
$hasGroupBy = $event->hasGroupBy();
|
||||
|
||||
switch ($context) {
|
||||
case self::CONTEXT_PAGES:
|
||||
$qb->from(MAUTIC_TABLE_PREFIX.'pages', 'p')
|
||||
->leftJoin('p', MAUTIC_TABLE_PREFIX.'pages', 'tp', 'p.id = tp.id')
|
||||
->leftJoin('p', MAUTIC_TABLE_PREFIX.'pages', 'vp', 'p.id = vp.id');
|
||||
$event->addCategoryLeftJoin($qb, 'p');
|
||||
break;
|
||||
case self::CONTEXT_PAGE_HITS:
|
||||
$event->applyDateFiltersWithoutNullValues($qb, 'date_hit', 'ph');
|
||||
$qb->from(MAUTIC_TABLE_PREFIX.'page_hits', 'ph')
|
||||
->leftJoin('ph', MAUTIC_TABLE_PREFIX.'pages', 'p', 'ph.page_id = p.id')
|
||||
->leftJoin('p', MAUTIC_TABLE_PREFIX.'pages', 'tp', 'p.id = tp.id')
|
||||
->leftJoin('p', MAUTIC_TABLE_PREFIX.'pages', 'vp', 'p.id = vp.id')
|
||||
->leftJoin('ph', MAUTIC_TABLE_PREFIX.'page_redirects', 'r', 'r.id = ph.redirect_id')
|
||||
->leftJoin('ph', MAUTIC_TABLE_PREFIX.'lead_devices', 'ds', 'ds.id = ph.device_id');
|
||||
|
||||
$event->addIpAddressLeftJoin($qb, 'ph');
|
||||
$event->addCategoryLeftJoin($qb, 'p');
|
||||
$event->addLeadLeftJoin($qb, 'ph');
|
||||
$event->addCampaignByChannelJoin($qb, 'p', 'page');
|
||||
|
||||
if ($this->companyReportData->eventHasCompanyColumns($event)) {
|
||||
$event->addCompanyLeftJoin($qb);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'video.hits':
|
||||
if (!$hasGroupBy) {
|
||||
$qb->groupBy('vh.id');
|
||||
}
|
||||
$event->applyDateFilters($qb, 'date_hit', 'vh');
|
||||
|
||||
$qb->from(MAUTIC_TABLE_PREFIX.'video_hits', 'vh');
|
||||
|
||||
$event->addIpAddressLeftJoin($qb, 'vh');
|
||||
$event->addLeadLeftJoin($qb, 'vh');
|
||||
break;
|
||||
}
|
||||
$event->setQueryBuilder($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the QueryBuilder object to generate reports from.
|
||||
*/
|
||||
public function onReportGraphGenerate(ReportGraphEvent $event): void
|
||||
{
|
||||
// Context check, we only want to fire for Lead reports
|
||||
if (!$event->checkContext(self::CONTEXT_PAGE_HITS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$graphs = $event->getRequestedGraphs();
|
||||
$qb = $event->getQueryBuilder();
|
||||
|
||||
foreach ($graphs as $g) {
|
||||
$options = $event->getOptions($g);
|
||||
$queryBuilder = clone $qb;
|
||||
|
||||
/** @var ChartQuery $chartQuery */
|
||||
$chartQuery = clone $options['chartQuery'];
|
||||
$chartQuery->applyDateFilters($queryBuilder, 'date_hit', 'ph');
|
||||
|
||||
switch ($g) {
|
||||
case 'mautic.page.graph.line.hits':
|
||||
$chart = new LineChart(null, $options['dateFrom'], $options['dateTo']);
|
||||
$chartQuery->modifyTimeDataQuery($queryBuilder, 'date_hit', 'ph');
|
||||
$hits = $chartQuery->loadAndBuildTimeData($queryBuilder);
|
||||
$chart->setDataset($options['translator']->trans($g), $hits);
|
||||
$data = $chart->render();
|
||||
$data['name'] = $g;
|
||||
|
||||
$event->setGraph($g, $data);
|
||||
break;
|
||||
|
||||
case 'mautic.page.graph.line.time.on.site':
|
||||
$chart = new LineChart(null, $options['dateFrom'], $options['dateTo']);
|
||||
$queryBuilder->select('TIMESTAMPDIFF(SECOND, ph.date_hit, ph.date_left) as data, ph.date_hit as date');
|
||||
$queryBuilder->andWhere($qb->expr()->isNotNull('ph.date_left'));
|
||||
|
||||
$hits = $chartQuery->loadAndBuildTimeData($queryBuilder);
|
||||
$chart->setDataset($options['translator']->trans($g), $hits);
|
||||
$data = $chart->render();
|
||||
$data['name'] = $g;
|
||||
|
||||
$event->setGraph($g, $data);
|
||||
break;
|
||||
|
||||
case 'mautic.page.graph.pie.time.on.site':
|
||||
$timesOnSite = $this->hitRepository->getDwellTimeLabels();
|
||||
$chart = new PieChart();
|
||||
|
||||
foreach ($timesOnSite as $time) {
|
||||
$q = clone $queryBuilder;
|
||||
$chartQuery->modifyCountDateDiffQuery($q, 'date_hit', 'date_left', $time['from'], $time['till'], 'ph');
|
||||
$data = $chartQuery->fetchCountDateDiff($q);
|
||||
$chart->setDataset($time['label'], $data);
|
||||
}
|
||||
|
||||
$event->setGraph(
|
||||
$g,
|
||||
[
|
||||
'data' => $chart->render(),
|
||||
'name' => $g,
|
||||
'iconClass' => 'ri-time-line',
|
||||
]
|
||||
);
|
||||
break;
|
||||
|
||||
case 'mautic.page.graph.pie.new.vs.returning':
|
||||
$chart = new PieChart();
|
||||
$allQ = clone $queryBuilder;
|
||||
$uniqueQ = clone $queryBuilder;
|
||||
$chartQuery->modifyCountQuery($allQ, 'date_hit', [], 'ph');
|
||||
$chartQuery->modifyCountQuery($uniqueQ, 'date_hit', ['getUnique' => true, 'selectAlso' => ['ph.page_id']], 'ph');
|
||||
$all = $chartQuery->fetchCount($allQ);
|
||||
$unique = $chartQuery->fetchCount($uniqueQ);
|
||||
$returning = $all - $unique;
|
||||
$chart->setDataset($this->translator->trans('mautic.page.unique'), $unique);
|
||||
$chart->setDataset($this->translator->trans('mautic.page.graph.pie.new.vs.returning.returning'), $returning);
|
||||
|
||||
$event->setGraph(
|
||||
$g,
|
||||
[
|
||||
'data' => $chart->render(),
|
||||
'name' => $g,
|
||||
'iconClass' => 'ri-information-2-line',
|
||||
]
|
||||
);
|
||||
break;
|
||||
|
||||
case 'mautic.page.graph.pie.languages':
|
||||
$queryBuilder->select('ph.page_language, COUNT(distinct(ph.id)) as the_count')
|
||||
->groupBy('ph.page_language')
|
||||
->andWhere($qb->expr()->isNotNull('ph.page_language'));
|
||||
$data = $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
$chart = new PieChart();
|
||||
|
||||
foreach ($data as $lang) {
|
||||
$chart->setDataset($lang['page_language'], $lang['the_count']);
|
||||
}
|
||||
|
||||
$event->setGraph(
|
||||
$g,
|
||||
[
|
||||
'data' => $chart->render(),
|
||||
'name' => $g,
|
||||
'iconClass' => 'ri-earth-line',
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'mautic.page.graph.pie.devices':
|
||||
$queryBuilder->select('ds.device, COUNT(distinct(ph.id)) as the_count')
|
||||
->groupBy('ds.device');
|
||||
$data = $queryBuilder->executeQuery()->fetchAllAssociative();
|
||||
$chart = new PieChart();
|
||||
|
||||
foreach ($data as $device) {
|
||||
$label = substr(empty($device['device']) ? $this->translator->trans('mautic.core.no.info') : $device['device'], 0, 12);
|
||||
$chart->setDataset($label, $device['the_count']);
|
||||
}
|
||||
|
||||
$event->setGraph(
|
||||
$g,
|
||||
[
|
||||
'data' => $chart->render(),
|
||||
'name' => $g,
|
||||
'iconClass' => 'ri-earth-line',
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'mautic.page.table.referrers':
|
||||
$limit = 10;
|
||||
$offset = 0;
|
||||
$items = $this->hitRepository->getReferers($queryBuilder, $limit, $offset);
|
||||
$graphData = [];
|
||||
$graphData['data'] = $items;
|
||||
$graphData['name'] = $g;
|
||||
$graphData['iconClass'] = 'ri-login-box-line';
|
||||
$event->setGraph($g, $graphData);
|
||||
break;
|
||||
|
||||
case 'mautic.page.table.most.visited':
|
||||
$limit = 10;
|
||||
$offset = 0;
|
||||
$items = $this->hitRepository->getMostVisited($queryBuilder, $limit, $offset);
|
||||
$graphData = [];
|
||||
$graphData['data'] = $items;
|
||||
$graphData['name'] = $g;
|
||||
$graphData['iconClass'] = 'ri-eye-line';
|
||||
$graphData['link'] = 'mautic_page_action';
|
||||
$event->setGraph($g, $graphData);
|
||||
break;
|
||||
|
||||
case 'mautic.page.table.most.visited.unique':
|
||||
$limit = 10;
|
||||
$offset = 0;
|
||||
$items = $this->hitRepository->getMostVisited($queryBuilder, $limit, $offset, 'p.unique_hits', 'sessions');
|
||||
$graphData = [];
|
||||
$graphData['data'] = $items;
|
||||
$graphData['name'] = $g;
|
||||
$graphData['iconClass'] = 'ri-eye-line';
|
||||
$graphData['link'] = 'mautic_page_action';
|
||||
$event->setGraph($g, $graphData);
|
||||
break;
|
||||
}
|
||||
|
||||
unset($queryBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
public function onReportDisplay(ReportDataEvent $event): void
|
||||
{
|
||||
$data = $event->getData();
|
||||
|
||||
if ($event->checkContext([self::CONTEXT_PAGE_HITS])) {
|
||||
$data = $this->dncReportService->processDncStatusDisplay($data);
|
||||
}
|
||||
|
||||
$event->setData($data);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
|
||||
use Mautic\CoreBundle\Event as MauticEvents;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\GlobalSearch;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private PageModel $pageModel,
|
||||
private CorePermissions $security,
|
||||
private GlobalSearch $globalSearch,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::GLOBAL_SEARCH => ['onGlobalSearch', 0],
|
||||
CoreEvents::BUILD_COMMAND_LIST => ['onBuildCommandList', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGlobalSearch(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->pageModel,
|
||||
'@MauticPage/SubscribedEvents/Search/global.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.page.pages', $results);
|
||||
}
|
||||
}
|
||||
|
||||
public function onBuildCommandList(MauticEvents\CommandListEvent $event): void
|
||||
{
|
||||
if ($this->security->isGranted(['page:pages:viewown', 'page:pages:viewother'], 'MATCH_ONE')) {
|
||||
$event->addCommands(
|
||||
'mautic.page.pages',
|
||||
$this->pageModel->getCommandList()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\EventListener\CommonStatsSubscriber;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use Mautic\PageBundle\Entity\VideoHit;
|
||||
|
||||
class StatsSubscriber extends CommonStatsSubscriber
|
||||
{
|
||||
public function __construct(CorePermissions $security, EntityManager $entityManager)
|
||||
{
|
||||
parent::__construct($security, $entityManager);
|
||||
$this->addContactRestrictedRepositories(
|
||||
[
|
||||
Hit::class,
|
||||
VideoHit::class,
|
||||
]
|
||||
);
|
||||
|
||||
$this->repositories[] = $entityManager->getRepository(Redirect::class);
|
||||
$this->repositories[] = $entityManager->getRepository(Trackable::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\PageBundle\Event\PageDisplayEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class TokenSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
PageEvents::PAGE_ON_DISPLAY => ['decodeTokens', 254],
|
||||
];
|
||||
}
|
||||
|
||||
public function decodeTokens(PageDisplayEvent $event): void
|
||||
{
|
||||
// Find and replace encoded tokens for trackable URL conversion
|
||||
$content = $event->getContent();
|
||||
$content = preg_replace('/(%7B)(.*?)(%7D)/i', '{$2}', $content);
|
||||
$event->setContent($content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\EventListener;
|
||||
|
||||
use Mautic\PageBundle\Event\PageHitEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Mautic\WebhookBundle\Event\WebhookBuilderEvent;
|
||||
use Mautic\WebhookBundle\Model\WebhookModel;
|
||||
use Mautic\WebhookBundle\WebhookEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class WebhookSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private WebhookModel $webhookModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
WebhookEvents::WEBHOOK_ON_BUILD => ['onWebhookBuild', 0],
|
||||
PageEvents::PAGE_ON_HIT => ['onPageHit', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event triggers and actions.
|
||||
*/
|
||||
public function onWebhookBuild(WebhookBuilderEvent $event): void
|
||||
{
|
||||
// add checkbox to the webhook form for new leads
|
||||
$pageHit = [
|
||||
'label' => 'mautic.page.webhook.event.hit',
|
||||
'description' => 'mautic.page.webhook.event.hit_desc',
|
||||
];
|
||||
|
||||
// add it to the list
|
||||
$event->addEvent(PageEvents::PAGE_ON_HIT, $pageHit);
|
||||
}
|
||||
|
||||
public function onPageHit(PageHitEvent $event): void
|
||||
{
|
||||
$this->webhookModel->queueWebhooksByType(
|
||||
PageEvents::PAGE_ON_HIT,
|
||||
[
|
||||
'hit' => $event->getHit(),
|
||||
],
|
||||
[
|
||||
'hitDetails',
|
||||
'emailDetails',
|
||||
'pageList',
|
||||
'leadList',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Exception;
|
||||
|
||||
class InvalidRenderedHtmlException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class AbTestPropertiesType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$options = ['label' => false];
|
||||
if (isset($options['formTypeOptions'])) {
|
||||
$options = array_merge($options, $options['formTypeOptions']);
|
||||
}
|
||||
$builder->add('properties', $options['formType'], $options);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefined([
|
||||
'formType',
|
||||
'formTypeOptions',
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'page_abtest_settings';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class CampaignEventPageHitType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('pages', PageListType::class, [
|
||||
'label' => 'mautic.page.campaign.event.form.pages',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.campaign.event.form.pages.descr',
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add('url', TextType::class, [
|
||||
'label' => 'mautic.page.campaign.event.form.url',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.campaign.event.form.url.descr',
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add('referer', TextType::class, [
|
||||
'label' => 'mautic.page.campaign.event.form.referer',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.campaign.event.form.referer.descr',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'campaignevent_pagehit';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class ConfigTrackingPageType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'anonymize_ip',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.anonymize_ip',
|
||||
'data' => isset($options['data']['anonymize_ip']) && (bool) $options['data']['anonymize_ip'],
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.page.config.form.anonymize_ip.tooltip',
|
||||
'onchange' => 'Mautic.showAnonymizeWarningMessage(this)',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'track_contact_by_ip',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.track_contact_by_ip',
|
||||
'data' => isset($options['data']['track_contact_by_ip']) && (bool) $options['data']['track_contact_by_ip'],
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.page.config.form.track_contact_by_ip.tooltip',
|
||||
'data-enable-on' => '{"config_trackingconfig_anonymize_ip_0":"checked"}',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'do_not_track_404_anonymous',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.do_not_track_404_anonymous',
|
||||
'data' => isset($options['data']['do_not_track_404_anonymous']) && (bool) $options['data']['do_not_track_404_anonymous'],
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.page.config.form.do_not_track_404_anonymous.tooltip',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'facebook_pixel_id',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.facebook.pixel.id',
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'facebook_pixel_trackingpage_enabled',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.tracking.trackingpage.enabled',
|
||||
'data' => isset($options['data']['facebook_pixel_trackingpage_enabled']) && (bool) $options['data']['facebook_pixel_trackingpage_enabled'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'facebook_pixel_landingpage_enabled',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.tracking.landingpage.enabled',
|
||||
'data' => isset($options['data']['facebook_pixel_landingpage_enabled']) && (bool) $options['data']['facebook_pixel_landingpage_enabled'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'google_analytics_id',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.google.analytics.id',
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'google_analytics_trackingpage_enabled',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.tracking.trackingpage.enabled',
|
||||
'data' => isset($options['data']['google_analytics_trackingpage_enabled']) && (bool) $options['data']['google_analytics_trackingpage_enabled'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'google_analytics_landingpage_enabled',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.tracking.landingpage.enabled',
|
||||
'data' => isset($options['data']['google_analytics_landingpage_enabled']) && (bool) $options['data']['google_analytics_landingpage_enabled'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'google_analytics_anonymize_ip',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.tracking.anonymize.ip.enabled',
|
||||
'data' => isset($options['data']['google_analytics_anonymize_ip']) && (bool) $options['data']['google_analytics_anonymize_ip'],
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.page.config.form.tracking.anonymize.ip.enabled.tooltip',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'trackingconfig';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class ConfigType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'cat_in_page_url',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.cat.in.url',
|
||||
'data' => (bool) $options['data']['cat_in_page_url'],
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.page.config.form.cat.in.url.tooltip',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'google_analytics',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.form.google.analytics',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'data' => htmlspecialchars_decode((string) $options['data']['google_analytics']),
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.config.form.google.analytics.tooltip',
|
||||
'rows' => 10,
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pageconfig';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class DashboardHitsInTimeWidgetType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('flag', ChoiceType::class, [
|
||||
'label' => 'mautic.page.visit.flag.filter',
|
||||
'choices' => [
|
||||
'mautic.page.show.total.visits' => '',
|
||||
'mautic.page.show.unique.visits' => 'unique',
|
||||
'mautic.page.show.unique.and.total.visits' => 'total_and_unique',
|
||||
],
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'empty_data' => '',
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'page_dashboard_hits_in_time_widget';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PageListType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $canViewOther = false;
|
||||
|
||||
public function __construct(
|
||||
private PageModel $model,
|
||||
CorePermissions $corePermissions,
|
||||
) {
|
||||
$this->canViewOther = $corePermissions->isGranted('page:pages:viewother');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$model = $this->model;
|
||||
$canViewOther = $this->canViewOther;
|
||||
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'choices' => function (Options $options) use ($model, $canViewOther): array {
|
||||
$choices = [];
|
||||
$publishedOnly = $options['published_only'] ?? false;
|
||||
$pages = $model->getRepository()->getPageList('', 0, 0, $canViewOther, $options['top_level'], $options['ignore_ids'], [], $publishedOnly);
|
||||
foreach ($pages as $page) {
|
||||
$choices[$page['language']]["{$page['title']} ({$page['id']})"] = $page['id'];
|
||||
}
|
||||
|
||||
// sort by language
|
||||
ksort($choices);
|
||||
|
||||
foreach ($choices as &$pages) {
|
||||
ksort($pages);
|
||||
}
|
||||
|
||||
return $choices;
|
||||
},
|
||||
'placeholder' => false,
|
||||
'expanded' => false,
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'top_level' => 'variant',
|
||||
'ignore_ids' => [],
|
||||
]
|
||||
);
|
||||
|
||||
$resolver->setDefined(['top_level', 'ignore_ids', 'published_only']);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CategoryBundle\Form\Type\CategoryListType;
|
||||
use Mautic\CoreBundle\Form\DataTransformer\IdToEntityModelTransformer;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishDownDateType;
|
||||
use Mautic\CoreBundle\Form\Type\PublishUpDateType;
|
||||
use Mautic\CoreBundle\Form\Type\ThemeListType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\CoreBundle\Helper\ThemeHelperInterface;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Helper\PageConfigInterface;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Mautic\ProjectBundle\Form\Type\ProjectType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\UrlType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<Page>
|
||||
*/
|
||||
class PageType extends AbstractType
|
||||
{
|
||||
private ?\Mautic\UserBundle\Entity\User $user;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $canViewOther = false;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $em,
|
||||
private PageModel $model,
|
||||
CorePermissions $corePermissions,
|
||||
UserHelper $userHelper,
|
||||
private ThemeHelperInterface $themeHelper,
|
||||
private PageConfigInterface $pageConfig,
|
||||
) {
|
||||
$this->canViewOther = $corePermissions->isGranted('page:pages:viewother');
|
||||
$this->user = $userHelper->getUser();
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['content' => 'html', 'customHtml' => 'html', 'redirectUrl' => 'url', 'headScript' => 'html', 'footerScript' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('page.page', $options));
|
||||
|
||||
$builder->add(
|
||||
'title',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.title',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$html = $options['data']->getCustomHtml();
|
||||
if ($this->pageConfig->isDraftEnabled() && !empty($options['data']->getId()) && $options['data']->hasDraft() && !empty($options['data']->getDraft()->getHtml())) {
|
||||
$html = $options['data']->getDraft()->getHtml();
|
||||
}
|
||||
$builder->add(
|
||||
'customHtml',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.page.form.customhtml',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.page.form.customhtml.help',
|
||||
'class' => 'form-control editor-builder-tokens builder-html',
|
||||
'data-token-callback' => 'page:getBuilderTokens',
|
||||
'data-token-activator' => '{',
|
||||
'rows' => '25',
|
||||
],
|
||||
'data' => $html,
|
||||
]
|
||||
);
|
||||
|
||||
$template = $options['data']->getTemplate() ?? 'blank';
|
||||
// If theme does not exist, set empty
|
||||
$template = $this->themeHelper->getCurrentTheme($template, 'page');
|
||||
if ($this->pageConfig->isDraftEnabled() && !empty($options['data']->getId()) && $options['data']->hasDraft() && !empty($options['data']->getDraft()->getTemplate())) {
|
||||
$template = $options['data']->getDraft()->getTemplate();
|
||||
}
|
||||
$builder->add(
|
||||
'template',
|
||||
ThemeListType::class,
|
||||
[
|
||||
'feature' => 'page',
|
||||
'attr' => [
|
||||
'class' => 'form-control not-chosen hidden',
|
||||
'tooltip' => 'mautic.page.form.template.help',
|
||||
],
|
||||
'placeholder' => 'mautic.core.none',
|
||||
'data' => $template,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('isPublished', YesNoButtonGroupType::class, [
|
||||
'label' => 'mautic.core.form.available',
|
||||
]);
|
||||
|
||||
$builder->add(
|
||||
'isPreferenceCenter',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.form.preference_center',
|
||||
'data' => $options['data']->isPreferenceCenter() ?: false,
|
||||
'attr' => [
|
||||
'tooltip' => 'mautic.page.form.preference_center.tooltip',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'noIndex',
|
||||
YesNoButtonGroupType::class,
|
||||
[
|
||||
'label' => 'mautic.page.config.no_index',
|
||||
'data' => $options['data']->getNoIndex() ?: false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('publishUp', PublishUpDateType::class);
|
||||
$builder->add('publishDown', PublishDownDateType::class);
|
||||
$builder->add('sessionId', HiddenType::class);
|
||||
|
||||
// Custom field for redirect URL
|
||||
$this->model->getRepository()->setCurrentUser($this->user);
|
||||
|
||||
$redirectUrlDataOptions = '';
|
||||
$pages = $this->model->getRepository()->getPageList('', 0, 0, $this->canViewOther, 'variant', [$options['data']->getId()]);
|
||||
foreach ($pages as $page) {
|
||||
$redirectUrlDataOptions .= "|{$page['alias']}";
|
||||
}
|
||||
|
||||
$transformer = new IdToEntityModelTransformer($this->em, Page::class);
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'variantParent',
|
||||
HiddenType::class
|
||||
)->addModelTransformer($transformer)
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'translationParent',
|
||||
PageListType::class,
|
||||
[
|
||||
'label' => 'mautic.core.form.translation_parent',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.core.form.translation_parent.help',
|
||||
],
|
||||
'required' => false,
|
||||
'multiple' => false,
|
||||
'placeholder' => 'mautic.core.form.translation_parent.empty',
|
||||
'top_level' => 'translation',
|
||||
'ignore_ids' => [(int) $options['data']->getId()],
|
||||
]
|
||||
)->addModelTransformer($transformer)
|
||||
);
|
||||
|
||||
$formModifier = function (FormInterface $form, $isVariant): void {
|
||||
if ($isVariant) {
|
||||
$form->add(
|
||||
'variantSettings',
|
||||
VariantType::class,
|
||||
[
|
||||
'label' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Building the form
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event) use ($formModifier): void {
|
||||
$formModifier(
|
||||
$event->getForm(),
|
||||
$event->getData()->getVariantParent()
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// After submit
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($formModifier): void {
|
||||
$data = $event->getData();
|
||||
if (isset($data['variantParent'])) {
|
||||
$formModifier(
|
||||
$event->getForm(),
|
||||
$data['variantParent']
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'metaDescription',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.page.form.metadescription',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control', 'maxlength' => 160],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'headScript',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.page.form.headscript',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'rows' => '8',
|
||||
'tooltip' => 'mautic.page.form.script.help',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'footerScript',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.page.form.footerscript',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'rows' => '8',
|
||||
'tooltip' => 'mautic.page.form.script.help',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'redirectType',
|
||||
RedirectListType::class,
|
||||
[
|
||||
'feature' => 'page',
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.form.redirecttype.help',
|
||||
],
|
||||
'placeholder' => 'mautic.page.form.redirecttype.none',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'redirectUrl',
|
||||
UrlType::class,
|
||||
[
|
||||
'required' => true,
|
||||
'label' => 'mautic.page.form.redirecturl',
|
||||
'label_attr' => [
|
||||
'class' => 'control-label',
|
||||
],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'maxlength' => 200,
|
||||
'tooltip' => 'mautic.page.form.redirecturl.help',
|
||||
'data-hide-on' => '{"page_redirectType":""}',
|
||||
'data-toggle' => 'field-lookup',
|
||||
'data-action' => 'page:fieldList',
|
||||
'data-target' => 'redirectUrl',
|
||||
'data-options' => $redirectUrlDataOptions,
|
||||
],
|
||||
'default_protocol' => null,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'alias',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.alias',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.help.alias',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
// add category
|
||||
$builder->add(
|
||||
'category',
|
||||
CategoryListType::class,
|
||||
[
|
||||
'bundle' => 'page',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('projects', ProjectType::class);
|
||||
|
||||
$builder->add(
|
||||
'language',
|
||||
LocaleType::class,
|
||||
[
|
||||
'label' => 'mautic.core.language',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.form.language.help',
|
||||
],
|
||||
'required' => true,
|
||||
]
|
||||
);
|
||||
|
||||
$extraButtons['pre_extra_buttons'] = [
|
||||
[
|
||||
'name' => 'builder',
|
||||
'label' => 'mautic.core.builder',
|
||||
'attr' => [
|
||||
'class' => 'btn btn-tertiary btn-dnd btn-nospin btn-builder text-interactive',
|
||||
'icon' => 'ri-layout-line',
|
||||
'onclick' => "Mautic.launchBuilder('page');",
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$draftActionButtons = $this->getDraftActionButtons($options['data']);
|
||||
if (!empty($draftActionButtons)) {
|
||||
$extraButtons['post_extra_buttons'] = $draftActionButtons;
|
||||
}
|
||||
$builder->add('buttons',
|
||||
FormButtonsType::class,
|
||||
$extraButtons
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>
|
||||
*/
|
||||
private function getDraftActionButtons(Page $page): array
|
||||
{
|
||||
$draftActionButtons = [];
|
||||
if (!$this->pageConfig->isDraftEnabled() || empty($page->getId())) {
|
||||
return $draftActionButtons;
|
||||
}
|
||||
|
||||
if ($page->hasDraft()) {
|
||||
$draftActionButtons[] = [
|
||||
'name' => 'apply_draft',
|
||||
'label' => 'mautic.core.applydraft',
|
||||
'type' => SubmitType::class,
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-apply-draft btn-copy',
|
||||
'icon' => 'fa fa-files-o text-success',
|
||||
],
|
||||
];
|
||||
$draftActionButtons[] = [
|
||||
'name' => 'discard_draft',
|
||||
'label' => 'mautic.core.discarddraft',
|
||||
'type' => SubmitType::class,
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-apply-draft btn-copy',
|
||||
'icon' => 'fa fa-trash text-danger',
|
||||
],
|
||||
];
|
||||
} else {
|
||||
$draftActionButtons[] = [
|
||||
'name' => 'save_draft',
|
||||
'label' => 'mautic.core.saveasdraft',
|
||||
'type' => SubmitType::class,
|
||||
'attr' => [
|
||||
'class' => 'btn btn-primary btn-default text-primary btn-save-draft',
|
||||
'icon' => 'fa fa-file text-success',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
return $draftActionButtons;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Page::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PointActionPageHitType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('pages', PageListType::class, [
|
||||
'label' => 'mautic.page.point.action.form.pages',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.point.action.form.pages.descr',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pointaction_pagehit';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\DataTransformer\SecondsConversionTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PointActionUrlHitType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('page_url', TextType::class, [
|
||||
'label' => 'mautic.page.point.action.form.page.url',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.point.action.form.page.url.descr',
|
||||
'placeholder' => 'http://',
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add('page_hits', IntegerType::class, [
|
||||
'label' => 'mautic.page.hits',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.point.action.form.page.hits.descr',
|
||||
],
|
||||
]);
|
||||
|
||||
$formModifier = function (FormInterface $form, $data) use ($builder): void {
|
||||
$unit = $data['accumulative_time_unit'] ?? 'H';
|
||||
$form->add('accumulative_time_unit', HiddenType::class, [
|
||||
'data' => $unit,
|
||||
]);
|
||||
|
||||
$secondsTransformer = new SecondsConversionTransformer($unit);
|
||||
$form->add(
|
||||
$builder->create('accumulative_time', TextType::class, [
|
||||
'label' => 'mautic.page.point.action.form.accumulative.time',
|
||||
'required' => false,
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.point.action.form.accumulative.time.descr',
|
||||
],
|
||||
'auto_initialize' => false,
|
||||
])
|
||||
->addViewTransformer($secondsTransformer)
|
||||
->getForm()
|
||||
);
|
||||
|
||||
$unit = $data['returns_within_unit'] ?? 'H';
|
||||
$secondsTransformer = new SecondsConversionTransformer($unit);
|
||||
$form->add('returns_within_unit', HiddenType::class, [
|
||||
'data' => $unit,
|
||||
]);
|
||||
|
||||
$form->add(
|
||||
$builder->create('returns_within', TextType::class, [
|
||||
'label' => 'mautic.page.point.action.form.returns.within',
|
||||
'required' => false,
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.point.action.form.returns.within.descr',
|
||||
'onBlur' => 'Mautic.EnablesOption(this.id)',
|
||||
],
|
||||
'auto_initialize' => false,
|
||||
])
|
||||
->addViewTransformer($secondsTransformer)
|
||||
->getForm()
|
||||
);
|
||||
|
||||
$unit = $data['returns_after_unit'] ?? 'H';
|
||||
$secondsTransformer = new SecondsConversionTransformer($unit);
|
||||
$form->add('returns_after_unit', HiddenType::class, [
|
||||
'data' => $unit,
|
||||
]);
|
||||
$form->add(
|
||||
$builder->create('returns_after', TextType::class, [
|
||||
'label' => 'mautic.page.point.action.form.returns.after',
|
||||
'required' => false,
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.point.action.form.returns.after.descr',
|
||||
'onBlur' => 'Mautic.EnablesOption(this.id)',
|
||||
],
|
||||
'auto_initialize' => false,
|
||||
])
|
||||
->addViewTransformer($secondsTransformer)
|
||||
->getForm()
|
||||
);
|
||||
};
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event) use ($formModifier): void {
|
||||
$data = $event->getData();
|
||||
$formModifier($event->getForm(), $data);
|
||||
}
|
||||
);
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SUBMIT,
|
||||
function (FormEvent $event) use ($formModifier): void {
|
||||
$data = $event->getData();
|
||||
$formModifier($event->getForm(), $data);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pointaction_urlhit';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<Page>
|
||||
*/
|
||||
class PreferenceCenterListType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $canViewOther = false;
|
||||
|
||||
public function __construct(
|
||||
private PageModel $model,
|
||||
CorePermissions $corePermissions,
|
||||
) {
|
||||
$this->canViewOther = $corePermissions->isGranted('page:pages:viewother');
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$model = $this->model;
|
||||
$canViewOther = $this->canViewOther;
|
||||
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'choices' => function (Options $options) use ($model, $canViewOther): array {
|
||||
$choices = [];
|
||||
$pages = $model->getRepository()->getPageList('', 0, 0, $canViewOther, $options['top_level'], $options['ignore_ids'], ['isPreferenceCenter']);
|
||||
foreach ($pages as $page) {
|
||||
if ($page['isPreferenceCenter']) {
|
||||
$choices[$page['language']]["{$page['title']} ({$page['id']})"] = $page['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// sort by language
|
||||
ksort($choices);
|
||||
|
||||
foreach ($choices as &$pages) {
|
||||
ksort($pages);
|
||||
}
|
||||
|
||||
return $choices;
|
||||
},
|
||||
'placeholder' => false,
|
||||
'expanded' => false,
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'top_level' => 'variant',
|
||||
'ignore_ids' => [],
|
||||
]
|
||||
);
|
||||
|
||||
$resolver->setDefined(['top_level', 'ignore_ids']);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class RedirectListType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$choices = [
|
||||
'mautic.page.form.redirecttype.permanent' => Response::HTTP_MOVED_PERMANENTLY,
|
||||
'mautic.page.form.redirecttype.temporary' => Response::HTTP_FOUND,
|
||||
'mautic.page.form.redirecttype.303_temporary' => Response::HTTP_SEE_OTHER,
|
||||
'mautic.page.form.redirecttype.307_temporary' => Response::HTTP_TEMPORARY_REDIRECT,
|
||||
'mautic.page.form.redirecttype.308_permanent' => Response::HTTP_PERMANENTLY_REDIRECT,
|
||||
];
|
||||
|
||||
$resolver->setDefaults([
|
||||
'choices' => $choices,
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'label' => 'mautic.page.form.redirecttype',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'placeholder' => false,
|
||||
'required' => false,
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'feature' => 'all',
|
||||
]);
|
||||
|
||||
$resolver->setDefined(['feature']);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Mautic\PageBundle\Helper\TrackingHelper;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class TrackingPixelSendType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
protected TrackingHelper $trackingHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$trackingServices = $this->trackingHelper->getEnabledServices();
|
||||
|
||||
$builder->add('services', ChoiceType::class, [
|
||||
'label' => 'mautic.page.tracking.form.services',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'expanded' => false,
|
||||
'multiple' => true,
|
||||
'choices' => array_flip($trackingServices),
|
||||
'placeholder' => 'mautic.core.form.chooseone',
|
||||
'constraints' => [
|
||||
new NotBlank(
|
||||
['message' => 'mautic.core.ab_test.winner_criteria.not_blank']
|
||||
),
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add(
|
||||
'category',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.page.tracking.form.category',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.page.tracking.form.category.tooltip',
|
||||
],
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'action',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.page.tracking.form.action',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'label',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.page.tracking.form.label',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new NotBlank(),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'tracking_pixel_send_action';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Form\Type;
|
||||
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class VariantType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private PageModel $pageModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add(
|
||||
'weight',
|
||||
IntegerType::class, [
|
||||
'label' => 'mautic.core.ab_test.form.traffic_weight',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.core.ab_test.form.traffic_weight.help',
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank(
|
||||
['message' => 'mautic.page.variant.weight.notblank']
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$abTestWinnerCriteria = $this->pageModel->getBuilderComponents(null, 'abTestWinnerCriteria');
|
||||
|
||||
if (!empty($abTestWinnerCriteria)) {
|
||||
$criteria = $abTestWinnerCriteria['criteria'];
|
||||
$choices = $abTestWinnerCriteria['choices'];
|
||||
|
||||
$builder->add(
|
||||
'winnerCriteria',
|
||||
ChoiceType::class, [
|
||||
'label' => 'mautic.core.ab_test.form.winner',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'onchange' => 'Mautic.getAbTestWinnerForm(\'page\', \'page\', this);',
|
||||
],
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'choices' => $choices,
|
||||
'placeholder' => 'mautic.core.form.chooseone',
|
||||
'constraints' => [
|
||||
new NotBlank(
|
||||
['message' => 'mautic.core.ab_test.winner_criteria.not_blank']
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($criteria): void {
|
||||
$form = $event->getForm();
|
||||
$data = $event->getData();
|
||||
if (isset($data['winnerCriteria'])) {
|
||||
if (!empty($criteria[$data['winnerCriteria']]['formType'])) {
|
||||
$formTypeOptions = [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
];
|
||||
if (!empty($criteria[$data]['formTypeOptions'])) {
|
||||
$formTypeOptions = array_merge($formTypeOptions, $criteria[$data]['formTypeOptions']);
|
||||
}
|
||||
$form->add('properties', $criteria[$data]['formType'], $formTypeOptions);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'pagevariant';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\Helper;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
final class PageConfig implements PageConfigInterface
|
||||
{
|
||||
public function __construct(private CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
}
|
||||
|
||||
public function isDraftEnabled(): bool
|
||||
{
|
||||
return (bool) $this->coreParametersHelper->get('page_draft_enabled', false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\Helper;
|
||||
|
||||
interface PageConfigInterface
|
||||
{
|
||||
public function isDraftEnabled(): bool;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Helper;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\PageBundle\Entity\Hit;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
|
||||
class PointActionHelper
|
||||
{
|
||||
public function __construct(private EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
public static function validatePageHit($eventDetails, $action): bool
|
||||
{
|
||||
$pageHit = $eventDetails->getPage();
|
||||
|
||||
if ($pageHit instanceof Page) {
|
||||
[$parent, $children] = $pageHit->getVariants();
|
||||
// use the parent (self or configured parent)
|
||||
$pageHitId = $parent->getId();
|
||||
} else {
|
||||
$pageHitId = 0;
|
||||
}
|
||||
|
||||
// If no pages are selected, the pages array does not exist
|
||||
if (isset($action['properties']['pages'])) {
|
||||
$limitToPages = $action['properties']['pages'];
|
||||
}
|
||||
|
||||
if (!empty($limitToPages) && !in_array($pageHitId, $limitToPages)) {
|
||||
// no points change
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateUrlHit($eventDetails, $action): bool
|
||||
{
|
||||
$changePoints = [];
|
||||
$url = $eventDetails->getUrl();
|
||||
$limitToUrl = html_entity_decode(trim($action['properties']['page_url']));
|
||||
|
||||
if (!$limitToUrl || !fnmatch($limitToUrl, $url)) {
|
||||
// no points change
|
||||
return false;
|
||||
}
|
||||
|
||||
$hitRepository = $this->entityManager->getRepository(Hit::class);
|
||||
$lead = $eventDetails->getLead();
|
||||
$urlWithSqlWC = str_replace('*', '%', $limitToUrl);
|
||||
|
||||
if (isset($action['properties']['first_time']) && true === $action['properties']['first_time']) {
|
||||
$hitStats = $hitRepository->getDwellTimesForUrl($urlWithSqlWC, ['leadId' => $lead->getId()]);
|
||||
if (isset($hitStats['count']) && $hitStats['count']) {
|
||||
$changePoints['first_time'] = false;
|
||||
} else {
|
||||
$changePoints['first_time'] = true;
|
||||
}
|
||||
}
|
||||
$now = new \DateTime();
|
||||
|
||||
if ($action['properties']['returns_within'] || $action['properties']['returns_after']) {
|
||||
// get the latest hit only when it's needed
|
||||
$latestHit = $hitRepository->getLatestHit(['leadId' => $lead->getId(), $urlWithSqlWC, 'second_to_last' => $eventDetails->getId()]);
|
||||
} else {
|
||||
$latestHit = null;
|
||||
}
|
||||
|
||||
if ($action['properties']['accumulative_time']) {
|
||||
if (!isset($hitStats)) {
|
||||
$hitStats = $hitRepository->getDwellTimesForUrl($urlWithSqlWC, ['leadId' => $lead->getId()]);
|
||||
}
|
||||
|
||||
if (isset($hitStats['sum'])) {
|
||||
if ($action['properties']['accumulative_time'] <= $hitStats['sum']) {
|
||||
$changePoints['accumulative_time'] = true;
|
||||
} else {
|
||||
$changePoints['accumulative_time'] = false;
|
||||
}
|
||||
} else {
|
||||
$changePoints['accumulative_time'] = false;
|
||||
}
|
||||
}
|
||||
if ($action['properties']['page_hits']) {
|
||||
if (!isset($hitStats)) {
|
||||
$hitStats = $hitRepository->getDwellTimesForUrl($urlWithSqlWC, ['leadId' => $lead->getId()]);
|
||||
}
|
||||
if (isset($hitStats['count']) && $hitStats['count'] >= $action['properties']['page_hits']) {
|
||||
$changePoints['page_hits'] = true;
|
||||
} else {
|
||||
$changePoints['page_hits'] = false;
|
||||
}
|
||||
}
|
||||
if ($action['properties']['returns_within']) {
|
||||
if ($latestHit && $now->getTimestamp() - $latestHit->getTimestamp() <= $action['properties']['returns_within']) {
|
||||
$changePoints['returns_within'] = true;
|
||||
} else {
|
||||
$changePoints['returns_within'] = false;
|
||||
}
|
||||
}
|
||||
if ($action['properties']['returns_after']) {
|
||||
if ($latestHit && $now->getTimestamp() - $latestHit->getTimestamp() >= $action['properties']['returns_after']) {
|
||||
$changePoints['returns_after'] = true;
|
||||
} else {
|
||||
$changePoints['returns_after'] = false;
|
||||
}
|
||||
}
|
||||
|
||||
// return true only if all configured options are true
|
||||
return !in_array(false, $changePoints);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Helper;
|
||||
|
||||
use Mautic\PageBundle\Model\PageModel;
|
||||
|
||||
class TokenHelper
|
||||
{
|
||||
public const REGEX = '/{pagelink=(.*?)}/';
|
||||
|
||||
public function __construct(
|
||||
protected PageModel $model,
|
||||
) {
|
||||
}
|
||||
|
||||
public function findPageTokens($content, $clickthrough = []): array
|
||||
{
|
||||
preg_match_all(self::REGEX, $content, $matches);
|
||||
|
||||
$tokens = [];
|
||||
if (!empty($matches[1])) {
|
||||
foreach ($matches[1] as $key => $pageId) {
|
||||
$token = $matches[0][$key];
|
||||
if (!empty($tokens[$token])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$page = $this->model->getEntity($pageId);
|
||||
|
||||
if (!$page) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tokens[$token] = $this->model->generateUrl($page, true, $clickthrough);
|
||||
}
|
||||
|
||||
unset($matches);
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Helper;
|
||||
|
||||
use Mautic\CacheBundle\Cache\CacheProvider;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\Serializer;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class TrackingHelper
|
||||
{
|
||||
public function __construct(
|
||||
protected ContactTracker $contactTracker,
|
||||
protected CacheProvider $cache,
|
||||
protected CoreParametersHelper $coreParametersHelper,
|
||||
protected RequestStack $requestStack,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, 'facebook_pixel'|'google_analytics'>
|
||||
*/
|
||||
public function getEnabledServices(): array
|
||||
{
|
||||
$keys = [
|
||||
'google_analytics' => 'Google Analytics',
|
||||
'facebook_pixel' => 'Facebook Pixel',
|
||||
];
|
||||
$result = [];
|
||||
foreach ($keys as $key => $service) {
|
||||
if ($id = $this->coreParametersHelper->get($key.'_id')) {
|
||||
$result[$service] = $key;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
private function getCacheKey()
|
||||
{
|
||||
$lead = $this->contactTracker->getContact();
|
||||
|
||||
return $lead instanceof Lead ? 'mtc-tracking-pixel-events-'.$lead->getId() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $values
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function updateCacheItem(array $values): void
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
if (null !== $cacheKey) {
|
||||
$item = $this->cache->getItem($cacheKey);
|
||||
$item->set(serialize(array_merge($values, $this->getCacheItem())));
|
||||
$item->expiresAfter(86400); // one day in seconds
|
||||
|
||||
$this->cache->save($item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed[]
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function getCacheItem(bool $remove = false): array
|
||||
{
|
||||
$cacheKey = $this->getCacheKey();
|
||||
$cacheValue = [];
|
||||
|
||||
/* @var CacheItemInterface $item */
|
||||
if (null !== $cacheKey) {
|
||||
$item = $this->cache->getItem($cacheKey);
|
||||
if ($item->isHit()) {
|
||||
$cacheValue = Serializer::decode($item->get(), ['allowed_classes' => false]);
|
||||
if ($remove) {
|
||||
$this->cache->deleteItem($cacheKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (array) $cacheValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool|mixed
|
||||
*/
|
||||
public function displayInitCode($service)
|
||||
{
|
||||
$pixelId = $this->coreParametersHelper->get($service.'_id');
|
||||
|
||||
if ($pixelId && $this->coreParametersHelper->get($service.'_landingpage_enabled') && $this->isLandingPage()) {
|
||||
return $pixelId;
|
||||
}
|
||||
if ($pixelId && $this->coreParametersHelper->get($service.'_trackingpage_enabled') && !$this->isLandingPage()) {
|
||||
return $pixelId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Lead|null
|
||||
*/
|
||||
public function getLead()
|
||||
{
|
||||
return $this->contactTracker->getContact();
|
||||
}
|
||||
|
||||
public function getAnonymizeIp()
|
||||
{
|
||||
return $this->coreParametersHelper->get('google_analytics_anonymize_ip');
|
||||
}
|
||||
|
||||
protected function isLandingPage(): bool
|
||||
{
|
||||
$server = $this->requestStack->getCurrentRequest()->server;
|
||||
if (!str_contains((string) $server->get('HTTP_REFERER'), $this->coreParametersHelper->get('site_url'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticPageBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\PageBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Entity\PageDraft;
|
||||
use Mautic\PageBundle\Entity\PageDraftRepository;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class PageDraftModel
|
||||
{
|
||||
public function __construct(
|
||||
private EntityManagerInterface $entityManager,
|
||||
private PageDraftRepository $pageDraftRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function createDraft(Page $page, string $html, string $template, bool $publicPreview = true): PageDraft
|
||||
{
|
||||
$pageDraft = $this->pageDraftRepository->findOneBy(['page' => $page]);
|
||||
if (!is_null($pageDraft)) {
|
||||
throw new \Exception(sprintf('Draft already exists for page %d', $page->getId()));
|
||||
}
|
||||
$pageDraft = new PageDraft($page, $html, $template, $publicPreview);
|
||||
|
||||
$this->entityManager->persist($pageDraft);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return $pageDraft;
|
||||
}
|
||||
|
||||
public function saveDraft(PageDraft $pageDraft): void
|
||||
{
|
||||
$this->entityManager->persist($pageDraft);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function deleteDraft(Page $page): void
|
||||
{
|
||||
if (is_null($pageDraft = $page->getDraft())) {
|
||||
throw new NotFoundHttpException(sprintf('Draft not found for page %d', $page->getId()));
|
||||
}
|
||||
$this->entityManager->remove($pageDraft);
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
public function getEntity(int $id): ?PageDraft
|
||||
{
|
||||
return $this->pageDraftRepository->find($id);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'page:pages';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UrlHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Shortener\Shortener;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
use Mautic\PageBundle\Entity\RedirectRepository;
|
||||
use Mautic\PageBundle\Event\RedirectGenerationEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Redirect>
|
||||
*/
|
||||
class RedirectModel extends FormModel
|
||||
{
|
||||
public function __construct(
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
private Shortener $shortener,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function getRepository(): RedirectRepository
|
||||
{
|
||||
return $this->em->getRepository(Redirect::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Redirect|null
|
||||
*/
|
||||
public function getRedirectById($identifier)
|
||||
{
|
||||
return $this->getRepository()->findOneBy(['redirectId' => $identifier]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a Mautic redirect/passthrough URL.
|
||||
*
|
||||
* @param array $clickthrough
|
||||
* @param bool $shortenUrl
|
||||
* @param array $utmTags
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateRedirectUrl(Redirect $redirect, $clickthrough = [], $shortenUrl = false, $utmTags = [])
|
||||
{
|
||||
if ($this->dispatcher->hasListeners(PageEvents::ON_REDIRECT_GENERATE)) {
|
||||
$event = new RedirectGenerationEvent($redirect, $clickthrough);
|
||||
$this->dispatcher->dispatch($event, PageEvents::ON_REDIRECT_GENERATE);
|
||||
|
||||
$clickthrough = $event->getClickthrough();
|
||||
}
|
||||
|
||||
$url = $this->buildUrl(
|
||||
'mautic_url_redirect',
|
||||
['redirectId' => $redirect->getRedirectId()],
|
||||
true,
|
||||
$clickthrough
|
||||
);
|
||||
|
||||
if (!empty($utmTags)) {
|
||||
$utmTags = $this->getUtmTagsForUrl($utmTags);
|
||||
$appendUtmString = http_build_query($utmTags, '', '&');
|
||||
$url = UrlHelper::appendQueryToUrl($url, $appendUtmString);
|
||||
}
|
||||
|
||||
if ($shortenUrl) {
|
||||
$url = $this->shortener->shortenUrl($url);
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate UTMs params for url.
|
||||
*/
|
||||
public function getUtmTagsForUrl($rawUtmTags): array
|
||||
{
|
||||
$utmTags = [];
|
||||
foreach ($rawUtmTags as $utmTag => $value) {
|
||||
$utmTags[str_replace('utm', 'utm_', strtolower($utmTag))] = $value;
|
||||
}
|
||||
|
||||
return $utmTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Redirect entity by URL.
|
||||
*
|
||||
* Use Mautic\PageBundle\Model\TrackableModel::getTrackableByUrl() if associated with a channel
|
||||
*
|
||||
* @return Redirect|null
|
||||
*/
|
||||
public function getRedirectByUrl($url)
|
||||
{
|
||||
// Ensure the URL saved to the database does not have encoded ampersands
|
||||
$url = UrlHelper::decodeAmpersands($url);
|
||||
|
||||
$repo = $this->getRepository();
|
||||
$redirect = $repo->findOneBy(['url' => $url]);
|
||||
|
||||
if (null == $redirect) {
|
||||
$redirect = $this->createRedirectEntity($url);
|
||||
}
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Redirect entities by an array of URLs.
|
||||
*
|
||||
* @return array<Redirect>
|
||||
*/
|
||||
public function getRedirectsByUrls(array $urls)
|
||||
{
|
||||
/** @var array<Redirect> $redirects */
|
||||
$redirects = $this->getRepository()->findByUrls(array_values($urls));
|
||||
$newEntities = [];
|
||||
|
||||
/** @var array<string, Redirect> $return */
|
||||
$return = [];
|
||||
|
||||
/** @var array<string, Redirect> $byUrl */
|
||||
$byUrl = [];
|
||||
|
||||
foreach ($redirects as $redirect) {
|
||||
$byUrl[$redirect->getUrl()] = $redirect;
|
||||
}
|
||||
|
||||
foreach ($urls as $key => $url) {
|
||||
if (empty($url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($byUrl[$url])) {
|
||||
$return[$key] = $byUrl[$url];
|
||||
} else {
|
||||
$redirect = $this->createRedirectEntity($url);
|
||||
$newEntities[] = $redirect;
|
||||
$return[$key] = $redirect;
|
||||
}
|
||||
}
|
||||
|
||||
// Save new entities
|
||||
if (count($newEntities)) {
|
||||
$this->getRepository()->saveEntities($newEntities);
|
||||
}
|
||||
|
||||
unset($redirects, $newEntities, $byUrl);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Redirect entity for URL.
|
||||
*
|
||||
* @return Redirect
|
||||
*/
|
||||
public function createRedirectEntity($url)
|
||||
{
|
||||
$redirect = new Redirect();
|
||||
$redirect->setUrl($url);
|
||||
$redirect->setRedirectId();
|
||||
|
||||
$this->setTimestamps($redirect, true);
|
||||
|
||||
return $redirect;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,892 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UrlHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\AbstractCommonModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\LeadFieldRepository;
|
||||
use Mautic\LeadBundle\Helper\TokenHelper;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
use Mautic\PageBundle\Entity\Trackable;
|
||||
use Mautic\PageBundle\Event\UntrackableUrlsEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractCommonModel<Trackable>
|
||||
*/
|
||||
class TrackableModel extends AbstractCommonModel
|
||||
{
|
||||
/**
|
||||
* Array of URLs and/or tokens that should not be converted to trackables.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $doNotTrack = [];
|
||||
|
||||
/**
|
||||
* Tokens with values that could be used as URLs.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $contentTokens = [];
|
||||
|
||||
/**
|
||||
* Stores content that needs to be replaced when URLs are parsed out of content.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $contentReplacements = [];
|
||||
|
||||
/**
|
||||
* Used to rebuild correct URLs when the tokenized URL contains query parameters.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $usingClickthrough = true;
|
||||
|
||||
private ?array $contactFieldUrlTokens = null;
|
||||
|
||||
public function __construct(
|
||||
protected RedirectModel $redirectModel,
|
||||
private LeadFieldRepository $leadFieldRepository,
|
||||
EntityManagerInterface $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \Mautic\PageBundle\Entity\TrackableRepository
|
||||
*/
|
||||
public function getRepository()
|
||||
{
|
||||
return $this->em->getRepository(Trackable::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RedirectModel
|
||||
*/
|
||||
protected function getRedirectModel()
|
||||
{
|
||||
return $this->redirectModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $clickthrough
|
||||
* @param bool|false $shortenUrl If true, use the configured shortener service to shorten the URLs
|
||||
* @param array $utmTags
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateTrackableUrl(Trackable $trackable, $clickthrough = [], $shortenUrl = false, $utmTags = [])
|
||||
{
|
||||
if (!isset($clickthrough['channel'])) {
|
||||
$clickthrough['channel'] = [$trackable->getChannel() => $trackable->getChannelId()];
|
||||
}
|
||||
|
||||
$redirect = $trackable->getRedirect();
|
||||
|
||||
return $this->getRedirectModel()->generateRedirectUrl($redirect, $clickthrough, $shortenUrl, $utmTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a channel Trackable entity by URL.
|
||||
*
|
||||
* @return Trackable|null
|
||||
*/
|
||||
public function getTrackableByUrl($url, $channel, $channelId)
|
||||
{
|
||||
if (empty($url)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the URL saved to the database does not have encoded ampersands
|
||||
$url = UrlHelper::decodeAmpersands($url);
|
||||
|
||||
$trackable = $this->getRepository()->findByUrl($url, $channel, $channelId);
|
||||
if (null == $trackable) {
|
||||
$trackable = $this->createTrackableEntity($url, $channel, $channelId);
|
||||
$this->getRepository()->saveEntity($trackable->getRedirect());
|
||||
$this->getRepository()->saveEntity($trackable);
|
||||
}
|
||||
|
||||
return $trackable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Trackable entities by an array of URLs.
|
||||
*
|
||||
* @return array<Trackable>
|
||||
*/
|
||||
public function getTrackablesByUrls($urls, $channel, $channelId)
|
||||
{
|
||||
$uniqueUrls = array_unique(
|
||||
array_values($urls)
|
||||
);
|
||||
|
||||
$trackables = $this->getRepository()->findByUrls(
|
||||
$uniqueUrls,
|
||||
$channel,
|
||||
$channelId
|
||||
);
|
||||
|
||||
$newRedirects = [];
|
||||
$newTrackables = [];
|
||||
|
||||
/** @var array<Trackable> $return */
|
||||
$return = [];
|
||||
|
||||
/** @var array<string, Trackable> $byUrl */
|
||||
$byUrl = [];
|
||||
|
||||
/** @var Trackable $trackable */
|
||||
foreach ($trackables as $trackable) {
|
||||
$url = $trackable->getRedirect()->getUrl();
|
||||
$byUrl[$url] = $trackable;
|
||||
}
|
||||
|
||||
foreach ($urls as $key => $url) {
|
||||
if (empty($url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($byUrl[$url])) {
|
||||
$return[$key] = $byUrl[$url];
|
||||
} else {
|
||||
$trackable = $this->createTrackableEntity($url, $channel, $channelId);
|
||||
// Redirect has to be saved first to have ID available
|
||||
$newRedirects[] = $trackable->getRedirect();
|
||||
$newTrackables[] = $trackable;
|
||||
$return[$key] = $trackable;
|
||||
// Keep track so it can be re-used if applicable
|
||||
$byUrl[$url] = $trackable;
|
||||
}
|
||||
}
|
||||
|
||||
// Save new entities
|
||||
if (count($newRedirects)) {
|
||||
$this->getRepository()->saveEntities($newRedirects);
|
||||
}
|
||||
if (count($newTrackables)) {
|
||||
$this->getRepository()->saveEntities($newTrackables);
|
||||
}
|
||||
|
||||
unset($trackables, $newRedirects, $newTrackables, $byUrl);
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of URLs that are tracked by a specific channel.
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function getTrackableList($channel, $channelId): array
|
||||
{
|
||||
return $this->getRepository()->findByChannel($channel, $channelId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of tokens and/or URLs that should not be converted to trackables.
|
||||
*
|
||||
* @param string|string[]|null $content
|
||||
*/
|
||||
public function getDoNotTrackList($content): array
|
||||
{
|
||||
/** @var UntrackableUrlsEvent $event */
|
||||
$event = $this->dispatcher->dispatch(
|
||||
new UntrackableUrlsEvent($content),
|
||||
PageEvents::REDIRECT_DO_NOT_TRACK
|
||||
);
|
||||
|
||||
return $event->getDoNotTrackList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract URLs from content and return as trackables.
|
||||
*
|
||||
* @param string|string[] $content
|
||||
* @param string[] $contentTokens
|
||||
* @param ?string $channel
|
||||
* @param ?int $channelId
|
||||
* @param bool $usingClickthrough Set to false if not using a clickthrough parameter.
|
||||
* This is to ensure that URLs are built correctly with ? or & for
|
||||
* URLs tracked that include query parameters
|
||||
*
|
||||
* @return array{string|string[],Redirect[]|Trackable[]}
|
||||
*/
|
||||
public function parseContentForTrackables($content, array $contentTokens = [], $channel = null, $channelId = null, $usingClickthrough = true): array
|
||||
{
|
||||
$this->usingClickthrough = $usingClickthrough;
|
||||
|
||||
// Set do not track list for validateUrlIsTrackable()
|
||||
$this->doNotTrack = $this->getDoNotTrackList($content);
|
||||
|
||||
// Set content tokens used by validateUrlIsTrackable()
|
||||
$this->contentTokens = $contentTokens;
|
||||
|
||||
$contentWasString = false;
|
||||
if (!is_array($content)) {
|
||||
$contentWasString = true;
|
||||
$content = [$content];
|
||||
}
|
||||
|
||||
$trackableTokens = [];
|
||||
foreach ($content as $key => $text) {
|
||||
$content[$key] = $this->parseContent($text, $channel, $channelId, $trackableTokens);
|
||||
}
|
||||
|
||||
return [
|
||||
$contentWasString ? $content[0] : $content,
|
||||
$trackableTokens,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts array of Trackable or Redirect entities into {trackable} tokens.
|
||||
*
|
||||
* @param array<string, Trackable|Redirect> $entities
|
||||
*
|
||||
* @return array<string, Redirect|Trackable>
|
||||
*/
|
||||
protected function createTrackingTokens(array $entities): array
|
||||
{
|
||||
$tokens = [];
|
||||
foreach ($entities as $trackable) {
|
||||
$redirect = ($trackable instanceof Trackable) ? $trackable->getRedirect() : $trackable;
|
||||
$token = '{trackable='.$redirect->getRedirectId().'}';
|
||||
$tokens[$token] = $trackable;
|
||||
|
||||
// Store the URL to be replaced by a token
|
||||
$this->contentReplacements['second_pass'][$redirect->getUrl()] = $token;
|
||||
}
|
||||
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares content for tokenized trackable URLs by replacing them with {trackable=ID} tokens.
|
||||
*
|
||||
* @param string $content
|
||||
* @param string $type html|text
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function prepareContentWithTrackableTokens($content, $type)
|
||||
{
|
||||
if (empty($content)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Simple search and replace to remove attributes, schema for tokens, and updating URL parameter order
|
||||
$firstPassSearch = array_keys($this->contentReplacements['first_pass']);
|
||||
$firstPassReplace = $this->contentReplacements['first_pass'];
|
||||
$content = str_ireplace($firstPassSearch, $firstPassReplace, $content);
|
||||
|
||||
// Sort longer to shorter strings to ensure that URLs that share the same base are appropriately replaced
|
||||
uksort($this->contentReplacements['second_pass'], fn ($a, $b): int => strlen($b) - strlen($a));
|
||||
|
||||
if ('html' == $type) {
|
||||
// For HTML, replace only the links; leaving the link text (if a URL) intact
|
||||
foreach ($this->contentReplacements['second_pass'] as $search => $replace) {
|
||||
// Make the search regular expression match both "&" and "&".
|
||||
$search = preg_quote($search, '/');
|
||||
$search = str_replace('&', '&', $search);
|
||||
$search = str_replace('&', '(?:&|&)', $search);
|
||||
$content = preg_replace(
|
||||
'/<(.*?) href=(["\'])'.$search.'(.*?)\\2(.*?)>/i',
|
||||
'<$1 href=$2'.$replace.'$3$2$4>',
|
||||
$content
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// For text, just do a simple search/replace
|
||||
$secondPassSearch = array_keys($this->contentReplacements['second_pass']);
|
||||
$secondPassReplace = $this->contentReplacements['second_pass'];
|
||||
$content = str_ireplace($secondPassSearch, $secondPassReplace, $content);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function extractTrackablesFromContent($content)
|
||||
{
|
||||
if (0 !== preg_match('/<[^<]+>/', $content)) {
|
||||
// Parse as HTML
|
||||
$trackableUrls = $this->extractTrackablesFromHtml($content);
|
||||
} else {
|
||||
// Parse as plain text
|
||||
$trackableUrls = $this->extractTrackablesFromText($content);
|
||||
}
|
||||
|
||||
return $trackableUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find URLs in HTML and parse into trackables.
|
||||
*
|
||||
* @param string $html HTML content
|
||||
*/
|
||||
protected function extractTrackablesFromHtml($html): array
|
||||
{
|
||||
// Find links using DOM to only find <a> tags
|
||||
$libxmlPreviousState = libxml_use_internal_errors(true);
|
||||
libxml_use_internal_errors(true);
|
||||
$dom = new \DOMDocument();
|
||||
$dom->loadHTML('<?xml encoding="UTF-8">'.$html);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($libxmlPreviousState);
|
||||
$links = $dom->getElementsByTagName('a');
|
||||
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$maps = $xpath->query('//map/area');
|
||||
|
||||
return array_merge($this->extractTrackables($links), $this->extractTrackables($maps));
|
||||
}
|
||||
|
||||
/**
|
||||
* Find URLs in plain text and parse into trackables.
|
||||
*
|
||||
* @param string $text Plain text content
|
||||
*/
|
||||
protected function extractTrackablesFromText($text): array
|
||||
{
|
||||
// Remove any HTML tags (such as img) that could contain href or src attributes prior to parsing for links
|
||||
$text = strip_tags($text);
|
||||
|
||||
// Get a list of URL type contact fields
|
||||
$allUrls = UrlHelper::getUrlsFromPlaintext($text, $this->getContactFieldUrlTokens());
|
||||
$trackableUrls = [];
|
||||
|
||||
foreach ($allUrls as $url) {
|
||||
if ($preparedUrl = $this->prepareUrlForTracking($url)) {
|
||||
[$urlKey, $urlValue] = $preparedUrl;
|
||||
$trackableUrls[$urlKey] = $urlValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $trackableUrls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Trackable entity.
|
||||
*/
|
||||
protected function createTrackableEntity($url, $channel, $channelId): Trackable
|
||||
{
|
||||
$redirect = $this->getRedirectModel()->createRedirectEntity($url);
|
||||
|
||||
$trackable = new Trackable();
|
||||
$trackable->setChannel($channel)
|
||||
->setChannelId($channelId)
|
||||
->setRedirect($redirect);
|
||||
|
||||
return $trackable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and parse link for tracking.
|
||||
*
|
||||
* @return bool|non-empty-array<mixed, mixed>
|
||||
*/
|
||||
protected function prepareUrlForTracking(string $url)
|
||||
{
|
||||
// Ensure it's clean
|
||||
$url = trim($url);
|
||||
|
||||
// Ensure ampersands are & for the sake of parsing
|
||||
$url = UrlHelper::decodeAmpersands($url);
|
||||
|
||||
// If this is just a token, validate it's supported before going further
|
||||
if (preg_match('/^{.*?}$/i', $url) && !$this->validateTokenIsTrackable($url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Default key and final URL to the given $url
|
||||
$trackableKey = $trackableUrl = $url;
|
||||
|
||||
// Convert URL
|
||||
$urlParts = parse_url($url);
|
||||
|
||||
// We need to ignore not parsable and invalid urls
|
||||
if (false === $urlParts || !$this->isValidUrl($urlParts, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if URL is trackable
|
||||
$tokenizedHost = (!isset($urlParts['host']) && isset($urlParts['path'])) ? $urlParts['path'] : $urlParts['host'];
|
||||
if (preg_match('/^(\{\S+?\})/', $tokenizedHost, $match)) {
|
||||
$token = $match[1];
|
||||
|
||||
// Tokenized hosts that are standalone tokens shouldn't use a scheme since the token value should contain it
|
||||
if ($token === $tokenizedHost && $scheme = (!empty($urlParts['scheme'])) ? $urlParts['scheme'] : false) {
|
||||
// Token has a schema so let's get rid of it before replacing tokens
|
||||
$this->contentReplacements['first_pass'][$scheme.'://'.$tokenizedHost] = $tokenizedHost;
|
||||
unset($urlParts['scheme']);
|
||||
}
|
||||
|
||||
// Validate that the token is something that can be trackable
|
||||
if (!$this->validateTokenIsTrackable($token, $tokenizedHost)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not convert contact tokens
|
||||
if (!$this->isContactFieldToken($token)) {
|
||||
$trackableUrl = (!empty($urlParts['query'])) ? $this->contentTokens[$token].'?'.$urlParts['query'] : $this->contentTokens[$token];
|
||||
$trackableKey = $trackableUrl;
|
||||
|
||||
// Replace the URL token with the actual URL
|
||||
$this->contentReplacements['first_pass'][$url] = $trackableUrl;
|
||||
}
|
||||
} else {
|
||||
// Regular URL without a tokenized host
|
||||
$trackableUrl = $this->httpBuildUrl($urlParts);
|
||||
|
||||
if ($this->isInDoNotTrack($trackableUrl)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isInDoNotTrack($trackableUrl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [$trackableKey, $trackableUrl];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a URL/token is in the do not track list.
|
||||
*/
|
||||
protected function isInDoNotTrack($url): bool
|
||||
{
|
||||
// Ensure it's not in the do not track list
|
||||
foreach ($this->doNotTrack as $notTrackable) {
|
||||
if (preg_match('~'.$notTrackable.'~', $url)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a token is trackable as a URL.
|
||||
*/
|
||||
protected function validateTokenIsTrackable($token, $tokenizedHost = null): bool
|
||||
{
|
||||
// Validate if this token is listed as not to be tracked
|
||||
if ($this->isInDoNotTrack($token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->isContactFieldToken($token)) {
|
||||
// Assume it's true as the redirect methods should handle this dynamically
|
||||
return true;
|
||||
}
|
||||
|
||||
$tokenValue = TokenHelper::getValueFromTokens($this->contentTokens, $token);
|
||||
|
||||
// Validate that the token is available
|
||||
if (!$tokenValue) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($tokenizedHost) {
|
||||
$url = str_ireplace($token, $tokenValue, $tokenizedHost);
|
||||
|
||||
return $this->isValidUrl($url, false);
|
||||
}
|
||||
|
||||
if (!$this->isValidUrl($tokenValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $forceScheme
|
||||
*/
|
||||
protected function isValidUrl($url, $forceScheme = true): bool
|
||||
{
|
||||
$urlParts = (!is_array($url)) ? parse_url($url) : $url;
|
||||
|
||||
// Ensure a applicable URL (rule out URLs as just #)
|
||||
if (!isset($urlParts['host']) && !isset($urlParts['path'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure a valid scheme
|
||||
if (($forceScheme && !isset($urlParts['scheme']))
|
||||
|| (isset($urlParts['scheme'])
|
||||
&& !in_array(
|
||||
$urlParts['scheme'],
|
||||
['http', 'https', 'ftp', 'ftps', 'mailto']
|
||||
))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and extract tokens from the URL as this have to be processed outside of tracking tokens.
|
||||
*
|
||||
* @param $urlParts Array from parse_url
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
protected function extractTokensFromQuery(&$urlParts)
|
||||
{
|
||||
$tokenizedParams = false;
|
||||
|
||||
// Check for a token with a query appended such as {pagelink=1}&key=value
|
||||
if (isset($urlParts['path']) && preg_match('/([https?|ftps?]?\{.*?\})&(.*?)$/', $urlParts['path'], $match)) {
|
||||
$urlParts['path'] = $match[1];
|
||||
if (isset($urlParts['query'])) {
|
||||
// Likely won't happen but append if this exists
|
||||
$urlParts['query'] .= '&'.$match[2];
|
||||
} else {
|
||||
$urlParts['query'] = $match[2];
|
||||
}
|
||||
}
|
||||
|
||||
// Check for tokens in the query
|
||||
if (!empty($urlParts['query'])) {
|
||||
[$tokenizedParams, $untokenizedParams] = $this->parseTokenizedQuery($urlParts['query']);
|
||||
if ($tokenizedParams) {
|
||||
// Rebuild the query without the tokenized query params for now
|
||||
$urlParts['query'] = $this->httpBuildQuery($untokenizedParams);
|
||||
}
|
||||
}
|
||||
|
||||
return $tokenizedParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group query parameters into those that have tokens and those that do not.
|
||||
*
|
||||
* @return array<array<string, mixed>> [$tokenizedParams[], $untokenizedParams[]]
|
||||
*/
|
||||
protected function parseTokenizedQuery($query): array
|
||||
{
|
||||
$tokenizedParams =
|
||||
$untokenizedParams = [];
|
||||
|
||||
// Test to see if there are tokens in the query and if so, extract and append them to the end of the tracked link
|
||||
if (preg_match('/(\{\S+?\})/', $query)) {
|
||||
// Equal signs in tokens will confuse parse_str so they need to be encoded
|
||||
$query = preg_replace('/\{(\S+?)=(\S+?)\}/', '{$1%3D$2}', $query);
|
||||
|
||||
parse_str($query, $queryParts);
|
||||
|
||||
foreach ($queryParts as $key => $value) {
|
||||
if (preg_match('/(\{\S+?\})/', $key) || preg_match('/(\{\S+?\})/', $value)) {
|
||||
$tokenizedParams[$key] = $value;
|
||||
} else {
|
||||
$untokenizedParams[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [$tokenizedParams, $untokenizedParams];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, Trackable|Redirect>
|
||||
*/
|
||||
protected function getEntitiesFromUrls($trackableUrls, $channel, $channelId)
|
||||
{
|
||||
if (!empty($channel) && !empty($channelId)) {
|
||||
// Track as channel aware
|
||||
return $this->getTrackablesByUrls($trackableUrls, $channel, $channelId);
|
||||
}
|
||||
|
||||
// Simple redirects
|
||||
return $this->getRedirectModel()->getRedirectsByUrls($trackableUrls);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function httpBuildUrl($parts)
|
||||
{
|
||||
if (function_exists('http_build_url')) {
|
||||
return http_build_url($parts);
|
||||
} else {
|
||||
/*
|
||||
* Used if extension is not installed
|
||||
*
|
||||
* http_build_url
|
||||
* Stand alone version of http_build_url (http://php.net/manual/en/function.http-build-url.php)
|
||||
* Based on buggy and inefficient version I found at http://www.mediafire.com/?zjry3tynkg5 by tycoonmaster[at]gmail[dot]com
|
||||
*
|
||||
* @author Chris Nasr (chris[at]fuelforthefire[dot]ca)
|
||||
* @copyright Fuel for the Fire
|
||||
* @package http
|
||||
* @version 0.1
|
||||
* @created 2012-07-26
|
||||
*/
|
||||
|
||||
if (!defined('HTTP_URL_REPLACE')) {
|
||||
// Define constants
|
||||
define('HTTP_URL_REPLACE', 0x0001); // Replace every part of the first URL when there's one of the second URL
|
||||
define('HTTP_URL_JOIN_PATH', 0x0002); // Join relative paths
|
||||
define('HTTP_URL_JOIN_QUERY', 0x0004); // Join query strings
|
||||
define('HTTP_URL_STRIP_USER', 0x0008); // Strip any user authentication information
|
||||
define('HTTP_URL_STRIP_PASS', 0x0010); // Strip any password authentication information
|
||||
define('HTTP_URL_STRIP_PORT', 0x0020); // Strip explicit port numbers
|
||||
define('HTTP_URL_STRIP_PATH', 0x0040); // Strip complete path
|
||||
define('HTTP_URL_STRIP_QUERY', 0x0080); // Strip query string
|
||||
define('HTTP_URL_STRIP_FRAGMENT', 0x0100); // Strip any fragments (#identifier)
|
||||
|
||||
// Combination constants
|
||||
define('HTTP_URL_STRIP_AUTH', HTTP_URL_STRIP_USER | HTTP_URL_STRIP_PASS);
|
||||
define('HTTP_URL_STRIP_ALL', HTTP_URL_STRIP_AUTH | HTTP_URL_STRIP_PORT | HTTP_URL_STRIP_QUERY | HTTP_URL_STRIP_FRAGMENT);
|
||||
}
|
||||
|
||||
$flags = HTTP_URL_REPLACE;
|
||||
$url = [];
|
||||
|
||||
// Scheme and Host are always replaced
|
||||
if (isset($parts['scheme'])) {
|
||||
$url['scheme'] = $parts['scheme'];
|
||||
}
|
||||
if (isset($parts['host'])) {
|
||||
$url['host'] = $parts['host'];
|
||||
}
|
||||
|
||||
// (If applicable) Replace the original URL with it's new parts
|
||||
if (HTTP_URL_REPLACE & $flags) {
|
||||
// Go through each possible key
|
||||
foreach (['user', 'pass', 'port', 'path', 'query', 'fragment'] as $key) {
|
||||
// If it's set in $parts, replace it in $url
|
||||
if (isset($parts[$key])) {
|
||||
$url[$key] = $parts[$key];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Join the original URL path with the new path
|
||||
if (isset($parts['path']) && (HTTP_URL_JOIN_PATH & $flags)) {
|
||||
if (isset($url['path']) && '' != $url['path']) {
|
||||
// If the URL doesn't start with a slash, we need to merge
|
||||
if ('/' != $url['path'][0]) {
|
||||
// If the path ends with a slash, store as is
|
||||
if ('/' == $parts['path'][strlen($parts['path']) - 1]) {
|
||||
$sBasePath = $parts['path'];
|
||||
} // Else trim off the file
|
||||
else {
|
||||
// Get just the base directory
|
||||
$sBasePath = dirname($parts['path']);
|
||||
}
|
||||
|
||||
// If it's empty
|
||||
if ('' == $sBasePath) {
|
||||
$sBasePath = '/';
|
||||
}
|
||||
|
||||
// Add the two together
|
||||
$url['path'] = $sBasePath.$url['path'];
|
||||
|
||||
// Free memory
|
||||
unset($sBasePath);
|
||||
}
|
||||
|
||||
if (str_contains($url['path'], './')) {
|
||||
// Remove any '../' and their directories
|
||||
while (preg_match('/\w+\/\.\.\//', $url['path'])) {
|
||||
$url['path'] = preg_replace('/\w+\/\.\.\//', '', $url['path']);
|
||||
}
|
||||
|
||||
// Remove any './'
|
||||
$url['path'] = str_replace('./', '', $url['path']);
|
||||
}
|
||||
} else {
|
||||
$url['path'] = $parts['path'];
|
||||
}
|
||||
}
|
||||
|
||||
// Join the original query string with the new query string
|
||||
if (isset($parts['query']) && (HTTP_URL_JOIN_QUERY & $flags)) {
|
||||
if (isset($url['query'])) {
|
||||
$url['query'] .= '&'.$parts['query'];
|
||||
} else {
|
||||
$url['query'] = $parts['query'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strips all the applicable sections of the URL
|
||||
if (HTTP_URL_STRIP_USER & $flags) {
|
||||
unset($url['user']);
|
||||
}
|
||||
if (HTTP_URL_STRIP_PASS & $flags) {
|
||||
unset($url['pass']);
|
||||
}
|
||||
if (HTTP_URL_STRIP_PORT & $flags) {
|
||||
unset($url['port']);
|
||||
}
|
||||
if (HTTP_URL_STRIP_PATH & $flags) {
|
||||
unset($url['path']);
|
||||
}
|
||||
if (HTTP_URL_STRIP_QUERY & $flags) {
|
||||
unset($url['query']);
|
||||
}
|
||||
if (HTTP_URL_STRIP_FRAGMENT & $flags) {
|
||||
unset($url['fragment']);
|
||||
}
|
||||
|
||||
// Combine the new elements into a string and return it
|
||||
return
|
||||
((isset($url['scheme'])) ? 'mailto' == $url['scheme'] ? $url['scheme'].':' : $url['scheme'].'://' : '')
|
||||
.((isset($url['user'])) ? $url['user'].((isset($url['pass'])) ? ':'.$url['pass'] : '').'@' : '')
|
||||
.($url['host'] ?? '')
|
||||
.((isset($url['port'])) ? ':'.$url['port'] : '')
|
||||
.($url['path'] ?? '')
|
||||
.((!empty($url['query'])) ? '?'.$url['query'] : '')
|
||||
.((!empty($url['fragment'])) ? '#'.$url['fragment'] : '');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build query string while accounting for tokens that include an equal sign.
|
||||
*
|
||||
* @return mixed|string
|
||||
*/
|
||||
protected function httpBuildQuery(array $queryParts)
|
||||
{
|
||||
$query = http_build_query($queryParts);
|
||||
|
||||
// http_build_query likely encoded tokens so that has to be fixed so they get replaced
|
||||
$query = preg_replace_callback(
|
||||
'/%7B(\S+?)%7D/i',
|
||||
fn ($matches): string => urldecode($matches[0]),
|
||||
$query
|
||||
);
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function isContactFieldToken($token): bool
|
||||
{
|
||||
return str_contains($token, '{contactfield') || str_contains($token, '{leadfield');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int|string, Redirect|Trackable> $trackableTokens
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function parseContent($content, $channel, $channelId, array &$trackableTokens)
|
||||
{
|
||||
// Reset content replacement arrays
|
||||
$this->contentReplacements = [
|
||||
// PHPSTAN reported duplicate keys in this array. I can't determine which is the right one.
|
||||
// I'm leaving the second one to keep current behaviour but leaving the first one commented
|
||||
// out as it may be the one we want.
|
||||
// 'first_pass' => [
|
||||
// // Remove internal attributes
|
||||
// // Editor may convert to HTML4
|
||||
// 'mautic:disable-tracking=""' => '',
|
||||
// // HTML5
|
||||
// 'mautic:disable-tracking' => '',
|
||||
// ],
|
||||
'first_pass' => [],
|
||||
'second_pass' => [],
|
||||
];
|
||||
|
||||
$trackableUrls = $this->extractTrackablesFromContent($content);
|
||||
$contentType = (preg_match('/<(.*?) href/i', $content)) ? 'html' : 'text';
|
||||
if (count($trackableUrls)) {
|
||||
// Create Trackable/Redirect entities for the URLs
|
||||
$entities = $this->getEntitiesFromUrls($trackableUrls, $channel, $channelId);
|
||||
unset($trackableUrls);
|
||||
|
||||
// Get a list of url => token to return to calling method and also to be used to
|
||||
// replace the urls in the content with tokens
|
||||
$trackableTokens = array_merge(
|
||||
$trackableTokens,
|
||||
$this->createTrackingTokens($entities)
|
||||
);
|
||||
|
||||
unset($entities);
|
||||
|
||||
// Replace URLs in content with tokens
|
||||
$content = $this->prepareContentWithTrackableTokens($content, $contentType);
|
||||
} elseif (!empty($this->contentReplacements['first_pass'])) {
|
||||
// Replace URLs in content with tokens
|
||||
$content = $this->prepareContentWithTrackableTokens($content, $contentType);
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getContactFieldUrlTokens()
|
||||
{
|
||||
if (null !== $this->contactFieldUrlTokens) {
|
||||
return $this->contactFieldUrlTokens;
|
||||
}
|
||||
|
||||
$this->contactFieldUrlTokens = [];
|
||||
|
||||
$fieldEntities = $this->leadFieldRepository->getFieldsByType('url');
|
||||
foreach ($fieldEntities as $field) {
|
||||
$this->contactFieldUrlTokens[] = $field->getAlias();
|
||||
}
|
||||
|
||||
$this->leadFieldRepository->detachEntities($fieldEntities);
|
||||
|
||||
return $this->contactFieldUrlTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DOMNodeList<\DOMNode> $links
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function extractTrackables(\DOMNodeList $links): array
|
||||
{
|
||||
$trackableUrls = [];
|
||||
/** @var \DOMElement $link */
|
||||
foreach ($links as $link) {
|
||||
$url = $link->getAttribute('href');
|
||||
|
||||
if ('' === $url) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for a do not track
|
||||
if ($link->hasAttribute('mautic:disable-tracking')) {
|
||||
$this->doNotTrack[$url] = $url;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($preparedUrl = $this->prepareUrlForTracking($url)) {
|
||||
[$urlKey, $urlValue] = $preparedUrl;
|
||||
$trackableUrls[$urlKey] = $urlValue;
|
||||
}
|
||||
}
|
||||
|
||||
return $trackableUrls;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Entity\Page;
|
||||
use Mautic\PageBundle\Entity\Redirect;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class Tracking404Model
|
||||
{
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private ContactTracker $contactTracker,
|
||||
private PageModel $pageModel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Page|Redirect $entity
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function hitPage($entity, Request $request): void
|
||||
{
|
||||
$this->pageModel->hitPage($entity, $request, 404);
|
||||
}
|
||||
|
||||
public function isTrackable(): bool
|
||||
{
|
||||
if (!$this->coreParametersHelper->get('do_not_track_404_anonymous')) {
|
||||
return true;
|
||||
}
|
||||
// already tracked and identified contact
|
||||
if ($lead = $this->contactTracker->getContactByTrackedDevice()) {
|
||||
if (!$lead->isAnonymous()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\LeadBundle\Entity\Lead;
|
||||
use Mautic\LeadBundle\Tracker\ContactTracker;
|
||||
use Mautic\PageBundle\Entity\VideoHit;
|
||||
use Mautic\PageBundle\Entity\VideoHitRepository;
|
||||
use Mautic\PageBundle\Event\VideoHitEvent;
|
||||
use Mautic\PageBundle\PageEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* @extends FormModel<VideoHit>
|
||||
*/
|
||||
class VideoModel extends FormModel
|
||||
{
|
||||
public function __construct(
|
||||
protected IpLookupHelper $ipLookupHelper,
|
||||
protected ContactTracker $contactTracker,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function getHitRepository(): VideoHitRepository
|
||||
{
|
||||
return $this->em->getRepository(VideoHit::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'page:pages';
|
||||
}
|
||||
|
||||
public function getNameGetter(): string
|
||||
{
|
||||
return 'getTitle';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $guid
|
||||
*
|
||||
* @return VideoHit
|
||||
*/
|
||||
public function getHitForLeadByGuid(Lead $lead, $guid)
|
||||
{
|
||||
return $this->getHitRepository()->getHitForLeadByGuid($lead, $guid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param string $code
|
||||
*
|
||||
* @throws \Doctrine\ORM\ORMException
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function hitVideo($request, $code = '200'): void
|
||||
{
|
||||
// don't skew results with in-house hits
|
||||
if (!$this->security->isAnonymous()) {
|
||||
// return;
|
||||
}
|
||||
|
||||
$lead = $this->contactTracker->getContact();
|
||||
$guid = $request->get('guid');
|
||||
|
||||
$hit = ($lead) ? $this->getHitForLeadByGuid($lead, $guid) : new VideoHit();
|
||||
|
||||
$hit->setGuid($guid);
|
||||
$hit->setDateHit(new \DateTime());
|
||||
|
||||
$hit->setDuration($request->get('duration'));
|
||||
$hit->setUrl($request->get('url'));
|
||||
$hit->setTimeWatched($request->get('total_watched'));
|
||||
|
||||
// check for existing IP
|
||||
$ipAddress = $this->ipLookupHelper->getIpAddress();
|
||||
$hit->setIpAddress($ipAddress);
|
||||
|
||||
// Store query array
|
||||
$query = $request->query->all();
|
||||
unset($query['d']);
|
||||
$hit->setQuery($query);
|
||||
|
||||
if ($lead) {
|
||||
$hit->setLead($lead);
|
||||
}
|
||||
|
||||
// glean info from the IP address
|
||||
if ($details = $ipAddress->getIpDetails()) {
|
||||
$hit->setCountry($details['country']);
|
||||
$hit->setRegion($details['region']);
|
||||
$hit->setCity($details['city']);
|
||||
$hit->setIsp($details['isp']);
|
||||
$hit->setOrganization($details['organization']);
|
||||
}
|
||||
|
||||
$hit->setCode($code);
|
||||
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'));
|
||||
|
||||
// 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::VIDEO_ON_HIT)) {
|
||||
$event = new VideoHitEvent($hit, $request, $code);
|
||||
$this->dispatcher->dispatch($event, PageEvents::VIDEO_ON_HIT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\PageBundle;
|
||||
|
||||
/**
|
||||
* Events available for PageBundle.
|
||||
*/
|
||||
final class PageEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.video_on_hit event is thrown when a public page is browsed and a hit recorded in the analytics table.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\VideoHitEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const VIDEO_ON_HIT = 'mautic.video_on_hit';
|
||||
|
||||
/**
|
||||
* The mautic.page_on_hit event is thrown when a public page is browsed and a hit recorded in the analytics table.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\PageHitEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAGE_ON_HIT = 'mautic.page_on_hit';
|
||||
|
||||
/**
|
||||
* The mautic.page_on_build event is thrown before displaying the page builder form to allow adding of tokens.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\PageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAGE_ON_BUILD = 'mautic.page_on_build';
|
||||
|
||||
/**
|
||||
* The mautic.page_on_display event is thrown before displaying the page content.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\PageDisplayEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAGE_ON_DISPLAY = 'mautic.page_on_display';
|
||||
|
||||
/**
|
||||
* The mautic.page_pre_save event is thrown right before a page is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\PageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAGE_PRE_SAVE = 'mautic.page_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.page_post_save event is thrown right after a page is persisted.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\PageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAGE_POST_SAVE = 'mautic.page_post_save';
|
||||
|
||||
/**
|
||||
* The mautic.page_pre_delete event is thrown prior to when a page is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\PageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAGE_PRE_DELETE = 'mautic.page_pre_delete';
|
||||
|
||||
/**
|
||||
* The mautic.page_post_delete event is thrown after a page is deleted.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\PageEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const PAGE_POST_DELETE = 'mautic.page_post_delete';
|
||||
|
||||
/**
|
||||
* The mautic.redirect_do_not_track event is thrown when converting email links to trackables/redirectables in order to compile of list of tokens/URLs
|
||||
* to ignore.
|
||||
*
|
||||
* The event listener receives a Mautic\PageBundle\Event\UntrackableUrlsEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const REDIRECT_DO_NOT_TRACK = 'mautic.redirect_do_not_track';
|
||||
|
||||
/**
|
||||
* The mautic.page.on_campaign_trigger_decision event is fired when the campaign decision triggers.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_TRIGGER_DECISION = 'mautic.page.on_campaign_trigger_decision';
|
||||
|
||||
/**
|
||||
* The mautic.page.on_campaign_trigger_action event is fired when the campaign action fired.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CampaignBundle\Event\CampaignExecutionEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_CAMPAIGN_TRIGGER_ACTION = 'mautic.page.on_campaign_trigger_action';
|
||||
|
||||
/**
|
||||
* The mautic.page.on_redirect_generate event is fired when generating a redirect.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\PageBundle\Event\RedirectGenerationEvent
|
||||
*/
|
||||
public const ON_REDIRECT_GENERATE = 'mautic.page.on_redirect_generate';
|
||||
|
||||
/**
|
||||
* The mautic.page.on_bounce_rate_winner event is fired when there is a need to determine bounce rate winner.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CoreBundle\Event\DetermineWinnerEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_DETERMINE_BOUNCE_RATE_WINNER = 'mautic.page.on_bounce_rate_winner';
|
||||
|
||||
/**
|
||||
* The mautic.page.on_dwell_time_winner event is fired when there is a need to determine a winner based on dwell time.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\CoreBundles\Event\DetermineWinnerEvent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const ON_DETERMINE_DWELL_TIME_WINNER = 'mautic.page.on_dwell_time_winner';
|
||||
|
||||
/**
|
||||
* The mautic.page.on_contact_tracked event is dispatched when a contact is tracked via the mt() tracking event.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\PageBundle\Event\TrackingEvent
|
||||
*/
|
||||
public const ON_CONTACT_TRACKED = 'mautic.page.on_contact_tracked';
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% for f in form %}
|
||||
{{ form_row(f) }}
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,17 @@
|
||||
{% block _config_pageconfig_widget %}
|
||||
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.config.tab.pageconfig'|trans }}</h4>
|
||||
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.pageconfig.description'|trans }}</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-default mb-md">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
{% for name, f in form.children %}
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(f) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,80 @@
|
||||
{% block _config_trackingconfig_widget %}
|
||||
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.config.tab.pagetracking'|trans }}</h4>
|
||||
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.pagetracking.description'|trans }}</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-default mb-md">
|
||||
<div class="panel-body">
|
||||
<div class="form-group">
|
||||
<p>{{ 'mautic.config.tab.pagetracking.info'|trans|purify }}</p>
|
||||
{% include '@MauticCore/Components/code-snippet.html.twig' with {
|
||||
variant: 'multi',
|
||||
innerText: '<script>
|
||||
(function(w,d,t,u,n,a,m){w[\'MauticTrackingObject\']=n;
|
||||
w[n]=w[n]||function(){(w[n].q=w[n].q||[]).push(arguments)},a=d.createElement(t),
|
||||
m=d.getElementsByTagName(t)[0];a.async=1;a.src=u;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,\'script\',\'' ~ url('mautic_js') ~ '\',\'mt\');
|
||||
mt(\'send\', \'pageview\');
|
||||
</script>',
|
||||
} %}
|
||||
</div>
|
||||
<div class="row">
|
||||
<hr>
|
||||
{% for name, f in form.children %}
|
||||
{% if name in ['anonymize_ip', 'track_contact_by_ip', 'do_not_track_404_anonymous'] %}
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(f) }}
|
||||
{% if name == 'anonymize_ip' %}
|
||||
<div class="anonymize_ip_address hide text-danger">{{ 'mautic.page.config.form.anonymize_ip.warning'|trans }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.config.tab.tracking.facebook.pixel'|trans }}</h4>
|
||||
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.tracking.facebook.pixel.description'|trans }}</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-default mb-md">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.facebook_pixel_id) }}
|
||||
</div>
|
||||
<hr>
|
||||
{% for name, f in form.children %}
|
||||
{% if name in ['facebook_pixel_trackingpage_enabled', 'facebook_pixel_landingpage_enabled'] %}
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(f) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.config.tab.tracking.google.analytics'|trans }}</h4>
|
||||
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.tracking.google.analytics.description'|trans }}</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-default mb-md">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.google_analytics_id) }}
|
||||
</div>
|
||||
<hr>
|
||||
{% for name, f in form.children %}
|
||||
{% if name in ['google_analytics_trackingpage_enabled', 'google_analytics_landingpage_enabled', 'google_analytics_anonymize_ip'] %}
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(f) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% block _page_abtest_settings_properties_row %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,3 @@
|
||||
{% block _page_variantSettings_properties_row %}
|
||||
{{ form_widget(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,95 @@
|
||||
{% block pointaction_urlhit_widget %}
|
||||
{%- set timeFrames = {
|
||||
's': 'mautic.core.time.seconds'|trans,
|
||||
'i': 'mautic.core.time.minutes'|trans,
|
||||
'H': 'mautic.core.time.hours'|trans,
|
||||
'd': 'mautic.core.time.days'|trans,
|
||||
} %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form['page_url']) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form['page_hits']) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12 form-group ">
|
||||
{{ form_label(form['returns_within']) }}
|
||||
<div class="input-group">
|
||||
{{ form_widget(form['returns_within']) }}
|
||||
{%- set default = form.returns_within_unit.vars.data %}
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-ghost dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="returns_within_label">{{ timeFrames[default] }}</span> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu time-dropdown">
|
||||
{%- for abbr, label in timeFrames %}
|
||||
<li><a href="#" data-time="{{ abbr }}" data-field="returns_within">{{ label }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_errors(form['returns_within']) }}
|
||||
{{ form_widget(form['returns_within_unit']) }}
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 form-group ">
|
||||
{{ form_label(form['returns_after']) }}
|
||||
<div class="input-group">
|
||||
{{ form_widget(form['returns_after']) }}
|
||||
{%- set default = form['returns_after_unit'].vars['data'] %}
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-ghost dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="returns_after_label">{{ timeFrames[default] }}</span> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu time-dropdown">
|
||||
{%- for abbr, label in timeFrames %}
|
||||
<li><a href="#" data-time="{{ abbr }}" data-field="returns_after">{{ label }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_errors(form['returns_after']) }}
|
||||
{{ form_widget(form['returns_after_unit']) }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-xs-12 form-group ">
|
||||
{{ form_label(form['accumulative_time']) }}
|
||||
<div class="input-group">
|
||||
{{ form_widget(form['accumulative_time']) }}
|
||||
{%- set default = form['accumulative_time_unit'].vars['data'] %}
|
||||
<div class="input-group-btn">
|
||||
<button type="button" class="btn btn-ghost dropdown-toggle" data-toggle="dropdown">
|
||||
<span class="accumulative_time_label">{{ timeFrames[default] }}</span> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu time-dropdown">
|
||||
{%- for abbr, label in timeFrames %}
|
||||
<li><a href="#" data-time="{{ abbr }}" data-field="accumulative_time">{{ label }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_errors(form['accumulative_time']) }}
|
||||
{{ form_widget(form['accumulative_time_unit']) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
mQuery('.time-dropdown li a').click(function (e) {
|
||||
e.preventDefault();
|
||||
var selected = mQuery(this).data('time');
|
||||
var label = mQuery(this).html();
|
||||
var field = mQuery(this).data('field');
|
||||
|
||||
mQuery('#point_properties_' + field + '_unit').val(selected);
|
||||
mQuery('.' + field + '_label').html(label);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,230 @@
|
||||
{#
|
||||
Variables
|
||||
- searchValue
|
||||
- items
|
||||
- categories
|
||||
- page
|
||||
- limit
|
||||
- permissions
|
||||
- model
|
||||
- tmpl
|
||||
- security
|
||||
#}
|
||||
{% if items|length > 0 %}
|
||||
<div class="table-responsive page-list">
|
||||
<table class="table table-hover pagetable-list" id="pageTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'checkall': 'true',
|
||||
'target': '#pageTable',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'p.title',
|
||||
'text': 'mautic.core.title',
|
||||
'class': 'col-page-title',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'c.title',
|
||||
'text': 'mautic.core.category',
|
||||
'class': 'visible-md visible-lg col-page-category',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'p.template',
|
||||
'text': 'mautic.core.form.theme',
|
||||
'class': 'visible-md visible-lg col-page-template',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'p.hits',
|
||||
'text': 'mautic.page.thead.hits',
|
||||
'class': 'col-page-hits visible-md visible-lg',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'p.dateAdded',
|
||||
'text': 'mautic.lead.import.label.dateAdded',
|
||||
'class': 'col-page-dateAdded visible-md visible-lg',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'p.dateModified',
|
||||
'text': 'mautic.lead.import.label.dateModified',
|
||||
'class': 'col-page-dateModified visible-md visible-lg',
|
||||
'default': true,
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'p.createdByUser',
|
||||
'text': 'mautic.core.createdby',
|
||||
'class': 'col-page-createdByUser visible-md visible-lg',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'submission_count',
|
||||
'text': 'mautic.form.form.results',
|
||||
'class': 'visible-md visible-lg col-page-submissions',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'page',
|
||||
'orderBy': 'p.id',
|
||||
'text': 'mautic.core.id',
|
||||
'class': 'col-page-id visible-md visible-lg',
|
||||
}) }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for i in items %}
|
||||
{% set item = i[0] %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ include('@MauticCore/Helper/list_actions.html.twig', {
|
||||
'item': item,
|
||||
'templateButtons': {
|
||||
'edit': securityHasEntityAccess(permissions['page:pages:editown'], permissions['page:pages:editother'], item.createdBy),
|
||||
'clone': permissions['page:pages:create'],
|
||||
'delete': securityHasEntityAccess(permissions['page:pages:deleteown'], permissions['page:pages:deleteother'], item.createdBy),
|
||||
},
|
||||
'routeBase': 'page',
|
||||
'nameGetter': 'getTitle',
|
||||
'customButtons': {
|
||||
'preview': {
|
||||
'attr': {
|
||||
'class': 'btn btn-ghost btn-sm btn-nospin',
|
||||
'href': path('mautic_page_preview', {'id': item.id}),
|
||||
'target': '_blank',
|
||||
'data-toggle': '',
|
||||
},
|
||||
'iconClass': 'ri-external-link-line',
|
||||
'btnText': 'mautic.core.open_link'|trans,
|
||||
'priority': 100
|
||||
},
|
||||
'results': {
|
||||
'attr': {
|
||||
'class': 'btn btn-ghost btn-sm btn-nospin',
|
||||
'href': path('mautic_page_results', {'objectId': item.id}),
|
||||
'data-toggle': 'ajax',
|
||||
'data-menu-link': 'mautic_form_index'
|
||||
},
|
||||
'iconClass': 'ri-bar-chart-line',
|
||||
'btnText': 'mautic.form.form.results'|trans,
|
||||
'priority': 80
|
||||
}
|
||||
}|merge(not item.isPreferenceCenter ? {
|
||||
'copy': {
|
||||
'attr': {
|
||||
'data-copy': url('mautic_page_public', {'slug': item.alias}),
|
||||
'data-toggle': 'none',
|
||||
},
|
||||
'btnText': 'mautic.core.copy_page_link'|trans,
|
||||
'iconClass': 'ri-clipboard-line',
|
||||
'priority': 90
|
||||
}
|
||||
} : {})
|
||||
}) }}
|
||||
</td>
|
||||
<td>
|
||||
{{ include('@MauticCore/Helper/publishstatus_icon.html.twig', {'item': item, 'model': 'page.page'}) }}
|
||||
<a href="{{ path('mautic_page_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
{{ item.title }} ({{ item.alias }})
|
||||
{% if item.isVariant or item.isTranslation or item.isPreferenceCenter or pageConfig.isDraftEnabled %}
|
||||
<span>
|
||||
{% if item.isVariant %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.core.icon_tooltip.ab_test'|trans }}">
|
||||
<i class="ri-fw ri-organization-chart"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if item.isTranslation %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.core.icon_tooltip.translation'|trans }}">
|
||||
<i class="ri-fw ri-translate"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if item.isPreferenceCenter %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.core.icon_tooltip.preference_center'|trans }}">
|
||||
<i class="ri-settings-5-line"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if pageConfig.isDraftEnabled and item.hasDraft %}
|
||||
<span data-toggle="tooltip" title="{{ 'mautic.email.icon_tooltip.has_draft'|trans }}">
|
||||
<i class="fa fa-fw fa-file"></i>
|
||||
</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{{ customContent('page.name', _context) }}
|
||||
{{ include('@MauticProject/Modules/projects.html.twig') }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ include('@MauticCore/Modules/category--expanded.html.twig', {'category': item.category}) }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{% if item.template %}
|
||||
{{ getThemeName(item.template) }}
|
||||
{% else %}
|
||||
<span class="text-muted">{{ 'mautic.core.form.default'|trans }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.hits }}</td>
|
||||
<td class="visible-md visible-lg" title="{% if item.dateAdded %}{{ dateToFullConcat(item.dateAdded) }}{% endif %}">
|
||||
{% if item.dateAdded %}{{ dateToDate(item.dateAdded) }}{% endif %}
|
||||
</td>
|
||||
<td class="visible-md visible-lg" title="{% if item.dateModified %}{{ dateToFullConcat(item.dateModified) }}{% endif %}">
|
||||
{% if item.dateModified %}{{ dateToDate(item.dateModified) }}{% endif %}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.createdByUser }}</td>
|
||||
<td class="visible-md visible-lg">
|
||||
<a href="{{ path('mautic_page_results', {'objectId': item.id}) }}" data-toggle="ajax" data-menu-link="mautic_form_index" size="sm" class="label label-gray" {% if 0 == i.submission_count %}disabled="disabled"{% endif %}>
|
||||
{{- 'mautic.form.form.viewresults'|trans({'%count%': i.submission_count}) -}}
|
||||
</a>
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.id }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
{{ include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems': items|length,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'menuLinkId': 'mautic_page_index',
|
||||
'baseUrl': path('mautic_page_index'),
|
||||
'sessionVar': 'page',
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if searchValue is not empty %}
|
||||
{{ include('@MauticCore/Helper/noresults.html.twig') }}
|
||||
{% else %}
|
||||
<div class="mt-80 col-md-offset-2 col-lg-offset-3 col-md-8 col-lg-5 height-auto">
|
||||
{% set childContainer %}
|
||||
<div class="mb-md">
|
||||
{% include '@MauticCore/Components/pictogram.html.twig' with {
|
||||
'pictogram': 'landing-page',
|
||||
'size': '80'
|
||||
} %}
|
||||
</div>
|
||||
{% endset %}
|
||||
|
||||
{{ include('@MauticCore/Components/content-block.html.twig', {
|
||||
heading: 'mautic.page.contentblock.heading',
|
||||
subheading: 'mautic.page.contentblock.subheading',
|
||||
childContainer: childContainer,
|
||||
}) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,367 @@
|
||||
{#
|
||||
Variables
|
||||
- activePage (\Mautic\PageBundle\Entity\Page)
|
||||
- variants
|
||||
- translations
|
||||
- permissions
|
||||
- stats
|
||||
- abTestResults
|
||||
- security
|
||||
- pageUrl
|
||||
- previewUrl
|
||||
- logs
|
||||
- dateRangeForm
|
||||
|
||||
@todo - add landing page stats/analytics
|
||||
#}
|
||||
{# Only show A/B test button if not already a translation of an a/b test #}
|
||||
{% set allowAbTest = (activePage.isPreferenceCenter or (activePage.isTranslation(true) and translations.parent.isVariant)) ? false : true %}
|
||||
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauticContent %}page{% endblock %}
|
||||
|
||||
{% block preHeader %}
|
||||
{{ include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'item': activePage,
|
||||
'customButtons': customButtons|default([]),
|
||||
'templateButtons': {
|
||||
'close': securityHasEntityAccess( permissions['page:pages:viewown'], permissions['page:pages:viewother'], activePage.createdBy),
|
||||
},
|
||||
'routeBase': 'page',
|
||||
'targetLabel': 'mautic.page.pages'|trans
|
||||
}) }}
|
||||
{{ include('@MauticCore/Modules/category--inline.html.twig', {'category': activePage.category}) }}
|
||||
{{ include('@MauticProject/Modules/projects.html.twig', {'item': activePage}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block headerTitle %}{{ activePage.title }}{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{{ include('@MauticCore/Helper/page_actions.html.twig', {
|
||||
'item': activePage,
|
||||
'customButtons': customButtons|default([]),
|
||||
'templateButtons': {
|
||||
'edit': securityHasEntityAccess(permissions['page:pages:editown'], permissions['page:pages:editother'], activePage.createdBy),
|
||||
'abtest': allowAbTest and permissions['page:pages:create'],
|
||||
'clone': permissions['page:pages:create'],
|
||||
'delete': securityHasEntityAccess(permissions['page:pages:deleteown'], permissions['page:pages:deleteown'], activePage.createdBy),
|
||||
},
|
||||
'routeBase': 'page',
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block publishStatus %}
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': activePage,
|
||||
'status': 'available'
|
||||
}) -}}
|
||||
<div class="label__divider"></div>
|
||||
|
||||
{% set blueTags = [] %}
|
||||
{% set grayTags = [] %}
|
||||
|
||||
{% if activePage.isPreferenceCenter %}
|
||||
{% set blueTags = blueTags|merge([{ type: 'read-only', color: 'blue', icon_only: true, label: 'mautic.email.form.preference_center', icon: 'ri-equalizer-2-fill' }]) %}
|
||||
{% endif %}
|
||||
|
||||
{% if activePage.isTranslation and not activePage.isTranslation(true) %}
|
||||
{% set blueTags = blueTags|merge([{ color: 'blue', label: 'mautic.core.icon_tooltip.translation', icon: 'ri-translate', icon_only: true }]) %}
|
||||
{% endif %}
|
||||
|
||||
{% if activePage.noIndex is defined and activePage.noIndex == 1 %}
|
||||
{% set blueTags = blueTags|merge([{ label: 'mautic.core.tag.search_index.disabled'|trans, icon: 'ri-eye-off-fill', color: 'blue', icon_only: true }]) %}
|
||||
{% endif %}
|
||||
|
||||
{% if activePage.isVariant and not activePage.isVariant(true) %}
|
||||
{% set grayTags = grayTags|merge([{ type: 'read-only', color: 'warm-gray', label: 'mautic.email.icon_tooltip.abtest' }]) %}
|
||||
{% endif %}
|
||||
|
||||
{% if activePage.isVariant(true) %}
|
||||
{% set grayTags = grayTags|merge([{ color: 'warm-gray', label: 'mautic.core.variant_of'|trans({'%parent%' : variants.parent.getName()}), icon: 'ri-organization-chart' }]) %}
|
||||
{% endif %}
|
||||
|
||||
{% if activePage.isTranslation(true) %}
|
||||
{% set grayTags = grayTags|merge([{
|
||||
color: 'warm-gray',
|
||||
label: 'mautic.core.translation_of'|trans({'%parent%' : translations.parent.getName()}),
|
||||
icon: 'ri-translate',
|
||||
attributes: {
|
||||
'href': path('mautic_page_action', {'objectAction': 'view', 'objectId': translations.parent.id})
|
||||
}
|
||||
}]) %}
|
||||
{% endif %}
|
||||
|
||||
{% if activePage.language is defined and activePage.language is not empty %}
|
||||
{% set grayTags = grayTags|merge([{ label: activePage.language|language_name, icon: 'ri-translate-2', color: 'warm-gray', attributes: { 'data-toggle': 'tooltip', 'data-placement': 'top', 'title': 'mautic.core.language'|trans } }]) %}
|
||||
{% endif %}
|
||||
|
||||
{% include '@MauticCore/Helper/_tag.html.twig' with { tags: blueTags|merge(grayTags) } %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% set variantContent = include('@MauticCore/Variant/index.html.twig', {
|
||||
'activeEntity': activePage,
|
||||
'variants': variants,
|
||||
'abTestResults': abTestResults,
|
||||
'model': 'page',
|
||||
'actionRoute': 'mautic_page_action',
|
||||
'nameGetter': 'getTitle',
|
||||
})|trim %}
|
||||
{% set showVariants = variantContent is not empty %}
|
||||
|
||||
{% set translationContent = include('@MauticCore/Translation/index.html.twig', {
|
||||
'activeEntity': activePage,
|
||||
'translations': translations,
|
||||
'model': 'page',
|
||||
'actionRoute': 'mautic_page_action',
|
||||
'nameGetter': 'getTitle',
|
||||
})|trim %}
|
||||
{% set showTranslations = translationContent is not empty %}
|
||||
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- left section -->
|
||||
<div class="col-md-9 height-auto">
|
||||
<div>
|
||||
<!-- page detail header -->
|
||||
{% include '@MauticCore/Helper/description--expanded.html.twig' with {
|
||||
'description': activePage.metaDescription
|
||||
} %}
|
||||
<!--/ page detail header -->
|
||||
|
||||
<!-- page detail collapseable -->
|
||||
<div class="collapse pr-md pl-md" id="page-details">
|
||||
<div class="pr-md pl-md pb-md">
|
||||
<div class="panel shd-none mb-0">
|
||||
<table class="table table-hover mb-0">
|
||||
<tbody>
|
||||
{{ include('@MauticCore/Helper/details.html.twig', {'entity': activePage}, with_context=false) }}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ page detail collapseable -->
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- page detail collapseable toggler -->
|
||||
<div class="hr-expand nm">
|
||||
<span data-toggle="tooltip" title="Detail">
|
||||
<a href="javascript:void(0)" class="arrow text-secondary collapsed" data-toggle="collapse" data-target="#page-details">
|
||||
<span class="caret"></span> {{ 'mautic.core.details'|trans }}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<!--/ page detail collapseable toggler -->
|
||||
|
||||
<!-- some stats -->
|
||||
<div class="pa-md">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="panel">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-md-3 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
<span class="ri-line-chart-fill"></span>
|
||||
{{ 'mautic.page.pageviews'|trans }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-md-9 va-m">
|
||||
{{ include('@MauticCore/Helper/graph_dateselect.html.twig', {'dateRangeForm': dateRangeForm, 'class': 'pull-right'}) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex fd-column pt-0 pl-15 pb-15 pr-15 min-h-256">
|
||||
{{ include('@MauticCore/Helper/chart.html.twig', {'chartData': stats.pageviews, 'chartType': 'line', 'chartHeight': 300}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ stats -->
|
||||
|
||||
{{ customContent('details.stats.graph.below', _context) }}
|
||||
|
||||
<!-- tabs controls -->
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
{% if showVariants %}
|
||||
<li class="active">
|
||||
<a href="#variants-container" role="tab" data-toggle="tab">
|
||||
{{- 'mautic.core.variants'|trans -}}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if showTranslations %}
|
||||
<li class="{% if not showVariants %}active{% endif %}">
|
||||
<a href="#translation-container" role="tab" data-toggle="tab">
|
||||
{{- 'mautic.core.translations'|trans -}}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<!--/ tabs controls -->
|
||||
</div>
|
||||
|
||||
{% if showVariants or showTranslations %}
|
||||
<!-- start: tab-content -->
|
||||
<div class="tab-content pa-md">
|
||||
{% if showVariants %}
|
||||
<!-- #variants-container -->
|
||||
<div class="tab-pane active bdr-w-0" id="variants-container">
|
||||
{{ variantContent|raw }}
|
||||
</div>
|
||||
<!--/ #variants-container -->
|
||||
{% endif %}
|
||||
<!-- #translation-container -->
|
||||
{% if showTranslations %}
|
||||
<div class="tab-pane {% if not showVariants %}active{% endif %} bdr-w-0" id="translation-container">
|
||||
{{ translationContent|raw }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<!--/ #translation-container -->
|
||||
</div>
|
||||
<!--/ end: tab-content -->
|
||||
{% endif %}
|
||||
|
||||
{% set additionalCards = [{
|
||||
heading: 'mautic.page.results.view',
|
||||
copy: (not showVariants and not showTranslations) ? 'mautic.page.results.view.description' : null,
|
||||
pictogram: 'data--set',
|
||||
href: path('mautic_page_results', {'objectId': activePage.id}),
|
||||
attributes: {
|
||||
'data-toggle': 'ajax',
|
||||
'data-menu-link': 'mautic_form_index'
|
||||
}
|
||||
}] %}
|
||||
|
||||
{{ include('@MauticCore/Modules/suggested-actions.html.twig', {
|
||||
'entity': activePage,
|
||||
'routeBase': 'page',
|
||||
'showVariants': showVariants,
|
||||
'showTranslations': showTranslations,
|
||||
'allowAbTest': allowAbTest,
|
||||
'additionalCards': additionalCards
|
||||
}) }}
|
||||
</div>
|
||||
<!--/ left section -->
|
||||
|
||||
<!-- right section -->
|
||||
<div class="col-md-3 bdr-l height-auto">
|
||||
<!-- preview URL -->
|
||||
{% if not activePage.isPreferenceCenter %}
|
||||
<div class="panel shd-none bdr-rds-0 bdr-w-0 mt-sm mb-0">
|
||||
<div class="panel-body pt-xs">
|
||||
{% include '@MauticCore/Components/card.html.twig' with {
|
||||
type: 'link',
|
||||
href: pageUrl,
|
||||
ctaType: 'new tab',
|
||||
heading: 'mautic.core.open_link',
|
||||
attributes: {
|
||||
'target': '_blank'
|
||||
}
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="panel shd-none bdr-rds-0 bdr-w-0 mt-sm mb-0">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{{ 'mautic.page.preview.url'|trans }}</div>
|
||||
</div>
|
||||
<div class="panel-body pt-xs">
|
||||
{% if previewSettingsForm.translation is defined %}
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-12 ">
|
||||
<div class="control-label">{{ 'mautic.email.preview.show.translation'|trans }}</div>
|
||||
{{ form_widget(previewSettingsForm.translation) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if previewSettingsForm.variant is defined %}
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-12 ">
|
||||
<div class="control-label">{{ 'mautic.email.preview.show.ab.variant'|trans }}</div>
|
||||
{{ form_widget(previewSettingsForm.variant) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if previewSettingsForm.contact is defined %}
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-12 ">
|
||||
<div class="control-label">{{ 'mautic.page.preview.show.contact'|trans }}</div>
|
||||
{{ form_widget(previewSettingsForm.contact) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-12 ">
|
||||
<div class="input-group">
|
||||
<div class="input-group-addon">
|
||||
{{- include('@MauticCore/Helper/publishstatus_icon.html.twig', {
|
||||
'item' : activePage,
|
||||
'model' : 'page',
|
||||
'query' : 'customToggle=publicPreview'
|
||||
}) -}}
|
||||
</div>
|
||||
<input id="content_preview_url"
|
||||
data-route="page/preview"
|
||||
onclick="this.setSelectionRange(0, this.value.length);"
|
||||
type="text"
|
||||
class="form-control"
|
||||
readonly
|
||||
value="{{ previewUrl|e }}"/>
|
||||
<span class="input-group-btn">
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: 'mautic.core.open_link',
|
||||
variant: 'ghost',
|
||||
icon_only: true,
|
||||
icon: 'ri-external-link-line',
|
||||
onclick: 'window.open("' ~ previewUrl ~ '", "_blank");',
|
||||
attributes: {
|
||||
'id': 'content_preview_url_button',
|
||||
'class': 'btn-nospin'
|
||||
}
|
||||
}
|
||||
]
|
||||
} %}
|
||||
</span>
|
||||
<input type="hidden" id="content_preview_settings_object_id" value="{{ activePage.id }}">
|
||||
<input type="hidden" id="content_preview_settings_contact_id" value="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if draftPreviewUrl is not empty %}
|
||||
<div class="panel bg-transparent shd-none bdr-rds-0 bdr-w-0 mt-sm mb-0">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{{ 'mautic.email.draft.preview.url'|trans }}</div>
|
||||
</div>
|
||||
<div class="panel-body pt-xs">
|
||||
<div class="input-group">
|
||||
<input onclick="this.setSelectionRange(0, this.value.length);" type="text" class="form-control"
|
||||
readonly
|
||||
value="{{ draftPreviewUrl|e }}"/>
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-default btn-nospin"
|
||||
onclick="window.open('{{ draftPreviewUrl }}', '_blank');">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<!--/ preview URL -->
|
||||
<hr class="hr-w-2" style="width:50%">
|
||||
<!-- recent activity -->
|
||||
{{ include('@MauticCore/Helper/recentactivity.html.twig', {'logs': logs}, with_context=false) }}
|
||||
</div>
|
||||
<!--/ right section -->
|
||||
</div>
|
||||
|
||||
<!--/ end: box layout -->
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,152 @@
|
||||
{#
|
||||
Variables
|
||||
- form
|
||||
- isVariant
|
||||
- tokens
|
||||
- activePage
|
||||
- themes
|
||||
- permissions
|
||||
- previewUrl (optional)
|
||||
- security (optional)
|
||||
- Defined when editing page
|
||||
#}
|
||||
{% form_theme form with [
|
||||
'@MauticPage/FormTheme/Page/_page_abtest_settings_properties_row.html.twig',
|
||||
'@MauticPage/FormTheme/Page/_page_variantSettings_properties_row.html.twig',
|
||||
] %}
|
||||
{% set isExisting = activePage.id %}
|
||||
{% set variantParent = activePage.variantParent %}
|
||||
{% set previewUrl = previewUrl|default('') %}
|
||||
{% set draftPreviewUrl = draftPreviewUrl|default('') %}
|
||||
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauticContent %}page{% endblock %}
|
||||
|
||||
{% block headerTitle %}
|
||||
{%- if isExisting -%}
|
||||
{{ 'mautic.page.header.edit'|trans({'%name%': activePage.title}) }}
|
||||
{%- else -%}
|
||||
{{ 'mautic.page.header.new'|trans }}
|
||||
{%- endif -%}
|
||||
{% if variantParent %}
|
||||
<div><span class="small">{{ 'mautic.core.variant_of'|trans({'%name%': activePage.title, '%parent%': variantParent.title}) }}</span></div>
|
||||
{% elseif activePage.isVariant(false) %}
|
||||
<div><span class="small">{{ 'mautic.page.form.has_variants'|trans }}</span></div>
|
||||
{%- endif -%}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="grapesjsbuilder_assets" class="hide"></div>
|
||||
|
||||
{% set template, attr = form.template.vars.data, form.vars.attr %}
|
||||
{% set attr = attr|merge({
|
||||
'data-submit-callback-async': 'clearThemeHtmlBeforeSave',
|
||||
}) %}
|
||||
|
||||
{{ form_start(form, {'attr': attr}) }}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- container -->
|
||||
<div class="col-md-9 height-auto">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<!-- tabs controls -->
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
<li class="active">
|
||||
<a href="#theme-container" role="tab" data-toggle="tab">{{ 'mautic.core.form.theme'|trans }}</a>
|
||||
</li>
|
||||
<li id="advanced-tab" class="hidden">
|
||||
<a href="#advanced-container" role="tab" data-toggle="tab">{{ 'mautic.core.advanced'|trans }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!--/ tabs controls -->
|
||||
|
||||
<div class="tab-content pa-md">
|
||||
<div class="tab-pane fade in active bdr-w-0" id="theme-container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{{ form_row(form.template) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ include('@MauticCore/Helper/theme_select.html.twig', {
|
||||
'type': 'page',
|
||||
'themes': themes,
|
||||
'active': form.template.vars.value,
|
||||
}, with_context=false) }}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade bdr-w-0" id="advanced-container">
|
||||
<br>
|
||||
<div class="row hidden" id="custom-html-row">
|
||||
<div class="col-md-12">
|
||||
{{ form_label(form.customHtml) }}
|
||||
{{ form_widget(form.customHtml) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 height-auto bdr-l">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
{{ form_row(form.title) }}
|
||||
{% if isVariant %}
|
||||
|
||||
{{ form_row(form.variantSettings) }}
|
||||
{% else %}
|
||||
{{ form_row(form.alias) }}
|
||||
{{ form_row(form.category) }}
|
||||
{{ form_row(form.projects) }}
|
||||
{{ form_row(form.language) }}
|
||||
{{ form_row(form.translationParent) }}
|
||||
{% endif %}
|
||||
|
||||
{{ form_row(form.isPublished, {
|
||||
'attr': {
|
||||
'data-none': 'mautic.core.form.unavailable_regardless_of_scheduling',
|
||||
'data-start': 'mautic.core.form.available_on_scheduled_date',
|
||||
'data-both': 'mautic.core.form.available_during_scheduled_period',
|
||||
'data-end': 'mautic.core.form.available_until_scheduled_end'
|
||||
}
|
||||
}) }}
|
||||
|
||||
{% if (permissions['page:preference_center:editown'] or permissions['page:preference_center:editother']) and not activePage.isVariant %}
|
||||
{{ form_row(form.isPreferenceCenter) }}
|
||||
{% endif %}
|
||||
|
||||
{{ form_row(form.publishUp, {'label': 'mautic.core.form.available.available_from'}) }}
|
||||
{{ form_row(form.publishDown, {'label': 'mautic.core.form.available.unavailable_from'}) }}
|
||||
|
||||
{% if not isVariant %}
|
||||
{{ form_row(form.redirectType) }}
|
||||
{{ form_row(form.redirectUrl) }}
|
||||
{% endif %}
|
||||
|
||||
{{ form_row(form.noIndex) }}
|
||||
<div class="template-fields {% if not template %}hide{% endif %}">
|
||||
{{ form_row(form.metaDescription) }}
|
||||
</div>
|
||||
<div class="template-fields {% if not template %}hide{% endif %}">
|
||||
{{ form_row(form.headScript) }}
|
||||
</div>
|
||||
<div class="template-fields {% if not template %}hide{% endif %}">
|
||||
{{ form_row(form.footerScript) }}
|
||||
</div>
|
||||
<div class="hide">
|
||||
{{ form_rest(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/builder.html.twig', {
|
||||
'type': 'page',
|
||||
'isCodeMode': ('mautic_code_mode' is same as activePage.template),
|
||||
'objectId': activePage.sessionId,
|
||||
'previewUrl': previewUrl,
|
||||
'draftPreviewUrl': draftPreviewUrl,
|
||||
}, with_context=false) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,98 @@
|
||||
{#
|
||||
Variables
|
||||
- searchValue
|
||||
- items
|
||||
- categories
|
||||
- page
|
||||
- limit
|
||||
- permissions
|
||||
- model
|
||||
- tmpl
|
||||
- security
|
||||
#}
|
||||
{% set isIndex = 'index' == tmpl ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
|
||||
{% block mauticContent %}page{% endblock %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.page.pages'|trans }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="{% if items|length > 0 or searchValue is not empty %}panel {% endif %}panel-default">
|
||||
{{ include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'searchHelp': 'mautic.page.help.searchcommands',
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['page:pages:create'],
|
||||
},
|
||||
'routeBase': 'page',
|
||||
},
|
||||
'bulk_actions': {
|
||||
'routeBase': 'page',
|
||||
'templateButtons': {
|
||||
'delete': permissions['page:pages:deleteown'] or permissions['page:pages:deleteother'],
|
||||
},
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ispublished',
|
||||
'label': 'mautic.core.form.active',
|
||||
'tooltip': 'mautic.core.searchcommand.ispublished.description',
|
||||
'icon': 'ri-check-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isunpublished',
|
||||
'label': 'mautic.core.form.inactive',
|
||||
'tooltip': 'mautic.core.searchcommand.isunpublished.description',
|
||||
'icon': 'ri-close-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isuncategorized',
|
||||
'label': 'mautic.core.form.uncategorized',
|
||||
'tooltip': 'mautic.core.searchcommand.isuncategorized.description',
|
||||
'icon': 'ri-folder-unknow-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ismine',
|
||||
'label': 'mautic.core.searchcommand.ismine.label',
|
||||
'tooltip': 'mautic.core.searchcommand.ismine.description',
|
||||
'icon': 'ri-user-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.page.searchcommand.isexpired',
|
||||
'label': 'mautic.core.form.expired',
|
||||
'tooltip': 'mautic.page.searchcommand.isexpired.description',
|
||||
'icon': 'ri-time-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.page.searchcommand.ispending',
|
||||
'label': 'mautic.core.form.pending',
|
||||
'tooltip': 'mautic.page.searchcommand.ispending.description',
|
||||
'icon': 'ri-timer-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.page.searchcommand.isprefcenter',
|
||||
'label': 'mautic.page.searchcommand.isprefcenter.label',
|
||||
'tooltip': 'mautic.page.searchcommand.isprefcenter.description',
|
||||
'icon': 'ri-settings-4-line'
|
||||
}
|
||||
]
|
||||
}) }}
|
||||
<div class="page-list">
|
||||
{% endif %}
|
||||
|
||||
{{ include('@MauticPage/Page/_list.html.twig') }}
|
||||
|
||||
{% if isIndex %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ include('@MauticCore/Modules/protip.html.twig', {
|
||||
tip: random(['mautic.protip.pages.mobile', 'mautic.protip.pages.forms'])
|
||||
}) }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,95 @@
|
||||
{#
|
||||
Variables
|
||||
- activePage
|
||||
- items
|
||||
#}
|
||||
{% set pageId = activePage.id %}
|
||||
<div class="table-responsive table-responsive-force">
|
||||
<table class="table table-hover pageresult-list" id="pageResultsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'pageresult.' ~ pageId,
|
||||
'orderBy': 's.id',
|
||||
'text': 'mautic.form.report.submission.id',
|
||||
'class': 'col-pageresult-id',
|
||||
'filterBy': 's.id',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'pageresult.' ~ pageId,
|
||||
'orderBy': 's.lead_id',
|
||||
'text': 'mautic.lead.report.contact_id',
|
||||
'class': 'col-pageresult-lead-id',
|
||||
'filterBy': 's.lead_id',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'pageresult.' ~ pageId,
|
||||
'orderBy': 's.form_id',
|
||||
'text': 'mautic.form.report.form_id',
|
||||
'class': 'col-pageresult-form-id',
|
||||
'filterBy': 's.form_id',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'pageresult.' ~ pageId,
|
||||
'orderBy': 's.date_submitted',
|
||||
'text': 'mautic.form.result.thead.date',
|
||||
'class': 'col-pageresult-date',
|
||||
'default': true,
|
||||
'filterBy': 's.date_submitted',
|
||||
'dataToggle': 'date',
|
||||
}) }}
|
||||
|
||||
{{ include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar': 'pageresult.' ~ pageId,
|
||||
'orderBy': 'i.ip_address',
|
||||
'text': 'mautic.core.ipaddress',
|
||||
'class': 'col-pageresult-ip',
|
||||
'filterBy': 'i.ip_address',
|
||||
}) }}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if items|length > 0 %}
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>{{ item.id|e }}</td>
|
||||
<td>
|
||||
{% if item.leadId is defined %}
|
||||
<a href="{{ path('mautic_contact_action', {'objectAction': 'view', 'objectId': item.leadId}) }}" data-toggle="ajax">
|
||||
{{- item.leadId|e -}}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if item.formId is defined %}
|
||||
<a href="{{ path('mautic_form_action', {'objectAction': 'view', 'objectId': item.formId}) }}" data-toggle="ajax">
|
||||
{{- item.formId|e -}}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ dateToFull(item.dateSubmitted, 'UTC') }}</td>
|
||||
<td>{{ item.ipAddress|e }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
{{ include('@MauticCore/Helper/noresults.html.twig') }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
{{ include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems': totalCount,
|
||||
'page': page,
|
||||
'limit': limit,
|
||||
'baseUrl': path('mautic_page_results', {'objectId': activePage.id}),
|
||||
'sessionVar': 'pageresult.' ~ pageId,
|
||||
}) }}
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
{#
|
||||
Variables
|
||||
- pageTitle
|
||||
- results
|
||||
- page
|
||||
#}
|
||||
{% set contentOnly = true %}
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block pageTitle %}{{ pageTitle }}{% endblock %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.page.result.header.index'|trans({'%name%': page.getName()}) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="pageresults">
|
||||
<table class="table table-hover pageresult-list">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col-pageresult-id">{{ 'mautic.form.report.submission.id'|trans }}</th>
|
||||
<th class="col-pageresult-leadId">{{ 'mautic.lead.report.contact_id'|trans }}</th>
|
||||
<th class="col-pageresult-formId">{{ 'mautic.form.report.form_id'|trans }}</th>
|
||||
<th class="col-pageresult-date">{{ 'mautic.form.result.thead.date'|trans }}</th>
|
||||
<th class="col-pageresult-ip">{{ 'mautic.core.ipaddress'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in results %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td>{{ item.leadId }}</td>
|
||||
<td>{{ item.formId }}</td>
|
||||
<td>{{ dateToFull(item.dateSubmitted, 'UTC') }}</td>
|
||||
<td>{{ item.ipAddress }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,79 @@
|
||||
{#
|
||||
Variables
|
||||
- activePage
|
||||
- items
|
||||
#}
|
||||
{% set isIndex = 'index' == tmpl ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
|
||||
{% block mauticContent %}pageresult{% endblock %}
|
||||
|
||||
{% block headerTitle %}
|
||||
{{ 'mautic.page.result.header.index'|trans({
|
||||
'%name%': activePage.getName(),
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
{% set buttons = [
|
||||
{
|
||||
'attr': {
|
||||
'target': '_new',
|
||||
'data-toggle': '',
|
||||
'class': 'btn btn-ghost btn-nospin',
|
||||
'href': path('mautic_page_export', {'objectId': activePage.id, 'format': 'html'}),
|
||||
},
|
||||
'btnText': 'mautic.form.result.export.html'|trans,
|
||||
'iconClass': 'ri-file-code-line',
|
||||
'primary': true,
|
||||
},
|
||||
{
|
||||
'attr': {
|
||||
'data-toggle': '',
|
||||
'class': 'btn btn-ghost btn-nospin',
|
||||
'href': path('mautic_page_export', {'objectId': activePage.id, 'format': 'csv'}),
|
||||
},
|
||||
'btnText': 'mautic.form.result.export.csv'|trans,
|
||||
'iconClass': 'ri-file-text-line',
|
||||
'primary': true,
|
||||
}
|
||||
] %}
|
||||
|
||||
{% if '\\PhpOffice\\PhpSpreadsheet\\Spreadsheet' is class %}
|
||||
{% set buttons = buttons|merge([{
|
||||
'attr': {
|
||||
'data-toggle': '',
|
||||
'class': 'btn btn-ghost btn-nospin',
|
||||
'href': path('mautic_page_export', {'objectId': activePage.id, 'format': 'xlsx'}),
|
||||
},
|
||||
'btnText': 'mautic.form.result.export.xlsx'|trans,
|
||||
'iconClass': 'ri-file-excel-2-fill',
|
||||
'primary': true,
|
||||
}]) %}
|
||||
{% endif %}
|
||||
|
||||
{% set buttons = buttons|merge([{
|
||||
'attr': {
|
||||
'class': 'btn btn-ghost',
|
||||
'href': path('mautic_page_action', {'objectAction': 'view', 'objectId': activePage.id}),
|
||||
'data-toggle': 'ajax',
|
||||
},
|
||||
'iconClass': 'ri-close-line',
|
||||
'btnText': 'mautic.core.form.close'|trans,
|
||||
}]) %}
|
||||
|
||||
{{ include('@MauticCore/Helper/page_actions.html.twig', {'customButtons': buttons}) }}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div class="page-list">
|
||||
{% endif %}
|
||||
|
||||
{{ include('@MauticPage/Result/_list.html.twig') }}
|
||||
|
||||
{% if isIndex %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,26 @@
|
||||
{#
|
||||
Variables
|
||||
- results
|
||||
- variants
|
||||
#}
|
||||
{% set support = results.support %}
|
||||
{% set label = variants.criteria[results.basedOn].label %}
|
||||
{% set chart = barChartInitialize(support.labels) %}
|
||||
{% if support.data is defined and support.data is iterable %}
|
||||
{% for datasetLabel, values in support.data %}
|
||||
{% do chart.setDataset(datasetLabel, values) %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="panel ovf-h bg-light-xs abtest-bar-chart">
|
||||
<div class="panel-body box-layout">
|
||||
<div class="col-xs-8 va-m">
|
||||
<h5 class="text-white dark-md fw-sb mb-xs">
|
||||
{{ label|trans }}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="col-xs-4 va-t text-right">
|
||||
<h3 class="text-white dark-sm"><span class="ri-bar-chart-box-line"></span></h3>
|
||||
</div>
|
||||
</div>
|
||||
{{ include('@MauticCore/Helper/chart.html.twig', {'chartData': chart.render, 'chartType': 'bar', 'chartHeight': 300}) }}
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
{#
|
||||
Variables
|
||||
- pages
|
||||
#}
|
||||
{% set count = pages|length %}
|
||||
{% if count > 0 %}
|
||||
<div class="page-lang-bar">
|
||||
{% for page in pages %}
|
||||
{% set active = app.request.requestUri == page.url %}
|
||||
<span>
|
||||
{% if not active %}
|
||||
<a href="{{ page.url }}">
|
||||
{% endif %}
|
||||
{{ page.lang }}
|
||||
{% if not active %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</span>
|
||||
{% set count = count - 1 %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,5 @@
|
||||
{{ assetAddScriptDeclaration('.share-buttons { display: block; }
|
||||
.share-button { float: left; margin-right: 5px; }
|
||||
.share-button.facebook-share-button.layout-box_count.action-like iframe { width: 50px !important; }
|
||||
.share-button.facebook-share-button.layout-box_count { margin-right: 10px !important; }
|
||||
.share-button.twitter-share-button.layout-horizontal { width: 75px !important; }') }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{% if showMore is defined %}
|
||||
<a href="{{ url('mautic_page_index', {'search': searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%': remaining}) }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url('mautic_page_action', {'objectAction': 'view', 'objectId': item.id}) }}" data-toggle="ajax">
|
||||
<span class="fw-sb">{{ item.title|purify }}</span>
|
||||
<span class="ml-4 mr-sm">#{{ item.getId() }}</span>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'available',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
<span class="pull-right" data-toggle="tooltip" title="{{ 'mautic.page.hits'|trans }}" data-placement="left">
|
||||
<i class="ri-eye-line"></i>
|
||||
{{ item.hits }}
|
||||
</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,100 @@
|
||||
{% if event.extra is defined %}
|
||||
{% set timeOnPage = 'mautic.core.unknown'|trans %}
|
||||
|
||||
{% if event.extra.hit.dateLeft %}
|
||||
{% set timeOnPage = (event.extra.hit.dateLeft.timestamp - event.extra.hit.dateHit.timestamp) %}
|
||||
|
||||
{# format the time #}
|
||||
{% if timeOnPage > 60 %}
|
||||
{% set sec = timeOnPage % 60 %}
|
||||
{% set min = (timeOnPage / 60)|round(0, 'floor') %}
|
||||
{% set timeOnPage = min ~ 'm ' ~ sec ~ 's' %}
|
||||
{% else %}
|
||||
{% set timeOnPage = timeOnPage ~ 's' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{{ 'mautic.page.time.on.page'|trans }}:</dt>
|
||||
<dd>{{ timeOnPage }}</dd>
|
||||
<dt>{{ 'mautic.page.referrer'|trans }}:</dt>
|
||||
<dd>{% if event['extra']['hit']['referer'] %}{{ assetMakeLinks(event['extra']['hit']['referer']) }}{% else %}{{ 'mautic.core.unknown'|trans }}{% endif %}</dd>
|
||||
<dt>{{ 'mautic.page.url'|trans }}:</dt>
|
||||
<dd>{% if event['extra']['hit']['url'] %}{{ assetMakeLinks(event['extra']['hit']['url']) }}{% else %}{{ 'mautic.core.unknown'|trans }}{% endif %}</dd>
|
||||
|
||||
{% if event.extra.hit.device is defined and event.extra.hit.device is not empty %}
|
||||
<dt>{{ 'mautic.core.timeline.device.name'|trans }}</dt>
|
||||
<dd class="ellipsis">{{ inputClean(event.extra.hit.device) }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if event.extra.hit.deviceOsName is defined and event.extra.hit.deviceOsName is not empty %}
|
||||
<dt>{{ 'mautic.core.timeline.device.os'|trans }}</dt>
|
||||
<dd class="ellipsis">{{ inputClean(event.extra.hit.deviceOsName) }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if event.extra.hit.deviceBrand is defined and event.extra.hit.deviceBrand is not empty %}
|
||||
<dt>{{ 'mautic.core.timeline.device.brand'|trans }}</dt>
|
||||
<dd class="ellipsis">{{ inputClean(event.extra.hit.deviceBrand) }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if event.extra.hit.deviceModel is defined and event.extra.hit.deviceModel is not empty %}
|
||||
<dt>{{ 'mautic.core.timeline.device.model'|trans }}</dt>
|
||||
<dd class="ellipsis">{{ inputClean(event.extra.hit.deviceModel) }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if event.extra.hit.sourceName is defined and event.extra.hit.sourceName is not empty %}
|
||||
<dt>{{ 'mautic.core.source'|trans }}:</dt>
|
||||
<dd>
|
||||
{% if event.extra.hit.sourceRate is defined %}
|
||||
<a href="{{ inputClean(event.extra.hit.sourceRoute) }}" data-toggle="ajax">{{ inputClean(event.extra.hit.sourceName) }}</a>
|
||||
{% else %}
|
||||
{{ inputClean(event.extra.hit.sourceName) }}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
{% if event.extra.hit.clientInfo is defined and event.extra.hit.clientInfo is not empty and event.extra.hit.clientInfo is iterable %}
|
||||
<dt>{{ 'mautic.core.timeline.device.client.info'|trans }}</dt>
|
||||
<dd class="ellipsis">
|
||||
{% for clientInfo in event.extra.hit.clientInfo %}
|
||||
{{ inputClean(clientInfo) }}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if event.extra.hit.query is defined and event.extra.hit.query is not empty and event.extra.hit.query is iterable %}
|
||||
{% set counter = 0 %}
|
||||
{% for k, v in event.extra.hit.query %}
|
||||
{% if v is not empty and k not in ['ct', 'page_title', 'page_referrer', 'page_url'] %}
|
||||
{% if v is iterable %}
|
||||
{% for k2, v2 in v %}
|
||||
{% set counter = counter + 1 %}
|
||||
{% set k2 = k|replace({'_': ' '})|title %}
|
||||
<dt>{{ k2 }}:</dt>
|
||||
<dd class="ellipsis">{{ v2 }}</dd>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% set counter = counter + 1 %}
|
||||
{% set k = k|replace({'_': ' '})|title %}
|
||||
<dt>{{ k }}</dt>
|
||||
<dd class="ellipsis">{{ v }}</dd>
|
||||
{% endif %}
|
||||
|
||||
{% if showMore is not defined and counter > 5 %}
|
||||
{% set showMore = true %}
|
||||
<div style="display:none">
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if showMore is defined and true == showMore %}
|
||||
</div>
|
||||
<a href="javascript:void(0);" class="text-center small center-block mt-xs" onclick="Mautic.toggleTimelineMoreVisiblity(mQuery(this).prev());">{{ 'mautic.core.more.show'|trans }}</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</dl>
|
||||
<div class="small">
|
||||
{{ inputClean(event.extra.hit.userAgent) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% set viewTime = 'mautic.core.unknown'|trans %}
|
||||
{% set duration = 'mautic.core.unknown'|trans %}
|
||||
{% set percentage = 'mautic.core.unknown'|trans %}
|
||||
{% set unknown = 'mautic.core.unknown'|trans %}
|
||||
{% set icon = event.icon|default('') %}
|
||||
|
||||
{% if event.extra.hit.time_watched is defined %}
|
||||
{% set viewTimeActual = event.extra.hit.time_watched %}
|
||||
{% set viewTime = event.extra.hit.time_watched %}
|
||||
|
||||
{# format the time #}
|
||||
{% if viewTime > 60 %}
|
||||
{% set sec = viewTime % 60 %}
|
||||
{% set min = (viewTime / 60)|round(0, 'floor') %}
|
||||
{% set viewTime = min ~ 'm ' ~ sec ~ 's' %}
|
||||
{% else %}
|
||||
{% set viewTime = viewTime ~ 's' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if event.extra.hit.duration is defined %}
|
||||
{% set durationActual = event.extra.hit.duration %}
|
||||
{% set duration = event.extra.hit.duration %}
|
||||
|
||||
{# format the time #}
|
||||
{% if duration > 60 %}
|
||||
{% set sec = duration % 60 %}
|
||||
{% set min = (duration / 60)|round(0, 'floor') %}
|
||||
{% set duration = min ~ 'm ' ~ sec ~ 's' %}
|
||||
{% else %}
|
||||
{% set duration = duration ~ 's' %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if viewTime != unknown and duration != unknown %}
|
||||
{% set percentage = ((viewTimeActual / durationActual) * 100)|round %}
|
||||
{% endif %}
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{{ 'mautic.page.time.on.video'|trans }}:</dt>
|
||||
<dd class="ellipsis">{{ 'mautic.page.time.on.video.value'|trans({'%time_watched%': viewTime, '%duration%': duration, '%percentage%': percentage}) }}</dd>
|
||||
<dt>{{ 'mautic.page.referrer'|trans }}:</dt>
|
||||
<dd class="ellipsis">{% if event.extra.hit.referer is defined and event.extra.hit.referer is not empty %}{{ assetMakeLinks(event['extra']['hit']['referer']) }}{% else %}{{ 'mautic.core.unknown'|trans }}{% endif %}</dd>
|
||||
<dt>{{ 'mautic.video.url'|trans }}:</dt>
|
||||
<dd class="ellipsis">{% if event.hit.url is defined and event.hit.url is not empty %}{{ assetMakeLinks(event['extra']['hit']['url']) }}{% else %}{{ 'mautic.core.unknown'|trans }}{% endif %}</dd>
|
||||
</dl>
|
||||
<div class="small">
|
||||
{{ inputClean(event.extra.hit.user_agent) }}
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user