Files
Cloud-CMS/lib/Report/ProofOfPlay.php

1369 lines
49 KiB
PHP
Raw Permalink Normal View History

2025-12-02 10:32:59 -05:00
<?php
/*
* Copyright (C) 2024 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Report;
use Carbon\Carbon;
use MongoDB\BSON\UTCDateTime;
use Psr\Container\ContainerInterface;
use Xibo\Controller\DataTablesDotNetTrait;
use Xibo\Entity\ReportForm;
use Xibo\Entity\ReportResult;
use Xibo\Entity\ReportSchedule;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ReportScheduleFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\TagFactory;
use Xibo\Helper\ApplicationState;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\SanitizerService;
use Xibo\Helper\Translate;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
/**
* Class ProofOfPlay
* @package Xibo\Report
*/
class ProofOfPlay implements ReportInterface
{
use ReportDefaultTrait, DataTablesDotNetTrait;
/**
* @var DisplayFactory
*/
private $displayFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var ReportScheduleFactory
*/
private $reportScheduleFactory;
/**
* @var DisplayGroupFactory
*/
private $displayGroupFactory;
/**
* @var TagFactory
*/
private $tagFactory;
/**
* @var SanitizerService
*/
private $sanitizer;
/**
* @var ApplicationState
*/
private $state;
private $table = 'stat';
private $tagsType = [
'dg' => 'Display group',
'media' => 'Media',
'layout' => 'Layout'
];
/** @inheritdoc */
public function setFactories(ContainerInterface $container)
{
$this->displayFactory = $container->get('displayFactory');
$this->mediaFactory = $container->get('mediaFactory');
$this->layoutFactory = $container->get('layoutFactory');
$this->reportScheduleFactory = $container->get('reportScheduleFactory');
$this->displayGroupFactory = $container->get('displayGroupFactory');
$this->tagFactory = $container->get('tagFactory');
$this->sanitizer = $container->get('sanitizerService');
return $this;
}
/** @inheritdoc */
public function getReportEmailTemplate()
{
return 'proofofplay-email-template.twig';
}
/** @inheritdoc */
public function getSavedReportTemplate()
{
return 'proofofplay-report-preview';
}
/** @inheritdoc */
public function getReportForm()
{
return new ReportForm(
'proofofplay-report-form',
'proofofplayReport',
'Proof of Play',
[
'fromDateOneDay' => Carbon::now()->subSeconds(86400)->format(DateFormatHelper::getSystemFormat()),
'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
],
__('Select a type and an item (i.e., layout/media/tag)')
);
}
/** @inheritdoc */
public function getReportScheduleFormData(SanitizerInterface $sanitizedParams)
{
$data = [];
$data['type'] = $sanitizedParams->getString('type');
$data['tagsType'] = $sanitizedParams->getString('tagsType');
$exactTags = $sanitizedParams->getCheckbox('exactTags');
$data['exactTags'] = $exactTags == 'true';
$tags = $sanitizedParams->getString('tags');
$data['tags'] = $tags;
$data['hiddenFields'] = '';
$data['reportName'] = 'proofofplayReport';
return [
'template' => 'proofofplay-schedule-form-add',
'data' => $data
];
}
/** @inheritdoc */
public function setReportScheduleFormData(SanitizerInterface $sanitizedParams)
{
$filter = $sanitizedParams->getString('filter');
$filterCriteria = [
'filter' => $filter,
'displayId' => $sanitizedParams->getInt('displayId'),
'layoutId' => $sanitizedParams->getIntArray('layoutId'),
'mediaId' => $sanitizedParams->getIntArray('mediaId'),
'type' => $sanitizedParams->getString('type'),
'sortBy' => $sanitizedParams->getString('sortBy'),
'tagsType' => $sanitizedParams->getString('tagsType'),
'tags' => $sanitizedParams->getString('tags'),
'exactTags' => $sanitizedParams->getCheckbox('exactTags'),
'logicalOperator' => $sanitizedParams->getString('logicalOperator')
];
$schedule = '';
if ($filter == 'daily') {
$schedule = ReportSchedule::$SCHEDULE_DAILY;
$filterCriteria['reportFilter'] = 'yesterday';
} elseif ($filter == 'weekly') {
$schedule = ReportSchedule::$SCHEDULE_WEEKLY;
$filterCriteria['reportFilter'] = 'lastweek';
} elseif ($filter == 'monthly') {
$schedule = ReportSchedule::$SCHEDULE_MONTHLY;
$filterCriteria['reportFilter'] = 'lastmonth';
} elseif ($filter == 'yearly') {
$schedule = ReportSchedule::$SCHEDULE_YEARLY;
$filterCriteria['reportFilter'] = 'lastyear';
}
$filterCriteria['sendEmail'] = $sanitizedParams->getCheckbox('sendEmail');
$filterCriteria['nonusers'] = $sanitizedParams->getString('nonusers');
// Return
return [
'filterCriteria' => json_encode($filterCriteria),
'schedule' => $schedule
];
}
/** @inheritdoc */
public function generateSavedReportName(SanitizerInterface $sanitizedParams)
{
$saveAs = sprintf(__('%s report for ', ucfirst($sanitizedParams->getString('filter'))));
switch ($sanitizedParams->getString('type')) {
case 'layout':
$saveAs .= 'Type: Layout. ';
break;
case 'media':
$saveAs .= 'Type: Media. ';
break;
case 'widget':
$saveAs .= 'Type: Widget. ';
break;
case 'event':
$saveAs .= 'Type: Event. ';
break;
default:
$saveAs .= 'Type: All. ';
break;
}
$layoutIds = $sanitizedParams->getIntArray('layoutIds');
if (isset($layoutIds)) {
if (count($layoutIds) > 0) {
$layouts = '';
foreach ($layoutIds as $id) {
try {
$layout = $this->layoutFactory->getById($id);
} catch (NotFoundException $error) {
// Get the campaign ID
$campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($id);
$layoutId = $this->layoutFactory->getLatestLayoutIdFromLayoutHistory($campaignId);
$layout = $this->layoutFactory->getById($layoutId);
}
$layouts .= $layout->layout . ', ';
}
$saveAs .= 'Layouts: '. $layouts;
}
}
$mediaIds = $sanitizedParams->getIntArray('mediaIds');
if (isset($mediaIds)) {
if (count($mediaIds) > 0) {
$medias = '';
foreach ($mediaIds as $id) {
try {
$media = $this->mediaFactory->getById($id);
$name = $media->name;
} catch (NotFoundException $error) {
$name = 'Media not found';
}
$medias .= $name . ', ';
}
$saveAs .= 'Media: ' . $medias;
}
}
$displayId = $sanitizedParams->getInt('displayId');
if (!empty($displayId)) {
// Get display
try {
$displayName = $this->displayFactory->getById($displayId)->display;
$saveAs .= '(Display: '. $displayName . ')';
} catch (NotFoundException $error) {
$saveAs .= '(DisplayId: Not Found )';
}
}
return $saveAs;
}
/** @inheritdoc */
public function restructureSavedReportOldJson($result) // TODO
{
return [
'periodStart' => $result['periodStart'],
'periodEnd' => $result['periodEnd'],
'table' => $result['result'],
];
}
/** @inheritdoc */
public function getSavedReportResults($json, $savedReport)
{
// Get filter criteria
$rs = $this->reportScheduleFactory->getById($savedReport->reportScheduleId, 1)->filterCriteria;
$filterCriteria = json_decode($rs, true);
$tagsType = $filterCriteria['tagsType'];
$tags = $filterCriteria['tags'];
$exactTags = ($filterCriteria['exactTags'] == 1) ? ' (exact match)': '';
// Show filter criteria
$metadata = [];
if ($tags != null) {
$metadata['filterInfo'] = 'Tags from: '. $this->tagsType[$tagsType]. ', Tags: '. $tags. $exactTags;
}
// Get Meta data
$metadata['periodStart'] = $json['metadata']['periodStart'];
$metadata['periodEnd'] = $json['metadata']['periodEnd'];
$metadata['generatedOn'] = Carbon::createFromTimestamp($savedReport->generatedOn)
->format(DateFormatHelper::getSystemFormat());
$metadata['title'] = $savedReport->saveAs;
// Report result object
return new ReportResult(
$metadata,
$json['table'],
$json['recordsTotal']
);
}
/** @inheritdoc */
public function getResults(SanitizerInterface $sanitizedParams)
{
$layoutIds = $sanitizedParams->getIntArray('layoutId', ['default' => []]);
$mediaIds = $sanitizedParams->getIntArray('mediaId', ['default' => []]);
$type = strtolower($sanitizedParams->getString('type'));
$tags = $sanitizedParams->getString('tags');
$tagsType = $sanitizedParams->getString('tagsType');
$exactTags = $sanitizedParams->getCheckbox('exactTags');
$operator = $sanitizedParams->getString('logicalOperator', ['default' => 'OR']);
$parentCampaignId = $sanitizedParams->getInt('parentCampaignId');
// Group the data by display, display group, or by tag
$groupBy = $sanitizedParams->getString('groupBy');
// Used with groupBy in case we want to filter by specific display groups only
$displayGroupIds = $sanitizedParams->getIntArray('displayGroupId', ['default' => []]);
// Display filter.
try {
// Get an array of display id this user has access to.
$displayIds = $this->getDisplayIdFilter($sanitizedParams);
} catch (GeneralException $exception) {
// stop the query
return new ReportResult();
}
// web
if ($sanitizedParams->getString('sortBy') == null) {
// Sorting?
$sortOrder = $this->gridRenderSort($sanitizedParams);
$columns = [];
if (is_array($sortOrder)) {
$columns = $sortOrder;
}
} else {
$sortBy = $sanitizedParams->getString('sortBy', ['default' => 'widgetId']);
if (!in_array($sortBy, [
'widgetId',
'type',
'display',
'displayId',
'media',
'layout',
'layoutId',
'tag',
])) {
throw new InvalidArgumentException(__('Invalid Sort By'), 'sortBy');
}
$columns = [$sortBy];
}
//
// From and To Date Selection
// --------------------------
// Our report has a range filter which determines whether the user has to enter their own from / to dates
// check the range filter first and set from/to dates accordingly.
$reportFilter = $sanitizedParams->getString('reportFilter');
// Use the current date as a helper
$now = Carbon::now();
switch ($reportFilter) {
case 'today':
$fromDt = $now->copy()->startOfDay();
$toDt = $fromDt->copy()->addDay();
break;
case 'yesterday':
$fromDt = $now->copy()->startOfDay()->subDay();
$toDt = $now->copy()->startOfDay();
break;
case 'thisweek':
$fromDt = $now->copy()->locale(Translate::GetLocale())->startOfWeek();
$toDt = $fromDt->copy()->addWeek();
break;
case 'thismonth':
$fromDt = $now->copy()->startOfMonth();
$toDt = $fromDt->copy()->addMonth();
break;
case 'thisyear':
$fromDt = $now->copy()->startOfYear();
$toDt = $fromDt->copy()->addYear();
break;
case 'lastweek':
$fromDt = $now->copy()->locale(Translate::GetLocale())->startOfWeek()->subWeek();
$toDt = $fromDt->copy()->addWeek();
break;
case 'lastmonth':
$fromDt = $now->copy()->startOfMonth()->subMonth();
$toDt = $fromDt->copy()->addMonth();
break;
case 'lastyear':
$fromDt = $now->copy()->startOfYear()->subYear();
$toDt = $fromDt->copy()->addYear();
break;
case '':
default:
// Expect dates to be provided.
$fromDt = $sanitizedParams->getDate('statsFromDt', ['default' => Carbon::now()->subDay()]);
$fromDt->startOfDay();
$toDt = $sanitizedParams->getDate('statsToDt', ['default' => Carbon::now()]);
$toDt->startOfDay();
$fromDtTime = $sanitizedParams->getString('statsFromDtTime');
$toDtTime = $sanitizedParams->getString('statsToDtTime');
if ($fromDtTime !== null && $toDtTime !== null) {
$startTimeArray = explode(':', $fromDtTime);
$fromDt->setTime(intval($startTimeArray[0]), intval($startTimeArray[1]));
$toTimeArray = explode(':', $toDtTime);
$toDt->setTime(intval($toTimeArray[0]), intval($toTimeArray[1]));
}
// What if the fromdt and todt are exactly the same?
// in this case assume an entire day from midnight on the fromdt to midnight on the todt (i.e. add a day to the todt)
if ($fromDt == $toDt) {
$toDt->addDay();
}
break;
}
//
// Get Results!
// -------------
$timeSeriesStore = $this->getTimeSeriesStore()->getEngine();
if ($timeSeriesStore == 'mongodb') {
$result = $this->getProofOfPlayReportMongoDb(
$fromDt,
$toDt,
$displayIds,
$parentCampaignId,
$layoutIds,
$mediaIds,
$type,
$columns,
$tags,
$tagsType,
$exactTags,
$operator,
$groupBy,
$displayGroupIds
);
} else {
$result = $this->getProofOfPlayReportMySql(
$fromDt,
$toDt,
$displayIds,
$parentCampaignId,
$layoutIds,
$mediaIds,
$type,
$columns,
$tags,
$tagsType,
$exactTags,
$operator,
$groupBy
);
}
// Sanitize results
$rows = [];
foreach ($result['result'] as $row) {
$entry = [];
$sanitizedRow = $this->sanitizer->getSanitizer($row);
$widgetId = $sanitizedRow->getInt('widgetId');
$widgetName = $sanitizedRow->getString('media');
// If the media name is empty, and the widgetid is not, then we can assume it has been deleted.
$widgetName = ($widgetName == '' && $widgetId != 0) ? __('Deleted from Layout') : $widgetName;
$displayName = $sanitizedRow->getString('display');
$layoutName = $sanitizedRow->getString('layout');
$parentCampaignName = $sanitizedRow->getString('parentCampaign');
$entry['type'] = $sanitizedRow->getString('type');
$entry['displayId'] = $sanitizedRow->getInt('displayId');
$entry['display'] = ($displayName != '') ? $displayName : __('Not Found');
$entry['layoutId'] = $sanitizedRow->getInt('layoutId');
$entry['layout'] = ($layoutName != '') ? $layoutName : __('Not Found');
$entry['parentCampaignId'] = $sanitizedRow->getInt('parentCampaignId');
$entry['parentCampaign'] = $parentCampaignName;
$entry['widgetId'] = $sanitizedRow->getInt('widgetId');
$entry['media'] = $widgetName;
$entry['tag'] = $sanitizedRow->getString('tag');
$entry['numberPlays'] = $sanitizedRow->getInt('numberPlays');
$entry['duration'] = $sanitizedRow->getInt('duration');
$entry['minStart'] = Carbon::createFromTimestamp($row['minStart'])->format(DateFormatHelper::getSystemFormat());
$entry['maxEnd'] = Carbon::createFromTimestamp($row['maxEnd'])->format(DateFormatHelper::getSystemFormat());
$entry['mediaId'] = $sanitizedRow->getInt('mediaId');
$entry['displayGroup'] = $sanitizedRow->getString('displayGroup');
$entry['displayGroupId'] = $sanitizedRow->getInt('displayGroupId');
$entry['tagName'] = $sanitizedRow->getString('tagName');
$entry['tagId'] = $sanitizedRow->getInt('tagId');
$rows[] = $entry;
}
// Set Meta data
$metadata = [
'periodStart' => $result['periodStart'],
'periodEnd' => $result['periodEnd'],
];
$recordsTotal = $result['count'];
// ----
// Table Only
// Return data to build chart/table
// This will get saved to a json file when schedule runs
return new ReportResult(
$metadata,
$rows,
$recordsTotal
);
}
/**
* MySQL proof of play report
* @param Carbon $fromDt The filter range from date
* @param Carbon $toDt The filter range to date
* @param $displayIds array
* @param $parentCampaignId int
* @param $layoutIds array
* @param $mediaIds array
* @param $type string
* @param $columns array
* @param $tags string
* @param $tagsType string
* @param $exactTags mixed
* @param $groupBy string
* @return array[array result, date periodStart, date periodEnd, int count, int totalStats]
*/
private function getProofOfPlayReportMySql(
$fromDt,
$toDt,
$displayIds,
$parentCampaignId,
$layoutIds,
$mediaIds,
$type,
$columns,
$tags,
$tagsType,
$exactTags,
$logicalOperator,
$groupBy
) {
$fromDt = $fromDt->format('U');
$toDt = $toDt->format('U');
// Media on Layouts Ran
$select = '
SELECT stat.type,
stat.parentCampaignId,
campaign.campaign as parentCampaign,
IFNULL(layout.Layout,
(SELECT MAX(`layout`) AS layout
FROM `layout`
INNER JOIN `layouthistory`
ON `layout`.layoutId = `layouthistory`.layoutId
WHERE `layouthistory`.campaignId = `stat`.campaignId)
) AS Layout,
IFNULL(`media`.name, IFNULL(`widgetoption`.value, `widget`.type)) AS Media,
SUM(stat.count) AS NumberPlays,
SUM(stat.duration) AS Duration,
MIN(start) AS MinStart,
MAX(end) AS MaxEnd,
stat.tag,
stat.layoutId,
stat.mediaId,
stat.widgetId
';
// We get the ID and name - either by display, display group or tag
if ($groupBy === 'display') {
$select .= ', display.Display, stat.displayId ';
} else if ($groupBy === 'displayGroup') {
$select .= ', displaydg.displayGroup, displaydg.displayGroupId ';
} else if ($groupBy === 'tag') {
if ($tagsType === 'dg' || $tagsType === 'media') {
$select .= ', taglink.value, taglink.tagId ';
} else {
// For layouts, we need to manually select taglink.tag
$select .= ', taglink.tag AS value, taglink.tagId ';
}
}
$body = '
FROM stat
LEFT OUTER JOIN display
ON stat.DisplayID = display.DisplayID
LEFT OUTER JOIN layouthistory
ON layouthistory.LayoutID = stat.LayoutID
LEFT OUTER JOIN layout
ON layout.LayoutID = layouthistory.layoutId
LEFT OUTER JOIN `widget`
ON `widget`.widgetId = stat.widgetId
LEFT OUTER JOIN `widgetoption`
ON `widgetoption`.widgetId = `widget`.widgetId
AND `widgetoption`.type = \'attrib\'
AND `widgetoption`.option = \'name\'
LEFT OUTER JOIN `media`
ON `media`.mediaId = `stat`.mediaId
LEFT OUTER JOIN `campaign`
ON `campaign`.campaignId = `stat`.parentCampaignId
';
if ($tags != '') {
if ($tagsType === 'dg') {
$body .= 'INNER JOIN `lkdisplaydg`
ON lkdisplaydg.DisplayID = display.displayid
INNER JOIN `displaygroup`
ON displaygroup.displaygroupId = lkdisplaydg.displaygroupId
AND `displaygroup`.isDisplaySpecific = 1 ';
}
}
if ($groupBy === 'displayGroup') {
// Group the data by display group
$body .= 'INNER JOIN `lkdisplaydg` AS linkdg
ON linkdg.DisplayID = display.displayid
INNER JOIN `displaygroup` AS displaydg
ON displaydg.displaygroupId = linkdg.displaygroupId
AND `displaydg`.isDisplaySpecific = 0 ';
} else if ($groupBy === 'tag') {
$body .= $this->groupByTagType($tagsType);
}
$body .= ' WHERE stat.type <> \'displaydown\'
AND stat.end > :fromDt
AND stat.start < :toDt
';
// Filter by display
if (count($displayIds) > 0) {
$body .= ' AND stat.displayID IN (' . implode(',', $displayIds) . ') ';
}
$params = [
'fromDt' => $fromDt,
'toDt' => $toDt
];
if ($tags != '') {
if (trim($tags) === '--no-tag') {
if ($tagsType === 'dg') {
$body .= ' AND `displaygroup`.displaygroupId NOT IN (
SELECT `lktagdisplaygroup`.displaygroupId
FROM tag
INNER JOIN `lktagdisplaygroup`
ON `lktagdisplaygroup`.tagId = tag.tagId
)
';
}
// old layout and latest layout have same tags
// old layoutId replaced with latest layoutId in the lktaglayout table and
// join with layout history to get campaignId then we can show old layouts that have no tag
if ($tagsType === 'layout') {
$body .= ' AND `stat`.campaignId NOT IN (
SELECT
`layouthistory`.campaignId
FROM
(
SELECT `lktaglayout`.layoutId
FROM tag
INNER JOIN `lktaglayout`
ON `lktaglayout`.tagId = tag.tagId ) B
LEFT OUTER JOIN
`layouthistory` ON `layouthistory`.layoutId = B.layoutId
)
';
}
if ($tagsType === 'media') {
$body .= ' AND `media`.mediaId NOT IN (
SELECT `lktagmedia`.mediaId
FROM tag
INNER JOIN `lktagmedia`
ON `lktagmedia`.tagId = tag.tagId
)
';
}
} else {
$operator = $exactTags == 1 ? '=' : 'LIKE';
$lkTagTable = '';
$lkTagTableIdColumn = '';
$idColumn = '';
$allTags = explode(',', $tags);
$excludeTags = [];
$includeTags = [];
foreach ($allTags as $tag) {
if (str_starts_with($tag, '-')) {
$excludeTags[] = ltrim(($tag), '-');
} else {
$includeTags[] = $tag;
}
}
if ($tagsType === 'dg') {
$lkTagTable = 'lktagdisplaygroup';
$lkTagTableIdColumn = 'lkTagDisplayGroupId';
$idColumn = 'displayGroupId';
}
if ($tagsType === 'layout') {
$lkTagTable = 'lktaglayout';
$lkTagTableIdColumn = 'lkTagLayoutId';
$idColumn = 'layoutId';
}
if ($tagsType === 'media') {
$lkTagTable = 'lktagmedia';
$lkTagTableIdColumn = 'lkTagMediaId';
$idColumn = 'mediaId';
}
if (!empty($excludeTags)) {
$body .= $this->getBodyForTagsType($tagsType, true);
// pass to BaseFactory tagFilter, it does not matter from which factory we do that.
$this->layoutFactory->tagFilter(
$excludeTags,
$lkTagTable,
$lkTagTableIdColumn,
$idColumn,
$logicalOperator,
$operator,
true,
$body,
$params
);
// old layout and latest layout have same tags
// old layoutId replaced with latest layoutId in the lktaglayout table and
// join with layout history to get campaignId then we can show old layouts that have given tag
if ($tagsType === 'layout') {
$body .= ' B
LEFT OUTER JOIN
`layouthistory` ON `layouthistory`.layoutId = B.layoutId ) ';
}
}
if (!empty($includeTags)) {
$body .= $this->getBodyForTagsType($tagsType, false);
// pass to BaseFactory tagFilter, it does not matter from which factory we do that.
$this->layoutFactory->tagFilter(
$includeTags,
$lkTagTable,
$lkTagTableIdColumn,
$idColumn,
$logicalOperator,
$operator,
false,
$body,
$params
);
// old layout and latest layout have same tags
// old layoutId replaced with latest layoutId in the lktaglayout table and
// join with layout history to get campaignId then we can show old layouts that have given tag
if ($tagsType === 'layout') {
$body .= ' C
LEFT OUTER JOIN
`layouthistory` ON `layouthistory`.layoutId = C.layoutId ) ';
}
}
}
}
// Type filter
if ($type == 'layout') {
$body .= ' AND `stat`.type = \'layout\' ';
} elseif ($type == 'media') {
$body .= ' AND `stat`.type = \'media\' AND IFNULL(`media`.mediaId, 0) <> 0 ';
} elseif ($type == 'widget') {
$body .= ' AND `stat`.type = \'widget\' AND IFNULL(`widget`.widgetId, 0) <> 0 ';
} elseif ($type == 'event') {
$body .= ' AND `stat`.type = \'event\' ';
}
// Campaign Filter
if ($parentCampaignId != null) {
$body .= ' AND `stat`.parentCampaignId = :parentCampaignId ';
$params['parentCampaignId'] = $parentCampaignId;
}
// Layout Filter
if (count($layoutIds) != 0) {
$layoutSql = '';
$i = 0;
foreach ($layoutIds as $layoutId) {
$i++;
$layoutSql .= ':layoutId_' . $i . ',';
$params['layoutId_' . $i] = $layoutId;
}
$body .= ' AND `stat`.campaignId IN (SELECT campaignId from layouthistory where layoutId IN ('
. trim($layoutSql, ',') . ')) ';
}
// Media Filter
if (count($mediaIds) != 0) {
$mediaSql = '';
$i = 0;
foreach ($mediaIds as $mediaId) {
$i++;
$mediaSql .= ':mediaId_' . $i . ',';
$params['mediaId_' . $i] = $mediaId;
}
$body .= ' AND `media`.mediaId IN (' . trim($mediaSql, ',') . ')';
}
// We first implement default groupings
$body .= '
GROUP BY stat.type,
stat.tag,
stat.parentCampaignId,
stat.campaignId,
layout.layout,
IFNULL(stat.mediaId, stat.widgetId),
IFNULL(`media`.name, IFNULL(`widgetoption`.value, `widget`.type)),
stat.layoutId,
stat.mediaId,
stat.widgetId
';
// Then add the optional groupings
if ($groupBy === 'display') {
$body .= ', display.Display, stat.displayId';
} else if ($groupBy === 'displayGroup') {
$body .= ', displaydg.displayGroupId, displaydg.displayGroup';
} else if ($groupBy === 'tag') {
$body .= ', value, taglink.tagId';
}
$order = '';
if ($columns != null) {
$order = 'ORDER BY ' . implode(',', $columns);
}
/*Execute sql statement*/
$sql = $select . $body . $order;
$rows = [];
foreach ($this->store->select($sql, $params) as $row) {
$entry = [];
$entry['type'] = $row['type'];
$entry['displayId'] = $row['displayId'] ?? '';
$entry['display'] = $row['Display'] ?? '';
$entry['layout'] = $row['Layout'];
$entry['parentCampaignId'] = $row['parentCampaignId'];
$entry['parentCampaign'] = $row['parentCampaign'];
$entry['media'] = $row['Media'];
$entry['numberPlays'] = $row['NumberPlays'];
$entry['duration'] = $row['Duration'];
$entry['minStart'] = $row['MinStart'];
$entry['maxEnd'] = $row['MaxEnd'];
$entry['layoutId'] = $row['layoutId'];
$entry['widgetId'] = $row['widgetId'];
$entry['mediaId'] = $row['mediaId'];
$entry['tag'] = $row['tag'];
$entry['displayGroupId'] = $row['displayGroupId'] ?? '';
$entry['displayGroup'] = $row['displayGroup'] ?? '';
$entry['tagId'] = $row['tagId'] ?? '';
$entry['tagName'] = $row['value'] ?? '';
$rows[] = $entry;
}
return [
'periodStart' => Carbon::createFromTimestamp($fromDt)->format(DateFormatHelper::getSystemFormat()),
'periodEnd' => Carbon::createFromTimestamp($toDt)->format(DateFormatHelper::getSystemFormat()),
'result' => $rows,
'count' => count($rows)
];
}
private function getBodyForTagsType($tagsType, $exclude) :string
{
if ($tagsType === 'dg') {
return ' AND `displaygroup`.displaygroupId ' . ($exclude ? 'NOT' : '') . ' IN (
SELECT `lktagdisplaygroup`.displaygroupId
FROM tag
INNER JOIN `lktagdisplaygroup`
ON `lktagdisplaygroup`.tagId = tag.tagId
';
} else if ($tagsType === 'media') {
return ' AND `media`.mediaId '. ($exclude ? 'NOT' : '') . ' IN (
SELECT `lktagmedia`.mediaId
FROM tag
INNER JOIN `lktagmedia`
ON `lktagmedia`.tagId = tag.tagId
';
} else if ($tagsType === 'layout') {
return ' AND `stat`.campaignId ' . ($exclude ? 'NOT' : '') . ' IN (
SELECT
`layouthistory`.campaignId
FROM
(
SELECT `lktaglayout`.layoutId
FROM tag
INNER JOIN `lktaglayout`
ON `lktaglayout`.tagId = tag.tagId
';
} else {
$this->getLog()->error(__('Incorrect Tag type selected'));
return '';
}
}
/**
* MongoDB proof of play report
* @param Carbon $filterFromDt The filter range from date
* @param Carbon $filterToDt The filter range to date
* @param $displayIds array
* @param $parentCampaignId int
* @param $layoutIds array
* @param $mediaIds array
* @param $type string
* @param $columns array
* @param $tags string
* @param $tagsType string
* @param $exactTags mixed
* @param $groupBy string
* @param $displayGroupIds array
* @return array[array result, date periodStart, date periodEnd, int count, int totalStats]
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\GeneralException
*/
private function getProofOfPlayReportMongoDb(
$filterFromDt,
$filterToDt,
$displayIds,
$parentCampaignId,
$layoutIds,
$mediaIds,
$type,
$columns,
$tags,
$tagsType,
$exactTags,
$operator,
$groupBy,
$displayGroupIds
) {
$fromDt = new UTCDateTime($filterFromDt->format('U')*1000);
$toDt = new UTCDateTime($filterToDt->format('U')*1000);
// Filters the documents to pass only the documents that
// match the specified condition(s) to the next pipeline stage.
$match = [
'$match' => [
'end' => ['$gt' => $fromDt],
'start' => ['$lt' => $toDt]
]
];
// Display Filter
if (count($displayIds) > 0) {
$match['$match']['displayId'] = [ '$in' => $displayIds ];
}
// Type Filter
if ($type != null) {
$match['$match']['type'] = $type;
}
$tagsArray = [];
// Tag Filter
if ($tags != null) {
$i = 0;
foreach (explode(',', $tags) as $tag) {
$tagV = explode('|', $tag);
if (!isset($tagV[1])) {
$tagsArray[$i]['tag'] = $tag;
} elseif ($tagV[0] == '') {
$tagsArray[$i]['val'] = $tagV[1];
} else {
$tagsArray[$i]['tag'] = $tagV[0];
$tagsArray[$i]['val'] = $tagV[1];
}
$i++;
}
if ($exactTags != 1) {
$tagsArray = array_map(function ($tagValue) {
return array_map(function ($tag) {
return new \MongoDB\BSON\Regex('.*'.$tag. '.*', 'i');
}, $tagValue);
}, $tagsArray);
}
// When exact match is not desired
if (count($tagsArray) > 0) {
$logicalOperator = ($operator === 'AND') ? '$and' : '$or';
foreach ($tagsArray as $tag) {
$match['$match'][$logicalOperator][] = [
'tagFilter.' . $tagsType => [
'$elemMatch' => $tag
]
];
}
}
}
// Campaign Filter
if ($parentCampaignId != null) {
$match['$match']['parentCampaignId'] = $parentCampaignId;
}
// Layout Filter
if (count($layoutIds) != 0) {
// Get campaignIds for selected layoutIds
$campaignIds = [];
foreach ($layoutIds as $layoutId) {
try {
$campaignIds[] = $this->layoutFactory->getCampaignIdFromLayoutHistory($layoutId);
} catch (NotFoundException $notFoundException) {
// Ignore the missing one
$this->getLog()->debug('Filter for Layout without Layout History Record, layoutId is ' . $layoutId);
}
}
$match['$match']['campaignId'] = [ '$in' => $campaignIds ];
}
// Media Filter
if (count($mediaIds) != 0) {
$this->getLog()->debug(json_encode($mediaIds, JSON_PRETTY_PRINT));
$match['$match']['mediaId'] = [ '$in' => $mediaIds ];
}
// For sorting
// The selected column has a key
$temp = [
'_id.type' => 'type',
'_id.display' => 'display',
'layout' => 'layout',
'media' => 'media',
'eventName' => 'eventName',
'layoutId' => 'layoutId',
'parentCampaignId' => 'parentCampaignId',
'parentCampaign' => 'parentCampaign',
'widgetId' => 'widgetId',
'_id.displayId' => 'displayId',
'numberPlays' => 'numberPlays',
'minStart' => 'minStart',
'maxEnd' => 'maxEnd',
'duration' => 'duration',
];
// Remove ` and DESC from the array strings
$cols = [];
foreach ($columns as $column) {
$str = str_replace('`', '', str_replace(' DESC', '', $column));
if (\strpos($column, 'DESC') !== false) {
$cols[$str] = -1;
} else {
$cols[$str] = 1;
}
}
// The selected column key gets stored in an array with value 1 or -1 (for DESC)
$array = [];
foreach ($cols as $k => $v) {
if (array_search($k, $temp)) {
$array[array_search($k, $temp)] = $v;
}
}
$order = ['_id.type'=> 1]; // default sorting by type
if ($array != null) {
$order = $array;
}
$project = [
'$project' => [
'campaignId' => 1,
'mediaId' => 1,
'mediaName'=> 1,
'media'=> [ '$ifNull' => [ '$mediaName', '$widgetName' ] ],
'eventName' => 1,
'widgetId' => 1,
'widgetName' => 1,
'layoutId' => 1,
'layoutName' => 1,
'parentCampaignId' => 1,
'parentCampaign' => 1,
'displayId' => 1,
'displayName' => 1,
'start' => 1,
'end' => 1,
'type' => 1,
'duration' => 1,
'count' => 1,
'total' => ['$sum' => 1],
]
];
$group = [
'$group' => [
'_id' => [
'type' => '$type',
'parentCampaignId'=> '$parentCampaignId',
'campaignId'=> [ '$ifNull' => [ '$campaignId', '$layoutId' ] ],
'mediaorwidget'=> [ '$ifNull' => [ '$mediaId', '$widgetId' ] ],
'displayId'=> [ '$ifNull' => [ '$displayId', null ] ],
'display'=> '$displayName',
'eventName'=> '$eventName',
// we don't need to group by media name and widget name
],
'media'=> [ '$first' => '$media'],
'eventName'=> [ '$first' => '$eventName'],
'mediaId' => ['$first' => '$mediaId'],
'widgetId' => ['$first' => '$widgetId' ],
'layout' => ['$first' => '$layoutName'],
// use the last layoutId to say that is the latest layoutId
'layoutId' => ['$last' => '$layoutId'],
'parentCampaign' => ['$first' => '$parentCampaign'],
'parentCampaignId' => ['$first' => '$parentCampaignId'],
'minStart' => ['$min' => '$start'],
'maxEnd' => ['$max' => '$end'],
'numberPlays' => ['$sum' => '$count'],
'duration' => ['$sum' => '$duration'],
'total' => ['$max' => '$total'],
],
];
$query = [
$match,
$project,
$group, [
'$facet' => [
'totalData' => [
['$sort' => $order],
]
]
],
];
$result = $this->getTimeSeriesStore()->executeQuery(['collection' => $this->table, 'query' => $query]);
$rows = [];
if (count($result) > 0) {
// Grid results
foreach ($result[0]['totalData'] as $row) {
$entry = [];
$entry['type'] = $row['_id']['type'];
$entry['displayId'] = $row['_id']['displayId'];
$entry['display'] = isset($row['_id']['display']) ? $row['_id']['display']: 'No display';
$entry['layout'] = isset($row['layout']) ? $row['layout']: 'No layout';
$entry['parentCampaignId'] = isset($row['parentCampaignId']) ? $row['parentCampaignId']: '';
$entry['parentCampaign'] = isset($row['parentCampaign']) ? $row['parentCampaign']: '';
$entry['media'] = isset($row['media']) ? $row['media'] : 'No media' ;
$entry['numberPlays'] = $row['numberPlays'];
$entry['duration'] = $row['duration'];
$entry['minStart'] = $row['minStart']->toDateTime()->format('U');
$entry['maxEnd'] = $row['maxEnd']->toDateTime()->format('U');
$entry['layoutId'] = $row['layoutId'];
$entry['widgetId'] = $row['widgetId'];
$entry['mediaId'] = $row['mediaId'];
$entry['tag'] = $row['eventName'];
$entry['displayGroupId'] = '';
$entry['displayGroup'] = '';
$entry['tagId'] = '';
$entry['tagName'] = '';
$rows[] = $entry;
}
}
if ($groupBy === 'tag') {
$rows = $this->groupByTagMongoDb($rows, $tagsType);
} else if ($groupBy === 'displayGroup') {
$rows = $this->groupByDisplayGroupMongoDb($rows, $displayGroupIds);
}
return [
'periodStart' => $filterFromDt->format(DateFormatHelper::getSystemFormat()),
'periodEnd' => $filterToDt->format(DateFormatHelper::getSystemFormat()),
'result' => $rows,
'count' => count($rows)
];
}
/**
* Add grouping by tag type
* @param string $tagType
* @return string
*/
private function groupByTagType(string $tagType) : string
{
return match ($tagType) {
'media' => 'INNER JOIN `lktagmedia` AS taglink ON taglink.mediaId = stat.mediaId',
'layout' => 'INNER JOIN `lktaglayout` ON `lktaglayout`.layoutId = stat.layoutId
INNER JOIN `tag` AS taglink ON taglink.tagId = `lktaglayout`.tagId',
'dg' => 'INNER JOIN `lkdisplaydg` AS linkdg
ON linkdg.DisplayID = display.displayid
INNER JOIN `displaygroup` AS displaydg
ON displaydg.displaygroupId = linkdg.displaygroupId
AND `displaydg`.isDisplaySpecific = 1 INNER JOIN
`lktagdisplaygroup` AS taglink ON taglink.displaygroupId = displaydg.displaygroupId',
};
}
/**
* Group by display group in MongoDB
* @param array $rows
* @param array $filteredDisplayGroupIds
* @return array
* @throws NotFoundException
*/
private function groupByDisplayGroupMongoDb(array $rows, array $filteredDisplayGroupIds) : array
{
$data = [];
$displayInfoArr = $this->displayGroupFactory->query();
// Get the display groups
foreach ($rows as $row) {
foreach ($displayInfoArr as $dg) {
// Do we have a filter?
if (!$filteredDisplayGroupIds || in_array($dg->displayGroupId, $filteredDisplayGroupIds)) {
// Create a temporary key to group by multiple columns at once
// and save memory instead of checking each column recursively
$key = $dg->displayGroupId . '_' . $row['layoutId'] . '_' . $row['mediaId'] . '_' .
$row['tag'] . '_' . $row['widgetId'] . '_' . $row['parentCampaignId'] . '_' . $row['type'];
if (!isset($data[$key])) {
// Since we already have the display group as the grouping option, we can remove the display info
$row['display'] = null;
$row['displayId'] = null;
$row['displayGroupId'] = $dg->displayGroupId;
$row['displayGroup'] = $dg->displayGroup;
$data[$key] = $row;
} else {
$data[$key]['duration'] += $row['duration'];
$data[$key]['numberPlays'] += $row['numberPlays'];
}
}
}
}
return $data;
}
/**
* Group by tag in MongoDB
* @param array $rows
* @param string $tagsType
* @return array
*/
private function groupByTagMongoDb(array $rows, string $tagsType) : array
{
$data = [];
$tags = $this->filterByTagType($tagsType);
$type = match ($tagsType) {
'media' => 'mediaId',
'layout' => 'layoutId',
'dg' => 'displayId',
};;
foreach ($rows as $row) {
foreach ($tags as $tag) {
if ($row[$type] == $tag['entityId']) {
// Create a temporary key to group by multiple columns at once
// and save memory instead of checking each column recursively
$key = $tag['tagId'] . '_' . $row['layoutId'] . '_' . $row['mediaId'] . '_' .
$row['tag'] . '_' . $row['widgetId'] . '_' . $row['parentCampaignId'] . '_' . $row['type'];
if (!isset($data[$key])) {
// Since we already have the tags as the grouping option, we can remove the display info
$row['display'] = null;
$row['displayId'] = null;
$row['tagName'] = $tag['tag'];
$row['tagId'] = $tag['tagId'];
$data[$key] = $row;
} else {
$data[$key]['duration'] += $row['duration'];
$data[$key]['numberPlays'] += $row['numberPlays'];
}
}
}
}
return $data;
}
/**
* @param string $tagsType
* @return array
*/
private function filterByTagType(string $tagsType): array
{
$tags = [];
$filter = match ($tagsType) {
'media' => 'Media',
'layout' => 'Layout',
'dg' => 'Display',
};
// Get the list of tags to get the tag type (ie media tag, layout tag, or display tag)
$tagInfoArr = $this->tagFactory->query();
foreach ($tagInfoArr as $tag) {
// What type of tags are we looking for?
foreach ($this->tagFactory->getAllLinks(null, ['tagId' => $tag->tagId]) as $filteredTag) {
if ($filteredTag['type'] == $filter) {
$filteredTag['tagId'] = $tag->tagId;
$filteredTag['tag'] = $tag->tag;
$tags[] = $filteredTag;
}
}
}
return $tags;
}
}