3139 lines
128 KiB
PHP
3139 lines
128 KiB
PHP
|
|
<?php
|
||
|
|
/*
|
||
|
|
* Copyright (C) 2025 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\Xmds;
|
||
|
|
|
||
|
|
use Carbon\Carbon;
|
||
|
|
use Monolog\Logger;
|
||
|
|
use Stash\Interfaces\PoolInterface;
|
||
|
|
use Stash\Invalidation;
|
||
|
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||
|
|
use Xibo\Entity\Bandwidth;
|
||
|
|
use Xibo\Entity\Display;
|
||
|
|
use Xibo\Entity\Region;
|
||
|
|
use Xibo\Entity\RequiredFile;
|
||
|
|
use Xibo\Entity\Schedule;
|
||
|
|
use Xibo\Event\DataConnectorScriptRequestEvent;
|
||
|
|
use Xibo\Event\XmdsDependencyListEvent;
|
||
|
|
use Xibo\Factory\BandwidthFactory;
|
||
|
|
use Xibo\Factory\DataSetFactory;
|
||
|
|
use Xibo\Factory\DayPartFactory;
|
||
|
|
use Xibo\Factory\DisplayEventFactory;
|
||
|
|
use Xibo\Factory\DisplayFactory;
|
||
|
|
use Xibo\Factory\LayoutFactory;
|
||
|
|
use Xibo\Factory\MediaFactory;
|
||
|
|
use Xibo\Factory\ModuleFactory;
|
||
|
|
use Xibo\Factory\NotificationFactory;
|
||
|
|
use Xibo\Factory\PlayerFaultFactory;
|
||
|
|
use Xibo\Factory\PlayerVersionFactory;
|
||
|
|
use Xibo\Factory\RegionFactory;
|
||
|
|
use Xibo\Factory\RequiredFileFactory;
|
||
|
|
use Xibo\Factory\ScheduleFactory;
|
||
|
|
use Xibo\Factory\SyncGroupFactory;
|
||
|
|
use Xibo\Factory\UserFactory;
|
||
|
|
use Xibo\Factory\UserGroupFactory;
|
||
|
|
use Xibo\Factory\WidgetFactory;
|
||
|
|
use Xibo\Helper\ByteFormatter;
|
||
|
|
use Xibo\Helper\DateFormatHelper;
|
||
|
|
use Xibo\Helper\LinkSigner;
|
||
|
|
use Xibo\Helper\SanitizerService;
|
||
|
|
use Xibo\Helper\Status;
|
||
|
|
use Xibo\Service\ConfigServiceInterface;
|
||
|
|
use Xibo\Service\LogServiceInterface;
|
||
|
|
use Xibo\Storage\StorageServiceInterface;
|
||
|
|
use Xibo\Storage\TimeSeriesStoreInterface;
|
||
|
|
use Xibo\Support\Exception\ControllerNotImplemented;
|
||
|
|
use Xibo\Support\Exception\DeadlockException;
|
||
|
|
use Xibo\Support\Exception\GeneralException;
|
||
|
|
use Xibo\Support\Exception\NotFoundException;
|
||
|
|
use Xibo\Xmds\Entity\Dependency;
|
||
|
|
use Xibo\Xmds\Listeners\XmdsDataConnectorListener;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Class Soap
|
||
|
|
* @package Xibo\Xmds
|
||
|
|
*/
|
||
|
|
class Soap
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* @var Display
|
||
|
|
*/
|
||
|
|
protected $display;
|
||
|
|
|
||
|
|
/** @var Carbon */
|
||
|
|
protected $fromFilter;
|
||
|
|
/** @var Carbon */
|
||
|
|
protected $toFilter;
|
||
|
|
/** @var Carbon */
|
||
|
|
protected $localFromFilter;
|
||
|
|
/** @var Carbon */
|
||
|
|
protected $localToFilter;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var LogProcessor
|
||
|
|
*/
|
||
|
|
protected $logProcessor;
|
||
|
|
|
||
|
|
/** @var PoolInterface */
|
||
|
|
protected $pool;
|
||
|
|
|
||
|
|
/** @var StorageServiceInterface */
|
||
|
|
private $store;
|
||
|
|
|
||
|
|
/** @var TimeSeriesStoreInterface */
|
||
|
|
private $timeSeriesStore;
|
||
|
|
|
||
|
|
/** @var LogServiceInterface */
|
||
|
|
private $logService;
|
||
|
|
|
||
|
|
/** @var SanitizerService */
|
||
|
|
private $sanitizerService;
|
||
|
|
|
||
|
|
/** @var ConfigServiceInterface */
|
||
|
|
protected $configService;
|
||
|
|
|
||
|
|
/** @var RequiredFileFactory */
|
||
|
|
protected $requiredFileFactory;
|
||
|
|
|
||
|
|
/** @var ModuleFactory */
|
||
|
|
protected $moduleFactory;
|
||
|
|
|
||
|
|
/** @var LayoutFactory */
|
||
|
|
protected $layoutFactory;
|
||
|
|
|
||
|
|
/** @var DataSetFactory */
|
||
|
|
protected $dataSetFactory;
|
||
|
|
|
||
|
|
/** @var DisplayFactory */
|
||
|
|
protected $displayFactory;
|
||
|
|
|
||
|
|
/** @var UserGroupFactory */
|
||
|
|
protected $userGroupFactory;
|
||
|
|
|
||
|
|
/** @var BandwidthFactory */
|
||
|
|
protected $bandwidthFactory;
|
||
|
|
|
||
|
|
/** @var MediaFactory */
|
||
|
|
protected $mediaFactory;
|
||
|
|
|
||
|
|
/** @var WidgetFactory */
|
||
|
|
protected $widgetFactory;
|
||
|
|
|
||
|
|
/** @var RegionFactory */
|
||
|
|
protected $regionFactory;
|
||
|
|
|
||
|
|
/** @var NotificationFactory */
|
||
|
|
protected $notificationFactory;
|
||
|
|
|
||
|
|
/** @var DisplayEventFactory */
|
||
|
|
protected $displayEventFactory;
|
||
|
|
|
||
|
|
/** @var ScheduleFactory */
|
||
|
|
protected $scheduleFactory;
|
||
|
|
|
||
|
|
/** @var DayPartFactory */
|
||
|
|
protected $dayPartFactory;
|
||
|
|
|
||
|
|
/** @var PlayerVersionFactory */
|
||
|
|
protected $playerVersionFactory;
|
||
|
|
|
||
|
|
/** @var \Xibo\Factory\SyncGroupFactory */
|
||
|
|
protected $syncGroupFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var EventDispatcher
|
||
|
|
*/
|
||
|
|
private $dispatcher;
|
||
|
|
|
||
|
|
/** @var \Xibo\Factory\CampaignFactory */
|
||
|
|
private $campaignFactory;
|
||
|
|
|
||
|
|
/** @var PlayerFaultFactory */
|
||
|
|
protected $playerFaultFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Soap constructor.
|
||
|
|
* @param LogProcessor $logProcessor
|
||
|
|
* @param PoolInterface $pool
|
||
|
|
* @param StorageServiceInterface $store
|
||
|
|
* @param TimeSeriesStoreInterface $timeSeriesStore
|
||
|
|
* @param LogServiceInterface $log
|
||
|
|
* @param SanitizerService $sanitizer
|
||
|
|
* @param ConfigServiceInterface $config
|
||
|
|
* @param RequiredFileFactory $requiredFileFactory
|
||
|
|
* @param ModuleFactory $moduleFactory
|
||
|
|
* @param LayoutFactory $layoutFactory
|
||
|
|
* @param DataSetFactory $dataSetFactory
|
||
|
|
* @param DisplayFactory $displayFactory
|
||
|
|
* @param UserFactory $userGroupFactory
|
||
|
|
* @param BandwidthFactory $bandwidthFactory
|
||
|
|
* @param MediaFactory $mediaFactory
|
||
|
|
* @param WidgetFactory $widgetFactory
|
||
|
|
* @param RegionFactory $regionFactory
|
||
|
|
* @param NotificationFactory $notificationFactory
|
||
|
|
* @param DisplayEventFactory $displayEventFactory
|
||
|
|
* @param ScheduleFactory $scheduleFactory
|
||
|
|
* @param DayPartFactory $dayPartFactory
|
||
|
|
* @param PlayerVersionFactory $playerVersionFactory
|
||
|
|
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
|
||
|
|
* @param \Xibo\Factory\CampaignFactory $campaignFactory
|
||
|
|
* @param SyncGroupFactory $syncGroupFactory
|
||
|
|
*/
|
||
|
|
public function __construct(
|
||
|
|
$logProcessor,
|
||
|
|
$pool,
|
||
|
|
$store,
|
||
|
|
$timeSeriesStore,
|
||
|
|
$log,
|
||
|
|
$sanitizer,
|
||
|
|
$config,
|
||
|
|
$requiredFileFactory,
|
||
|
|
$moduleFactory,
|
||
|
|
$layoutFactory,
|
||
|
|
$dataSetFactory,
|
||
|
|
$displayFactory,
|
||
|
|
$userGroupFactory,
|
||
|
|
$bandwidthFactory,
|
||
|
|
$mediaFactory,
|
||
|
|
$widgetFactory,
|
||
|
|
$regionFactory,
|
||
|
|
$notificationFactory,
|
||
|
|
$displayEventFactory,
|
||
|
|
$scheduleFactory,
|
||
|
|
$dayPartFactory,
|
||
|
|
$playerVersionFactory,
|
||
|
|
$dispatcher,
|
||
|
|
$campaignFactory,
|
||
|
|
$syncGroupFactory,
|
||
|
|
$playerFaultFactory
|
||
|
|
) {
|
||
|
|
$this->logProcessor = $logProcessor;
|
||
|
|
$this->pool = $pool;
|
||
|
|
$this->store = $store;
|
||
|
|
$this->timeSeriesStore = $timeSeriesStore;
|
||
|
|
$this->logService = $log;
|
||
|
|
$this->sanitizerService = $sanitizer;
|
||
|
|
$this->configService = $config;
|
||
|
|
$this->requiredFileFactory = $requiredFileFactory;
|
||
|
|
$this->moduleFactory = $moduleFactory;
|
||
|
|
$this->layoutFactory = $layoutFactory;
|
||
|
|
$this->dataSetFactory = $dataSetFactory;
|
||
|
|
$this->displayFactory = $displayFactory;
|
||
|
|
$this->userGroupFactory = $userGroupFactory;
|
||
|
|
$this->bandwidthFactory = $bandwidthFactory;
|
||
|
|
$this->mediaFactory = $mediaFactory;
|
||
|
|
$this->widgetFactory = $widgetFactory;
|
||
|
|
$this->regionFactory = $regionFactory;
|
||
|
|
$this->notificationFactory = $notificationFactory;
|
||
|
|
$this->displayEventFactory = $displayEventFactory;
|
||
|
|
$this->scheduleFactory = $scheduleFactory;
|
||
|
|
$this->dayPartFactory = $dayPartFactory;
|
||
|
|
$this->playerVersionFactory = $playerVersionFactory;
|
||
|
|
$this->dispatcher = $dispatcher;
|
||
|
|
$this->campaignFactory = $campaignFactory;
|
||
|
|
$this->syncGroupFactory = $syncGroupFactory;
|
||
|
|
$this->playerFaultFactory = $playerFaultFactory;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Cache Pool
|
||
|
|
* @return \Stash\Interfaces\PoolInterface
|
||
|
|
*/
|
||
|
|
protected function getPool()
|
||
|
|
{
|
||
|
|
return $this->pool;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Store
|
||
|
|
* @return StorageServiceInterface
|
||
|
|
*/
|
||
|
|
protected function getStore()
|
||
|
|
{
|
||
|
|
return $this->store;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Time Series Store
|
||
|
|
* @return TimeSeriesStoreInterface
|
||
|
|
*/
|
||
|
|
protected function getTimeSeriesStore()
|
||
|
|
{
|
||
|
|
return $this->timeSeriesStore;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Log
|
||
|
|
* @return LogServiceInterface
|
||
|
|
*/
|
||
|
|
protected function getLog()
|
||
|
|
{
|
||
|
|
return $this->logService;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param $array
|
||
|
|
* @return \Xibo\Support\Sanitizer\SanitizerInterface
|
||
|
|
*/
|
||
|
|
protected function getSanitizer($array)
|
||
|
|
{
|
||
|
|
return $this->sanitizerService->getSanitizer($array);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Config
|
||
|
|
* @return ConfigServiceInterface
|
||
|
|
*/
|
||
|
|
protected function getConfig()
|
||
|
|
{
|
||
|
|
return $this->configService;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return EventDispatcher
|
||
|
|
*/
|
||
|
|
public function getDispatcher(): EventDispatcher
|
||
|
|
{
|
||
|
|
if ($this->dispatcher === null) {
|
||
|
|
$this->getLog()->error('getDispatcher: [soap] No dispatcher found, returning an empty one');
|
||
|
|
$this->dispatcher = new EventDispatcher();
|
||
|
|
}
|
||
|
|
|
||
|
|
return $this->dispatcher;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Required Files (common)
|
||
|
|
* @param $serverKey
|
||
|
|
* @param $hardwareKey
|
||
|
|
* @param bool $httpDownloads
|
||
|
|
* @param bool $isSupportsDataUrl Does the callee support data URLs in widgets?
|
||
|
|
* @param bool $isSupportsDependency Does the callee support a separate dependency call?
|
||
|
|
* @return string
|
||
|
|
* @throws \DOMException
|
||
|
|
* @throws \SoapFault
|
||
|
|
* @throws \Xibo\Support\Exception\NotFoundException
|
||
|
|
*/
|
||
|
|
protected function doRequiredFiles(
|
||
|
|
$serverKey,
|
||
|
|
$hardwareKey,
|
||
|
|
bool $httpDownloads,
|
||
|
|
bool $isSupportsDataUrl = false,
|
||
|
|
bool $isSupportsDependency = false
|
||
|
|
) {
|
||
|
|
$this->logProcessor->setRoute('RequiredFiles');
|
||
|
|
$sanitizer = $this->getSanitizer([
|
||
|
|
'serverKey' => $serverKey,
|
||
|
|
'hardwareKey' => $hardwareKey
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Sanitize
|
||
|
|
$serverKey = $sanitizer->getString('serverKey');
|
||
|
|
$hardwareKey = $sanitizer->getString('hardwareKey');
|
||
|
|
|
||
|
|
// Check the serverKey matches
|
||
|
|
if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) {
|
||
|
|
throw new \SoapFault(
|
||
|
|
'Sender',
|
||
|
|
'The Server key you entered does not match with the server key at this address'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
$libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
|
||
|
|
|
||
|
|
// auth this request...
|
||
|
|
if (!$this->authDisplay($hardwareKey)) {
|
||
|
|
throw new \SoapFault('Sender', 'This Display is not authorised.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now that we authenticated the Display, make sure we are sticking to our bandwidth limit
|
||
|
|
if (!$this->checkBandwidth($this->display->displayId)) {
|
||
|
|
throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the cache
|
||
|
|
$cache = $this->getPool()->getItem($this->display->getCacheKey() . '/requiredFiles');
|
||
|
|
$cache->setInvalidationMethod(Invalidation::OLD);
|
||
|
|
|
||
|
|
$output = $cache->get();
|
||
|
|
|
||
|
|
// Required files are cached for 4 hours
|
||
|
|
if ($cache->isHit()) {
|
||
|
|
$this->getLog()->info('Returning required files from Cache for display ' . $this->display->display);
|
||
|
|
|
||
|
|
// Resign HTTP links and extend expiry
|
||
|
|
$document = new \DOMDocument('1.0');
|
||
|
|
$document->loadXML($output);
|
||
|
|
|
||
|
|
$cdnUrl = $this->configService->getSetting('CDN_URL');
|
||
|
|
|
||
|
|
foreach ($document->documentElement->childNodes as $node) {
|
||
|
|
if ($node instanceof \DOMElement) {
|
||
|
|
if ($node->getAttribute('download') === 'http') {
|
||
|
|
$type = match ($node->getAttribute('type')) {
|
||
|
|
'layout' => 'L',
|
||
|
|
'media' => 'M',
|
||
|
|
default => 'P',
|
||
|
|
};
|
||
|
|
|
||
|
|
// HTTP download for a v3 player will have saved the type and fileType as M and media
|
||
|
|
// respectively, which is different to what we use in the URL.
|
||
|
|
$assetType = $node->getAttribute('assetType');
|
||
|
|
if (!empty($assetType)) {
|
||
|
|
$type = 'P';
|
||
|
|
$fileType = $assetType;
|
||
|
|
} else {
|
||
|
|
$fileType = $node->getAttribute('fileType');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Use the realId if we have it.
|
||
|
|
$realId = $node->getAttribute('realId');
|
||
|
|
if (empty($realId)) {
|
||
|
|
$realId = $node->getAttribute('id');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Generate a new URL.
|
||
|
|
$newUrl = LinkSigner::generateSignedLink(
|
||
|
|
$this->display,
|
||
|
|
$this->configService->getApiKeyDetails()['encryptionKey'],
|
||
|
|
$cdnUrl,
|
||
|
|
$type,
|
||
|
|
$realId,
|
||
|
|
$node->getAttribute('saveAs'),
|
||
|
|
$fileType,
|
||
|
|
);
|
||
|
|
|
||
|
|
$node->setAttribute('path', $newUrl);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$output = $document->saveXML();
|
||
|
|
|
||
|
|
// Log Bandwidth
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$RF, strlen($output));
|
||
|
|
|
||
|
|
return $output;
|
||
|
|
}
|
||
|
|
|
||
|
|
// We need to regenerate
|
||
|
|
// Lock the cache
|
||
|
|
$cache->lock(120);
|
||
|
|
|
||
|
|
// Get all required files for this display.
|
||
|
|
// we will use this to drop items from the requirefile table if they are no longer in required files
|
||
|
|
$rfIds = array_map(function ($element) {
|
||
|
|
return intval($element['rfId']);
|
||
|
|
}, $this->getStore()->select('SELECT rfId FROM `requiredfile` WHERE displayId = :displayId', [
|
||
|
|
'displayId' => $this->display->displayId
|
||
|
|
]));
|
||
|
|
$newRfIds = [];
|
||
|
|
|
||
|
|
// Build a new RF
|
||
|
|
$requiredFilesXml = new \DOMDocument('1.0');
|
||
|
|
$fileElements = $requiredFilesXml->createElement('files');
|
||
|
|
$requiredFilesXml->appendChild($fileElements);
|
||
|
|
|
||
|
|
// Filter criteria
|
||
|
|
$this->setDateFilters();
|
||
|
|
|
||
|
|
// Add the filter dates to the RF xml document
|
||
|
|
$fileElements->setAttribute('generated', Carbon::now()->format(DateFormatHelper::getSystemFormat()));
|
||
|
|
$fileElements->setAttribute('fitlerFrom', $this->fromFilter->format(DateFormatHelper::getSystemFormat()));
|
||
|
|
$fileElements->setAttribute('fitlerTo', $this->toFilter->format(DateFormatHelper::getSystemFormat()));
|
||
|
|
|
||
|
|
// Player dependencies
|
||
|
|
// -------------------
|
||
|
|
// Output player dependencies such as the player bundle, fonts, etc.
|
||
|
|
// 1) get a list of dependencies.
|
||
|
|
$dependencyListEvent = new XmdsDependencyListEvent($this->display);
|
||
|
|
|
||
|
|
$this->getDispatcher()->dispatch($dependencyListEvent, 'xmds.dependency.list');
|
||
|
|
|
||
|
|
// 2) Each dependency returned needs to be added to RF.
|
||
|
|
foreach ($dependencyListEvent->getDependencies() as $dependency) {
|
||
|
|
// Add a new required file for this.
|
||
|
|
$this->addDependency(
|
||
|
|
$newRfIds,
|
||
|
|
$requiredFilesXml,
|
||
|
|
$fileElements,
|
||
|
|
$httpDownloads,
|
||
|
|
$isSupportsDependency,
|
||
|
|
$dependency
|
||
|
|
);
|
||
|
|
}
|
||
|
|
// ------------
|
||
|
|
// Dependencies finished
|
||
|
|
|
||
|
|
// Default Layout
|
||
|
|
$defaultLayoutId = ($this->display->defaultLayoutId === null || $this->display->defaultLayoutId === 0)
|
||
|
|
? $this->getConfig()->getSetting('DEFAULT_LAYOUT')
|
||
|
|
: $this->display->defaultLayoutId;
|
||
|
|
|
||
|
|
// Get a list of all layout ids in the schedule right now
|
||
|
|
// including any layouts that have been associated to our Display Group
|
||
|
|
try {
|
||
|
|
$dbh = $this->getStore()->getConnection();
|
||
|
|
|
||
|
|
$SQL = '
|
||
|
|
SELECT DISTINCT `lklayoutdisplaygroup`.layoutId
|
||
|
|
FROM `lklayoutdisplaygroup`
|
||
|
|
INNER JOIN `lkdgdg`
|
||
|
|
ON `lkdgdg`.parentId = `lklayoutdisplaygroup`.displayGroupId
|
||
|
|
INNER JOIN `lkdisplaydg`
|
||
|
|
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
|
||
|
|
INNER JOIN `layout`
|
||
|
|
ON `layout`.layoutID = `lklayoutdisplaygroup`.layoutId
|
||
|
|
WHERE lkdisplaydg.DisplayID = :displayId
|
||
|
|
ORDER BY layoutId
|
||
|
|
';
|
||
|
|
|
||
|
|
$params = [
|
||
|
|
'displayId' => $this->display->displayId
|
||
|
|
];
|
||
|
|
|
||
|
|
$this->getLog()->sql($SQL, $params);
|
||
|
|
|
||
|
|
$sth = $dbh->prepare($SQL);
|
||
|
|
$sth->execute($params);
|
||
|
|
|
||
|
|
// Build a list of Layouts
|
||
|
|
$layouts = [];
|
||
|
|
|
||
|
|
// Our layout list will always include the default layout
|
||
|
|
if ($defaultLayoutId != null) {
|
||
|
|
$layouts[] = $defaultLayoutId;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Build up the other layouts into an array
|
||
|
|
foreach ($sth->fetchAll() as $row) {
|
||
|
|
$parsedRow = $this->getSanitizer($row);
|
||
|
|
$layouts[] = $parsedRow->getInt('layoutId');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Also look at the schedule
|
||
|
|
foreach ($this->scheduleFactory->getForXmds(
|
||
|
|
$this->display->displayId,
|
||
|
|
$this->fromFilter,
|
||
|
|
$this->toFilter
|
||
|
|
) as $row) {
|
||
|
|
$parsedRow = $this->getSanitizer($row);
|
||
|
|
$schedule = $this->scheduleFactory->createEmpty()->hydrate($row);
|
||
|
|
|
||
|
|
// Is this scheduled event a synchronised timezone?
|
||
|
|
// if it is, then we get our events with respect to the timezone of the display
|
||
|
|
$isSyncTimezone = ($schedule->syncTimezone == 1 && !empty($this->display->timeZone));
|
||
|
|
|
||
|
|
try {
|
||
|
|
if ($isSyncTimezone) {
|
||
|
|
$scheduleEvents = $schedule->getEvents($this->localFromFilter, $this->localToFilter);
|
||
|
|
} else {
|
||
|
|
$scheduleEvents = $schedule->getEvents($this->fromFilter, $this->toFilter);
|
||
|
|
}
|
||
|
|
} catch (GeneralException $e) {
|
||
|
|
$this->getLog()->error('Unable to getEvents for ' . $schedule->eventId);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (count($scheduleEvents) <= 0) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug(count($scheduleEvents) . ' events for eventId ' . $schedule->eventId);
|
||
|
|
|
||
|
|
// Sync events
|
||
|
|
$layoutId = ($schedule->eventTypeId == Schedule::$SYNC_EVENT)
|
||
|
|
? $parsedRow->getInt('syncLayoutId')
|
||
|
|
: $parsedRow->getInt('layoutId');
|
||
|
|
|
||
|
|
// Layout codes (action events)
|
||
|
|
$layoutCode = $parsedRow->getString('actionLayoutCode');
|
||
|
|
if ($layoutId != null &&
|
||
|
|
(
|
||
|
|
$schedule->eventTypeId == Schedule::$LAYOUT_EVENT ||
|
||
|
|
$schedule->eventTypeId == Schedule::$OVERLAY_EVENT ||
|
||
|
|
$schedule->eventTypeId == Schedule::$INTERRUPT_EVENT ||
|
||
|
|
$schedule->eventTypeId == Schedule::$CAMPAIGN_EVENT ||
|
||
|
|
$schedule->eventTypeId == Schedule::$MEDIA_EVENT ||
|
||
|
|
$schedule->eventTypeId == Schedule::$PLAYLIST_EVENT ||
|
||
|
|
$schedule->eventTypeId == Schedule::$SYNC_EVENT
|
||
|
|
)
|
||
|
|
) {
|
||
|
|
$layouts[] = $layoutId;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($layoutCode) && $schedule->eventTypeId == Schedule::$ACTION_EVENT) {
|
||
|
|
$actionEventLayout = $this->layoutFactory->getByCode($layoutCode);
|
||
|
|
if ($actionEventLayout->status <= 3) {
|
||
|
|
$layouts[] = $actionEventLayout->layoutId;
|
||
|
|
} else {
|
||
|
|
$this->getLog()->error(sprintf(__('Scheduled Action Event ID %d contains an invalid Layout linked to it by the Layout code.'), $schedule->eventId));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Data Connectors
|
||
|
|
if ($isSupportsDependency && $schedule->eventTypeId === Schedule::$DATA_CONNECTOR_EVENT) {
|
||
|
|
$dataSet = $this->dataSetFactory->getById($row['dataSetId']);
|
||
|
|
|
||
|
|
if ($dataSet->dataConnectorSource != 'user_defined') {
|
||
|
|
// Dispatch an event to save the data connector javascript from the connector
|
||
|
|
$dataConnectorScriptRequestEvent = new DataConnectorScriptRequestEvent($dataSet);
|
||
|
|
$this->getDispatcher()
|
||
|
|
->dispatch($dataConnectorScriptRequestEvent, DataConnectorScriptRequestEvent::$NAME);
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->addDependency(
|
||
|
|
$newRfIds,
|
||
|
|
$requiredFilesXml,
|
||
|
|
$fileElements,
|
||
|
|
$httpDownloads,
|
||
|
|
$isSupportsDependency,
|
||
|
|
XmdsDataConnectorListener::getDataConnectorDependency($libraryLocation, $row['dataSetId']),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$this->getLog()->error('Unable to get a list of layouts. ' . $e->getMessage());
|
||
|
|
return new \SoapFault('Sender', 'Unable to get a list of layouts');
|
||
|
|
}
|
||
|
|
|
||
|
|
// workout if any of the layouts we have in our list has Actions pointing to another Layout.
|
||
|
|
$actionLayoutIds = [];
|
||
|
|
$processedLayoutIds = [];
|
||
|
|
foreach ($layouts as $layoutId) {
|
||
|
|
// this is recursive function, as we need to get 2nd level nesting and beyond
|
||
|
|
$this->layoutFactory->getActionPublishedLayoutIds($layoutId, $actionLayoutIds, $processedLayoutIds);
|
||
|
|
}
|
||
|
|
|
||
|
|
// merge the Action layouts to our array, we need the player to download all resources on them
|
||
|
|
if (!empty($actionLayoutIds)) {
|
||
|
|
$layouts = array_unique(array_merge($layouts, $actionLayoutIds));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create a comma separated list to pass into the query which gets file nodes
|
||
|
|
$layoutIdList = implode(',', $layouts);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$dbh = $this->getStore()->getConnection();
|
||
|
|
|
||
|
|
// Run a query to get all required files for this display.
|
||
|
|
// Include the following:
|
||
|
|
// DownloadOrder:
|
||
|
|
// 1 - Media Linked to Displays
|
||
|
|
// 2 - Media Linked to Widgets in the Scheduled Layouts (linked through Playlists)
|
||
|
|
// 3 - Background Images for all Scheduled Layouts
|
||
|
|
// 4 - Media linked to display profile (linked through PlayerSoftware)
|
||
|
|
$SQL = "
|
||
|
|
SELECT 1 AS DownloadOrder,
|
||
|
|
`media`.storedAs AS path,
|
||
|
|
`media`.mediaID AS id,
|
||
|
|
`media`.`MD5`,
|
||
|
|
`media`.FileSize,
|
||
|
|
`media`.released
|
||
|
|
FROM `media`
|
||
|
|
INNER JOIN `display_media`
|
||
|
|
ON `display_media`.mediaid = `media`.mediaId
|
||
|
|
WHERE `display_media`.displayId = :displayId
|
||
|
|
UNION ALL
|
||
|
|
SELECT 2 AS DownloadOrder,
|
||
|
|
`media`.storedAs AS path,
|
||
|
|
`media`.mediaID AS id,
|
||
|
|
`media`.`MD5`,
|
||
|
|
`media`.FileSize,
|
||
|
|
`media`.released
|
||
|
|
FROM `media`
|
||
|
|
INNER JOIN `lkmediadisplaygroup`
|
||
|
|
ON lkmediadisplaygroup.mediaid = media.MediaID
|
||
|
|
INNER JOIN `lkdgdg`
|
||
|
|
ON `lkdgdg`.parentId = `lkmediadisplaygroup`.displayGroupId
|
||
|
|
INNER JOIN `lkdisplaydg`
|
||
|
|
ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId
|
||
|
|
WHERE lkdisplaydg.DisplayID = :displayId
|
||
|
|
UNION ALL
|
||
|
|
SELECT 3 AS DownloadOrder,
|
||
|
|
`media`.storedAs AS path,
|
||
|
|
`media`.mediaID AS id,
|
||
|
|
`media`.`MD5`,
|
||
|
|
`media`.FileSize,
|
||
|
|
`media`.released
|
||
|
|
FROM region
|
||
|
|
INNER JOIN playlist
|
||
|
|
ON playlist.regionId = region.regionId
|
||
|
|
INNER JOIN lkplaylistplaylist
|
||
|
|
ON lkplaylistplaylist.parentId = playlist.playlistId
|
||
|
|
INNER JOIN widget
|
||
|
|
ON widget.playlistId = lkplaylistplaylist.childId
|
||
|
|
INNER JOIN lkwidgetmedia
|
||
|
|
ON widget.widgetId = lkwidgetmedia.widgetId
|
||
|
|
INNER JOIN media
|
||
|
|
ON media.mediaId = lkwidgetmedia.mediaId
|
||
|
|
WHERE region.layoutId IN (%s)
|
||
|
|
UNION ALL
|
||
|
|
SELECT 4 AS DownloadOrder,
|
||
|
|
`media`.storedAs AS path,
|
||
|
|
`media`.mediaId AS id,
|
||
|
|
`media`.`MD5`,
|
||
|
|
`media`.FileSize,
|
||
|
|
`media`.released
|
||
|
|
FROM `media`
|
||
|
|
WHERE `media`.mediaID IN (
|
||
|
|
SELECT backgroundImageId
|
||
|
|
FROM `layout`
|
||
|
|
WHERE layoutId IN (%s)
|
||
|
|
)
|
||
|
|
";
|
||
|
|
|
||
|
|
$params = ['displayId' => $this->display->displayId];
|
||
|
|
|
||
|
|
$SQL .= ' ORDER BY DownloadOrder ';
|
||
|
|
|
||
|
|
// Sub layoutId list
|
||
|
|
$SQL = sprintf($SQL, $layoutIdList, $layoutIdList);
|
||
|
|
|
||
|
|
$this->getLog()->sql($SQL, $params);
|
||
|
|
|
||
|
|
$sth = $dbh->prepare($SQL);
|
||
|
|
$sth->execute($params);
|
||
|
|
|
||
|
|
// Prepare a SQL statement in case we need to update the MD5 and FileSize on media nodes.
|
||
|
|
$mediaSth = $dbh->prepare('UPDATE media SET `MD5` = :md5, FileSize = :size WHERE MediaID = :mediaid');
|
||
|
|
|
||
|
|
// Keep a list of path names added to RF to prevent duplicates
|
||
|
|
$pathsAdded = [];
|
||
|
|
$cdnUrl = $this->configService->getSetting('CDN_URL');
|
||
|
|
|
||
|
|
foreach ($sth->fetchAll() as $row) {
|
||
|
|
$parsedRow = $this->getSanitizer($row);
|
||
|
|
// Media
|
||
|
|
$path = $parsedRow->getString('path');
|
||
|
|
$id = $parsedRow->getParam('id');
|
||
|
|
$md5 = $row['MD5'];
|
||
|
|
$fileSize = $parsedRow->getInt('FileSize');
|
||
|
|
$released = $parsedRow->getInt('released');
|
||
|
|
|
||
|
|
// Check we haven't added this before
|
||
|
|
if (in_array($path, $pathsAdded)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Do we need to calculate a new MD5?
|
||
|
|
// If they are empty calculate them and save them back to the media.
|
||
|
|
if ($md5 == '' || $fileSize == 0) {
|
||
|
|
$md5 = md5_file($libraryLocation . $path);
|
||
|
|
$fileSize = filesize($libraryLocation . $path);
|
||
|
|
|
||
|
|
// Update the media record with this information
|
||
|
|
$mediaSth->execute(['md5' => $md5, 'size' => $fileSize, 'mediaid' => $id]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add nonce
|
||
|
|
$mediaNonce = $this->requiredFileFactory
|
||
|
|
->createForMedia($this->display->displayId, $id, $fileSize, $path, $released)
|
||
|
|
->save();
|
||
|
|
|
||
|
|
// skip media which has released == 0 or 2
|
||
|
|
if ($released == 0 || $released == 2) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$newRfIds[] = $mediaNonce->rfId;
|
||
|
|
|
||
|
|
// Add the file node
|
||
|
|
$file = $requiredFilesXml->createElement('file');
|
||
|
|
$file->setAttribute('type', 'media');
|
||
|
|
$file->setAttribute('id', $id);
|
||
|
|
$file->setAttribute('size', $fileSize);
|
||
|
|
$file->setAttribute('md5', $md5);
|
||
|
|
|
||
|
|
if ($httpDownloads) {
|
||
|
|
// Serve a link instead (standard HTTP link)
|
||
|
|
$file->setAttribute('path', LinkSigner::generateSignedLink(
|
||
|
|
$this->display,
|
||
|
|
$this->configService->getApiKeyDetails()['encryptionKey'],
|
||
|
|
$cdnUrl,
|
||
|
|
'M',
|
||
|
|
$id,
|
||
|
|
$path,
|
||
|
|
));
|
||
|
|
$file->setAttribute('saveAs', $path);
|
||
|
|
$file->setAttribute('download', 'http');
|
||
|
|
} else {
|
||
|
|
$file->setAttribute('download', 'xmds');
|
||
|
|
$file->setAttribute('path', $path);
|
||
|
|
}
|
||
|
|
|
||
|
|
$fileElements->appendChild($file);
|
||
|
|
|
||
|
|
// Add to paths added
|
||
|
|
$pathsAdded[] = $path;
|
||
|
|
}
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$this->getLog()->error('Unable to get a list of required files. ' . $e->getMessage());
|
||
|
|
$this->getLog()->debug($e->getTraceAsString());
|
||
|
|
return new \SoapFault('Sender', 'Unable to get a list of files');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get an array of modules to use
|
||
|
|
$modules = $this->moduleFactory->getKeyedArrayOfModules();
|
||
|
|
|
||
|
|
// Reset the paths added array to start again with layouts
|
||
|
|
$pathsAdded = [];
|
||
|
|
|
||
|
|
$cdnUrl = $this->configService->getSetting('CDN_URL');
|
||
|
|
|
||
|
|
// Go through each layout and see if we need to supply any resource nodes.
|
||
|
|
foreach ($layouts as $layoutId) {
|
||
|
|
try {
|
||
|
|
// Check we haven't added this before
|
||
|
|
if (in_array($layoutId, $pathsAdded)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load this layout
|
||
|
|
$layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->loadById($layoutId));
|
||
|
|
try {
|
||
|
|
$layout->loadPlaylists();
|
||
|
|
|
||
|
|
// Make sure its XLF is up-to-date
|
||
|
|
$path = $layout->xlfToDisk(['notify' => false]);
|
||
|
|
} finally {
|
||
|
|
$this->layoutFactory->concurrentRequestRelease($layout);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the status is *still* 4, then we skip this layout as it cannot build
|
||
|
|
if ($layout->status === Status::$STATUS_INVALID) {
|
||
|
|
$this->getLog()->debug('Skipping layoutId ' . $layout->layoutId . ' which wont build');
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// For layouts the MD5 column is the layout xml
|
||
|
|
$fileSize = filesize($path);
|
||
|
|
$md5 = md5_file($path);
|
||
|
|
$fileName = basename($path);
|
||
|
|
|
||
|
|
// Log
|
||
|
|
$this->getLog()->debug('MD5 for layoutid ' . $layoutId . ' is: [' . $md5 . ']');
|
||
|
|
|
||
|
|
// Add nonce
|
||
|
|
$layoutNonce = $this->requiredFileFactory
|
||
|
|
->createForLayout($this->display->displayId, $layoutId, $fileSize, $fileName)
|
||
|
|
->save();
|
||
|
|
$newRfIds[] = $layoutNonce->rfId;
|
||
|
|
|
||
|
|
// Add the Layout file element
|
||
|
|
$file = $requiredFilesXml->createElement('file');
|
||
|
|
$file->setAttribute('type', 'layout');
|
||
|
|
$file->setAttribute('id', $layoutId);
|
||
|
|
$file->setAttribute('size', $fileSize);
|
||
|
|
$file->setAttribute('md5', $md5);
|
||
|
|
|
||
|
|
// add Layout code only if code identifier is set on the Layout.
|
||
|
|
if ($layout->code != null) {
|
||
|
|
$file->setAttribute('code', $layout->code);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Permissive check for http layouts - always allow unless windows and <= 120
|
||
|
|
$supportsHttpLayouts = !($this->display->clientType == 'windows' && $this->display->clientCode <= 120);
|
||
|
|
|
||
|
|
if ($httpDownloads && $supportsHttpLayouts) {
|
||
|
|
// Serve a link instead (standard HTTP link)
|
||
|
|
$file->setAttribute('path', LinkSigner::generateSignedLink(
|
||
|
|
$this->display,
|
||
|
|
$this->configService->getApiKeyDetails()['encryptionKey'],
|
||
|
|
$cdnUrl,
|
||
|
|
'L',
|
||
|
|
$layoutId,
|
||
|
|
$fileName,
|
||
|
|
));
|
||
|
|
$file->setAttribute('saveAs', $fileName);
|
||
|
|
$file->setAttribute('download', 'http');
|
||
|
|
} else {
|
||
|
|
$file->setAttribute('download', 'xmds');
|
||
|
|
$file->setAttribute('path', $layoutId);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the Layout Modified Date
|
||
|
|
// To cover for instances where modifiedDt isn't correctly recorded
|
||
|
|
// use the createdDt instead.
|
||
|
|
$layoutModifiedDt = Carbon::createFromFormat(
|
||
|
|
DateFormatHelper::getSystemFormat(),
|
||
|
|
$layout->modifiedDt ?? $layout->createdDt
|
||
|
|
);
|
||
|
|
|
||
|
|
// merge regions and drawers
|
||
|
|
/** @var Region[] $allRegions */
|
||
|
|
$allRegions = array_merge($layout->regions, $layout->drawers);
|
||
|
|
|
||
|
|
// Load the layout XML and work out if we have any ticker / text / dataset media items
|
||
|
|
// Append layout resources before layout, so they are downloaded first.
|
||
|
|
// If layouts are set to expire immediately, the new layout will use the old resources if
|
||
|
|
// the layout is downloaded first.
|
||
|
|
foreach ($allRegions as $region) {
|
||
|
|
$playlist = $region->getPlaylist();
|
||
|
|
$playlist->setModuleFactory($this->moduleFactory);
|
||
|
|
|
||
|
|
// Playlists might mean we include a widget more than once per region
|
||
|
|
// if so, we only want to download a single copy of its resource node
|
||
|
|
// if it is included in 2 regions - we most likely want a copy for each
|
||
|
|
$resourcesAdded = [];
|
||
|
|
|
||
|
|
foreach ($playlist->expandWidgets() as $widget) {
|
||
|
|
if (!array_key_exists($widget->type, $modules)) {
|
||
|
|
$this->getLog()->debug('Unknown type of widget: ' . $widget->type);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Any global widgets, html based widgets or region specific widgets
|
||
|
|
if ($widget->type === 'global'
|
||
|
|
|| $modules[$widget->type]->renderAs === 'html'
|
||
|
|
|| $modules[$widget->type]->regionSpecific == 1
|
||
|
|
) {
|
||
|
|
// If we've already parsed this widget in this region, then don't bother doing it again
|
||
|
|
// we will only generate the same details.
|
||
|
|
if (in_array($widget->widgetId, $resourcesAdded)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// We've added this widget already
|
||
|
|
$resourcesAdded[] = $widget->widgetId;
|
||
|
|
|
||
|
|
// Get the widget modified date
|
||
|
|
// we will use the latter of this vs the layout modified date as the updated attribute
|
||
|
|
// on required files
|
||
|
|
$widgetModifiedDt = Carbon::createFromTimestamp($widget->modifiedDt);
|
||
|
|
|
||
|
|
// Updated date is the greatest of layout/widget modified date
|
||
|
|
$updatedDt = ($layoutModifiedDt->greaterThan($widgetModifiedDt))
|
||
|
|
? $layoutModifiedDt
|
||
|
|
: $widgetModifiedDt;
|
||
|
|
|
||
|
|
// If this is a canvas region, then only send the data, unless we're the global
|
||
|
|
// widget
|
||
|
|
$isShouldSendHtml = $region->type !== 'canvas' || $widget->type === 'global';
|
||
|
|
if ($isShouldSendHtml) {
|
||
|
|
// Add the resource node to the XML for this widfget.
|
||
|
|
$getResourceRf = $this->requiredFileFactory
|
||
|
|
->createForGetResource($this->display->displayId, $widget->widgetId)
|
||
|
|
->save();
|
||
|
|
$newRfIds[] = $getResourceRf->rfId;
|
||
|
|
|
||
|
|
// Append this item to required files
|
||
|
|
$resourceFile = $requiredFilesXml->createElement('file');
|
||
|
|
$resourceFile->setAttribute('type', 'resource');
|
||
|
|
$resourceFile->setAttribute('id', $widget->widgetId);
|
||
|
|
$resourceFile->setAttribute('layoutid', $layoutId);
|
||
|
|
$resourceFile->setAttribute('regionid', $region->regionId);
|
||
|
|
$resourceFile->setAttribute('mediaid', $widget->widgetId);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the module
|
||
|
|
$dataModule = $modules[$widget->type];
|
||
|
|
|
||
|
|
// Does this also have an associated data file?
|
||
|
|
// we add this for < XMDS v7 as well, because the record is used by the widget sync task
|
||
|
|
// the player shouldn't receive it.
|
||
|
|
if ($dataModule->isDataProviderExpected()) {
|
||
|
|
// A node specifically for the widget data.
|
||
|
|
if ($isSupportsDataUrl) {
|
||
|
|
// Newer player (v4 onward), add widget node for returning data
|
||
|
|
$dataFile = $requiredFilesXml->createElement('file');
|
||
|
|
$dataFile->setAttribute('type', 'widget');
|
||
|
|
$dataFile->setAttribute('id', $widget->widgetId);
|
||
|
|
$dataFile->setAttribute(
|
||
|
|
'updateInterval',
|
||
|
|
$widget->getOptionValue(
|
||
|
|
'updateInterval',
|
||
|
|
$dataModule->getPropertyDefault('updateInterval') ?? 120,
|
||
|
|
)
|
||
|
|
);
|
||
|
|
$fileElements->appendChild($dataFile);
|
||
|
|
} else if ($isShouldSendHtml) {
|
||
|
|
// Older player, needs to change the updated date on the resource node
|
||
|
|
// Has our widget been updated recently?
|
||
|
|
// TODO: Does this need to be the most recent updated date for all the widgets in
|
||
|
|
// this region?
|
||
|
|
$dataProvider = $dataModule->createDataProvider($widget);
|
||
|
|
$dataProvider->setDisplayProperties(
|
||
|
|
$this->display->latitude ?: $this->getConfig()->getSetting('DEFAULT_LAT'),
|
||
|
|
$this->display->longitude ?: $this->getConfig()->getSetting('DEFAULT_LONG'),
|
||
|
|
$this->display->displayId
|
||
|
|
);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$widgetDataProviderCache = $this->moduleFactory
|
||
|
|
->createWidgetDataProviderCache();
|
||
|
|
$cacheKey = $this->moduleFactory->determineCacheKey(
|
||
|
|
$dataModule,
|
||
|
|
$widget,
|
||
|
|
$this->display->displayId,
|
||
|
|
$dataProvider,
|
||
|
|
$dataModule->getWidgetProviderOrNull()
|
||
|
|
);
|
||
|
|
|
||
|
|
// We do not pass a modifiedDt in here because we always expect to be cached
|
||
|
|
$cacheDt = $widgetDataProviderCache->getCacheDate($dataProvider, $cacheKey);
|
||
|
|
if ($cacheDt !== null) {
|
||
|
|
$updatedDt = ($cacheDt->greaterThan($updatedDt))
|
||
|
|
? $cacheDt
|
||
|
|
: $updatedDt;
|
||
|
|
}
|
||
|
|
} catch (\Exception) {
|
||
|
|
$this->getLog()->error(
|
||
|
|
'doRequiredFiles: Failed to get data cache modified date for widgetId '
|
||
|
|
. $widget->widgetId
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Used by WidgetSync to understand when to keep the cache warm
|
||
|
|
$getDataRf = $this->requiredFileFactory
|
||
|
|
->createForGetData($this->display->displayId, $widget->widgetId)
|
||
|
|
->save();
|
||
|
|
$newRfIds[] = $getDataRf->rfId;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($isShouldSendHtml) {
|
||
|
|
// Append our resource node.
|
||
|
|
$resourceFile->setAttribute('updated', $updatedDt->format('U'));
|
||
|
|
$fileElements->appendChild($resourceFile);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add any assets from this widget/template (unless assetId already added)
|
||
|
|
if (!in_array('module_' . $widget->type, $resourcesAdded)) {
|
||
|
|
foreach ($modules[$widget->type]->getAssets() as $asset) {
|
||
|
|
// Do not send assets if they are CMS only
|
||
|
|
if (!$asset->isSendToPlayer()) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$asset->updateAssetCache($libraryLocation);
|
||
|
|
|
||
|
|
// Add a new required file for this.
|
||
|
|
try {
|
||
|
|
$this->addDependency(
|
||
|
|
$newRfIds,
|
||
|
|
$requiredFilesXml,
|
||
|
|
$fileElements,
|
||
|
|
$httpDownloads,
|
||
|
|
$isSupportsDependency,
|
||
|
|
$asset->getDependency()
|
||
|
|
);
|
||
|
|
} catch (\Exception $exception) {
|
||
|
|
$this->getLog()->error('Invalid asset: ' . $exception->getMessage());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$resourcesAdded[] = 'module_' . $widget->type;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Templates (there should only be one)
|
||
|
|
if ($modules[$widget->type]->isTemplateExpected()) {
|
||
|
|
$templateId = $widget->getOptionValue('templateId', null);
|
||
|
|
if ($templateId !== null && !in_array('template_' . $templateId, $resourcesAdded)) {
|
||
|
|
// Get this template and its assets
|
||
|
|
$templates = $this->widgetFactory->getTemplatesForWidgets(
|
||
|
|
$modules[$widget->type],
|
||
|
|
[$widget]
|
||
|
|
);
|
||
|
|
|
||
|
|
foreach ($templates as $template) {
|
||
|
|
foreach ($template->getAssets() as $asset) {
|
||
|
|
// Do not send assets if they are CMS only
|
||
|
|
if (!$asset->isSendToPlayer()) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$asset->updateAssetCache($libraryLocation);
|
||
|
|
|
||
|
|
// Add a new required file for this.
|
||
|
|
try {
|
||
|
|
$this->addDependency(
|
||
|
|
$newRfIds,
|
||
|
|
$requiredFilesXml,
|
||
|
|
$fileElements,
|
||
|
|
$httpDownloads,
|
||
|
|
$isSupportsDependency,
|
||
|
|
$asset->getDependency()
|
||
|
|
);
|
||
|
|
} catch (\Exception $exception) {
|
||
|
|
$this->getLog()->error('Invalid asset: ' . $exception->getMessage());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$resourcesAdded[] = 'template_' . $templateId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Append Layout
|
||
|
|
$fileElements->appendChild($file);
|
||
|
|
|
||
|
|
// Add to paths added
|
||
|
|
$pathsAdded[] = $layoutId;
|
||
|
|
} catch (NotFoundException) {
|
||
|
|
$this->getLog()->error('Layout not found - ID: ' . $layoutId . ', skipping');
|
||
|
|
continue;
|
||
|
|
} catch (GeneralException $e) {
|
||
|
|
$this->getLog()->error('Cannot generate layout - ID: ' . $layoutId
|
||
|
|
. ', skipping, e = ' . $e->getMessage());
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add Purge List node
|
||
|
|
$purgeList = $requiredFilesXml->createElement('purge');
|
||
|
|
$fileElements->appendChild($purgeList);
|
||
|
|
|
||
|
|
try {
|
||
|
|
$dbh = $this->getStore()->getConnection();
|
||
|
|
|
||
|
|
// get list of mediaId/storedAs that should be purged from the Player storage
|
||
|
|
// records in that table older than provided expiryDate, should be removed by the task
|
||
|
|
$sth = $dbh->prepare('SELECT mediaId, storedAs FROM purge_list');
|
||
|
|
$sth->execute();
|
||
|
|
|
||
|
|
// Add a purge list item for each file
|
||
|
|
foreach ($sth->fetchAll() as $row) {
|
||
|
|
$item = $requiredFilesXml->createElement('item');
|
||
|
|
$item->setAttribute('id', $row['mediaId']);
|
||
|
|
$item->setAttribute('storedAs', $row['storedAs']);
|
||
|
|
|
||
|
|
$purgeList->appendChild($item);
|
||
|
|
}
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$this->getLog()->error('Unable to get a list of purge_list files. ' . $e->getMessage());
|
||
|
|
return new \SoapFault('Sender', 'Unable to get purge list files');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug($requiredFilesXml->saveXML());
|
||
|
|
|
||
|
|
// Return the results of requiredFiles()
|
||
|
|
$requiredFilesXml->formatOutput = true;
|
||
|
|
$output = $requiredFilesXml->saveXML();
|
||
|
|
|
||
|
|
// Cache
|
||
|
|
$cache->set($output);
|
||
|
|
|
||
|
|
// RF cache expires every 4 hours
|
||
|
|
$cache->expiresAfter(3600*4);
|
||
|
|
$this->getPool()->saveDeferred($cache);
|
||
|
|
|
||
|
|
// Remove any required files that remain in the array of rfIds
|
||
|
|
$rfIds = array_values(array_diff($rfIds, $newRfIds));
|
||
|
|
if (count($rfIds) > 0) {
|
||
|
|
$this->getLog()->debug('Removing ' . count($rfIds) . ' from requiredfiles');
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Execute this on the default connection
|
||
|
|
$this->getStore()->updateWithDeadlockLoop(
|
||
|
|
'DELETE FROM `requiredfile` WHERE rfId IN ('
|
||
|
|
. implode(',', array_fill(0, count($rfIds), '?')) . ')',
|
||
|
|
$rfIds
|
||
|
|
);
|
||
|
|
} catch (DeadlockException $deadlockException) {
|
||
|
|
$this->getLog()->error('Deadlock when deleting required files - ignoring and continuing with request');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set any remaining required files to have 0 bytes requested (as we've generated a new nonce)
|
||
|
|
$this->getStore()->update('
|
||
|
|
UPDATE `requiredfile`
|
||
|
|
SET `bytesRequested` = 0
|
||
|
|
WHERE `displayId` = :displayId
|
||
|
|
AND `type` NOT IN (\'W\', \'D\')
|
||
|
|
', [
|
||
|
|
'displayId' => $this->display->displayId
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Log Bandwidth
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$RF, strlen($output));
|
||
|
|
|
||
|
|
return $output;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param $serverKey
|
||
|
|
* @param $hardwareKey
|
||
|
|
* @param array $options
|
||
|
|
* @return mixed
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \SoapFault
|
||
|
|
*/
|
||
|
|
protected function doSchedule($serverKey, $hardwareKey, $options = [])
|
||
|
|
{
|
||
|
|
$this->logProcessor->setRoute('Schedule');
|
||
|
|
$sanitizer = $this->getSanitizer([
|
||
|
|
'serverKey' => $serverKey,
|
||
|
|
'hardwareKey' => $hardwareKey
|
||
|
|
]);
|
||
|
|
$options = array_merge(['dependentsAsNodes' => false, 'includeOverlays' => false], $options);
|
||
|
|
|
||
|
|
// Sanitize
|
||
|
|
$serverKey = $sanitizer->getString('serverKey');
|
||
|
|
$hardwareKey = $sanitizer->getString('hardwareKey');
|
||
|
|
|
||
|
|
// Check the serverKey matches
|
||
|
|
if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) {
|
||
|
|
throw new \SoapFault(
|
||
|
|
'Sender',
|
||
|
|
'The Server key you entered does not match with the server key at this address'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// auth this request...
|
||
|
|
if (!$this->authDisplay($hardwareKey)) {
|
||
|
|
throw new \SoapFault('Sender', 'This Display is not authorised.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now that we authenticated the Display, make sure we are sticking to our bandwidth limit
|
||
|
|
if (!$this->checkBandwidth($this->display->displayId)) {
|
||
|
|
throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the cache
|
||
|
|
$cache = $this->getPool()->getItem($this->display->getCacheKey() . '/schedule');
|
||
|
|
$cache->setInvalidationMethod(Invalidation::OLD);
|
||
|
|
|
||
|
|
$output = $cache->get();
|
||
|
|
|
||
|
|
if ($cache->isHit()) {
|
||
|
|
$this->getLog()->info(sprintf(
|
||
|
|
'Returning Schedule from Cache for display %s. Options %s.',
|
||
|
|
$this->display->display,
|
||
|
|
json_encode($options)
|
||
|
|
));
|
||
|
|
|
||
|
|
// Log Bandwidth
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$SCHEDULE, strlen($output));
|
||
|
|
|
||
|
|
return $output;
|
||
|
|
}
|
||
|
|
|
||
|
|
// We need to regenerate
|
||
|
|
// Lock the cache
|
||
|
|
$cache->lock(120);
|
||
|
|
|
||
|
|
// Generate the Schedule XML
|
||
|
|
$scheduleXml = new \DOMDocument('1.0');
|
||
|
|
$layoutElements = $scheduleXml->createElement('schedule');
|
||
|
|
$scheduleXml->appendChild($layoutElements);
|
||
|
|
|
||
|
|
// Filter criteria
|
||
|
|
$this->setDateFilters();
|
||
|
|
|
||
|
|
// Add the filter dates to the RF xml document
|
||
|
|
$layoutElements->setAttribute('generated', Carbon::now()->format(DateFormatHelper::getSystemFormat()));
|
||
|
|
$layoutElements->setAttribute('filterFrom', $this->fromFilter->format(DateFormatHelper::getSystemFormat()));
|
||
|
|
$layoutElements->setAttribute('filterTo', $this->toFilter->format(DateFormatHelper::getSystemFormat()));
|
||
|
|
|
||
|
|
// Default Layout
|
||
|
|
$defaultLayoutId = ($this->display->defaultLayoutId === null || $this->display->defaultLayoutId === 0)
|
||
|
|
? intval($this->getConfig()->getSetting('DEFAULT_LAYOUT', 0))
|
||
|
|
: $this->display->defaultLayoutId;
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Dependencies
|
||
|
|
// ------------
|
||
|
|
$moduleDependents = [];
|
||
|
|
$dependencyListEvent = new XmdsDependencyListEvent($this->display);
|
||
|
|
$this->getDispatcher()->dispatch($dependencyListEvent, 'xmds.dependency.list');
|
||
|
|
|
||
|
|
// Add each resolved dependency to our list of global dependents.
|
||
|
|
foreach ($dependencyListEvent->getDependencies() as $dependency) {
|
||
|
|
$moduleDependents[] = basename($dependency->path);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add file nodes to the $fileElements
|
||
|
|
// Firstly get all the scheduled layouts
|
||
|
|
$events = $this->scheduleFactory->getForXmds(
|
||
|
|
$this->display->displayId,
|
||
|
|
$this->fromFilter,
|
||
|
|
$this->toFilter,
|
||
|
|
$options
|
||
|
|
);
|
||
|
|
|
||
|
|
// If our dependents are nodes, then build a list of layouts we can use to query for nodes
|
||
|
|
$layoutDependents = [];
|
||
|
|
|
||
|
|
// Layouts
|
||
|
|
$layoutIds = [];
|
||
|
|
|
||
|
|
// Add the default layout if it isn't empty.
|
||
|
|
if ($defaultLayoutId !== 0) {
|
||
|
|
$layoutIds[] = $defaultLayoutId;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Preparse events
|
||
|
|
foreach ($events as $event) {
|
||
|
|
$layoutId = ($event['eventTypeId'] == Schedule::$SYNC_EVENT) ? $event['syncLayoutId'] : $event['layoutId'];
|
||
|
|
if (!empty($layoutId) && !in_array($layoutId, $layoutIds)) {
|
||
|
|
$layoutIds[] = $layoutId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$SQL = '
|
||
|
|
SELECT DISTINCT `region`.layoutId, `media`.storedAs
|
||
|
|
FROM region
|
||
|
|
INNER JOIN playlist
|
||
|
|
ON playlist.regionId = region.regionId
|
||
|
|
INNER JOIN lkplaylistplaylist
|
||
|
|
ON lkplaylistplaylist.parentId = playlist.playlistId
|
||
|
|
INNER JOIN widget
|
||
|
|
ON widget.playlistId = lkplaylistplaylist.childId
|
||
|
|
INNER JOIN lkwidgetmedia
|
||
|
|
ON widget.widgetId = lkwidgetmedia.widgetId
|
||
|
|
INNER JOIN media
|
||
|
|
ON media.mediaId = lkwidgetmedia.mediaId
|
||
|
|
WHERE region.layoutId IN (' . implode(',', $layoutIds) . ')
|
||
|
|
AND media.type <> \'module\'
|
||
|
|
';
|
||
|
|
|
||
|
|
foreach ($this->getStore()->select($SQL, []) as $row) {
|
||
|
|
if (!array_key_exists($row['layoutId'], $layoutDependents))
|
||
|
|
$layoutDependents[$row['layoutId']] = [];
|
||
|
|
|
||
|
|
$layoutDependents[$row['layoutId']][] = $row['storedAs'];
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug(sprintf('Resolved dependents for Schedule: %s.', json_encode($layoutDependents, JSON_PRETTY_PRINT)));
|
||
|
|
|
||
|
|
// Additional nodes.
|
||
|
|
$overlayNodes = null;
|
||
|
|
$actionNodes = null;
|
||
|
|
$dataConnectorNodes = null;
|
||
|
|
|
||
|
|
// We must have some results in here by this point
|
||
|
|
foreach ($events as $row) {
|
||
|
|
$parsedRow = $this->getSanitizer($row);
|
||
|
|
$schedule = $this->scheduleFactory->createEmpty()->hydrate($row);
|
||
|
|
|
||
|
|
// Is this scheduled event a synchronised timezone?
|
||
|
|
// if it is, then we get our events with respect to the timezone of the display
|
||
|
|
$isSyncTimezone = ($schedule->syncTimezone == 1 && !empty($this->display->timeZone));
|
||
|
|
|
||
|
|
try {
|
||
|
|
if ($isSyncTimezone) {
|
||
|
|
$scheduleEvents = $schedule->getEvents($this->localFromFilter, $this->localToFilter);
|
||
|
|
} else {
|
||
|
|
$scheduleEvents = $schedule->getEvents($this->fromFilter, $this->toFilter);
|
||
|
|
}
|
||
|
|
} catch (GeneralException $e) {
|
||
|
|
$this->getLog()->error('Unable to getEvents for ' . $schedule->eventId);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug(count($scheduleEvents) . ' events for eventId ' . $schedule->eventId);
|
||
|
|
|
||
|
|
// Load the whole schedule object if we have some events attached
|
||
|
|
if (count($scheduleEvents) > 0) {
|
||
|
|
$schedule->load(['loadDisplayGroups' => false]);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($scheduleEvents as $scheduleEvent) {
|
||
|
|
$eventTypeId = $row['eventTypeId'];
|
||
|
|
|
||
|
|
if ($row['eventTypeId'] == Schedule::$SYNC_EVENT) {
|
||
|
|
$layoutId = $row['syncLayoutId'];
|
||
|
|
$status = intval($row['syncLayoutStatus']);
|
||
|
|
$duration = $row['syncLayoutDuration'];
|
||
|
|
} else {
|
||
|
|
$layoutId = $row['layoutId'];
|
||
|
|
$status = intval($row['status']);
|
||
|
|
$duration = $row['duration'];
|
||
|
|
}
|
||
|
|
|
||
|
|
$commandCode = $row['code'];
|
||
|
|
|
||
|
|
// Handle the from/to date of the events we have been returned (they are all returned with respect to
|
||
|
|
// the current CMS timezone)
|
||
|
|
// Does the Display have a timezone?
|
||
|
|
if ($isSyncTimezone) {
|
||
|
|
$fromDt = Carbon::createFromTimestamp($scheduleEvent->fromDt, $this->display->timeZone)->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$toDt = Carbon::createFromTimestamp($scheduleEvent->toDt, $this->display->timeZone)->format(DateFormatHelper::getSystemFormat());
|
||
|
|
} else {
|
||
|
|
$fromDt = Carbon::createFromTimestamp($scheduleEvent->fromDt)->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$toDt = Carbon::createFromTimestamp($scheduleEvent->toDt)->format(DateFormatHelper::getSystemFormat());
|
||
|
|
}
|
||
|
|
|
||
|
|
$scheduleId = $row['eventId'];
|
||
|
|
$is_priority = $parsedRow->getInt('isPriority');
|
||
|
|
|
||
|
|
// Criteria
|
||
|
|
$criteriaNodes = [];
|
||
|
|
foreach ($schedule->criteria as $scheduleCriteria) {
|
||
|
|
$criteriaNode = $scheduleXml->createElement('criteria');
|
||
|
|
$criteriaNode->setAttribute('metric', $scheduleCriteria->metric);
|
||
|
|
$criteriaNode->setAttribute('condition', $scheduleCriteria->condition);
|
||
|
|
$criteriaNode->setAttribute('type', $scheduleCriteria->type);
|
||
|
|
$criteriaNode->textContent = $scheduleCriteria->value;
|
||
|
|
$criteriaNodes[] = $criteriaNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle event type
|
||
|
|
if ($eventTypeId == Schedule::$LAYOUT_EVENT ||
|
||
|
|
$eventTypeId == Schedule::$INTERRUPT_EVENT ||
|
||
|
|
$eventTypeId == Schedule::$CAMPAIGN_EVENT ||
|
||
|
|
$eventTypeId == Schedule::$MEDIA_EVENT ||
|
||
|
|
$eventTypeId == Schedule::$PLAYLIST_EVENT ||
|
||
|
|
$eventTypeId == Schedule::$SYNC_EVENT
|
||
|
|
) {
|
||
|
|
// Ensure we have a layoutId (we may not if an empty campaign is assigned)
|
||
|
|
// https://github.com/xibosignage/xibo/issues/894
|
||
|
|
if ($layoutId == 0 || empty($layoutId)) {
|
||
|
|
$this->getLog()->info(sprintf('Player has empty event scheduled. Display = %s, EventId = %d', $this->display->display, $scheduleId));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the layout status
|
||
|
|
// https://github.com/xibosignage/xibo/issues/743
|
||
|
|
if ($status > 3) {
|
||
|
|
$this->getLog()->info(sprintf('Player has invalid layout scheduled. Display = %s, LayoutId = %d', $this->display->display, $layoutId));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add a layout node to the schedule
|
||
|
|
$layout = $scheduleXml->createElement('layout');
|
||
|
|
$layout->setAttribute('file', $layoutId);
|
||
|
|
$layout->setAttribute('fromdt', $fromDt);
|
||
|
|
$layout->setAttribute('todt', $toDt);
|
||
|
|
$layout->setAttribute('scheduleid', $scheduleId);
|
||
|
|
$layout->setAttribute('priority', $is_priority);
|
||
|
|
$layout->setAttribute('syncEvent', ($row['eventTypeId'] == Schedule::$SYNC_EVENT) ? 1 : 0);
|
||
|
|
$layout->setAttribute('shareOfVoice', $row['shareOfVoice'] ?? 0);
|
||
|
|
$layout->setAttribute('duration', $duration ?? 0);
|
||
|
|
$layout->setAttribute('isGeoAware', $row['isGeoAware'] ?? 0);
|
||
|
|
$layout->setAttribute('geoLocation', $row['geoLocation'] ?? null);
|
||
|
|
$layout->setAttribute('cyclePlayback', $row['cyclePlayback'] ?? 0);
|
||
|
|
$layout->setAttribute('groupKey', $row['groupKey'] ?? 0);
|
||
|
|
$layout->setAttribute('playCount', $row['playCount'] ?? 0);
|
||
|
|
$layout->setAttribute('maxPlaysPerHour', $row['maxPlaysPerHour'] ?? 0);
|
||
|
|
|
||
|
|
// Handle dependents
|
||
|
|
if (array_key_exists($layoutId, $layoutDependents)) {
|
||
|
|
if ($options['dependentsAsNodes']) {
|
||
|
|
// Add the dependents to the layout as new nodes
|
||
|
|
$dependentNode = $scheduleXml->createElement('dependents');
|
||
|
|
|
||
|
|
foreach ($layoutDependents[$layoutId] as $storedAs) {
|
||
|
|
$fileNode = $scheduleXml->createElement('file', $storedAs);
|
||
|
|
|
||
|
|
$dependentNode->appendChild($fileNode);
|
||
|
|
}
|
||
|
|
|
||
|
|
$layout->appendChild($dependentNode);
|
||
|
|
} else {
|
||
|
|
// Add the dependents to the layout as an attribute
|
||
|
|
$layout->setAttribute('dependents', implode(',', $layoutDependents[$layoutId]));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add criteria notes
|
||
|
|
if (count($criteriaNodes) > 0) {
|
||
|
|
foreach ($criteriaNodes as $criteriaNode) {
|
||
|
|
$layout->appendChild($criteriaNode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$layoutElements->appendChild($layout);
|
||
|
|
} elseif ($eventTypeId == Schedule::$COMMAND_EVENT) {
|
||
|
|
// Add a command node to the schedule
|
||
|
|
$command = $scheduleXml->createElement('command');
|
||
|
|
$command->setAttribute('date', $fromDt);
|
||
|
|
$command->setAttribute('scheduleid', $scheduleId);
|
||
|
|
$command->setAttribute('code', $commandCode);
|
||
|
|
|
||
|
|
// Add criteria notes
|
||
|
|
if (count($criteriaNodes) > 0) {
|
||
|
|
foreach ($criteriaNodes as $criteriaNode) {
|
||
|
|
$command->appendChild($criteriaNode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$layoutElements->appendChild($command);
|
||
|
|
} elseif ($eventTypeId == Schedule::$OVERLAY_EVENT && $options['includeOverlays']) {
|
||
|
|
// Ensure we have a layoutId (we may not if an empty campaign is assigned)
|
||
|
|
// https://github.com/xibosignage/xibo/issues/894
|
||
|
|
if ($layoutId == 0 || empty($layoutId)) {
|
||
|
|
$this->getLog()->error(sprintf('Player has empty event scheduled. Display = %s, EventId = %d', $this->display->display, $scheduleId));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the layout status
|
||
|
|
// https://github.com/xibosignage/xibo/issues/743
|
||
|
|
if (intval($row['status']) > 3) {
|
||
|
|
$this->getLog()->error(sprintf('Player has invalid layout scheduled. Display = %s, LayoutId = %d', $this->display->display, $layoutId));
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($overlayNodes == null) {
|
||
|
|
$overlayNodes = $scheduleXml->createElement('overlays');
|
||
|
|
}
|
||
|
|
|
||
|
|
$overlay = $scheduleXml->createElement('overlay');
|
||
|
|
$overlay->setAttribute('file', $layoutId);
|
||
|
|
$overlay->setAttribute('fromdt', $fromDt);
|
||
|
|
$overlay->setAttribute('todt', $toDt);
|
||
|
|
$overlay->setAttribute('scheduleid', $scheduleId);
|
||
|
|
$overlay->setAttribute('priority', $is_priority);
|
||
|
|
$overlay->setAttribute('duration', $row['duration'] ?? 0);
|
||
|
|
$overlay->setAttribute('isGeoAware', $row['isGeoAware'] ?? 0);
|
||
|
|
$overlay->setAttribute('geoLocation', $row['geoLocation'] ?? null);
|
||
|
|
|
||
|
|
// Add criteria notes
|
||
|
|
if (count($criteriaNodes) > 0) {
|
||
|
|
foreach ($criteriaNodes as $criteriaNode) {
|
||
|
|
$overlay->appendChild($criteriaNode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add to the overlays node list
|
||
|
|
$overlayNodes->appendChild($overlay);
|
||
|
|
} elseif ($eventTypeId == Schedule::$ACTION_EVENT) {
|
||
|
|
if ($actionNodes == null) {
|
||
|
|
$actionNodes = $scheduleXml->createElement('actions');
|
||
|
|
}
|
||
|
|
$action = $scheduleXml->createElement('action');
|
||
|
|
$action->setAttribute('fromdt', $fromDt);
|
||
|
|
$action->setAttribute('todt', $toDt);
|
||
|
|
$action->setAttribute('scheduleid', $scheduleId);
|
||
|
|
$action->setAttribute('priority', $is_priority);
|
||
|
|
$action->setAttribute('duration', $row['duration'] ?? 0);
|
||
|
|
$action->setAttribute('isGeoAware', $row['isGeoAware'] ?? 0);
|
||
|
|
$action->setAttribute('geoLocation', $row['geoLocation'] ?? null);
|
||
|
|
$action->setAttribute('triggerCode', $row['actionTriggerCode']);
|
||
|
|
$action->setAttribute('actionType', $row['actionType']);
|
||
|
|
$action->setAttribute('layoutCode', $row['actionLayoutCode']);
|
||
|
|
$action->setAttribute('commandCode', $commandCode);
|
||
|
|
|
||
|
|
// Add criteria notes
|
||
|
|
if (count($criteriaNodes) > 0) {
|
||
|
|
foreach ($criteriaNodes as $criteriaNode) {
|
||
|
|
$action->appendChild($criteriaNode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$actionNodes->appendChild($action);
|
||
|
|
} else if ($eventTypeId === Schedule::$DATA_CONNECTOR_EVENT) {
|
||
|
|
if ($dataConnectorNodes == null) {
|
||
|
|
$dataConnectorNodes = $scheduleXml->createElement('dataConnectors');
|
||
|
|
}
|
||
|
|
|
||
|
|
$dataConnector = $scheduleXml->createElement('connector');
|
||
|
|
$dataConnector->setAttribute('fromdt', $fromDt);
|
||
|
|
$dataConnector->setAttribute('todt', $toDt);
|
||
|
|
$dataConnector->setAttribute('scheduleid', $scheduleId);
|
||
|
|
$dataConnector->setAttribute('priority', $is_priority);
|
||
|
|
$dataConnector->setAttribute('duration', $row['duration'] ?? 0);
|
||
|
|
$dataConnector->setAttribute('isGeoAware', $row['isGeoAware'] ?? 0);
|
||
|
|
$dataConnector->setAttribute('geoLocation', $row['geoLocation'] ?? null);
|
||
|
|
$dataConnector->setAttribute('dataSetId', $row['dataSetId']);
|
||
|
|
$dataConnector->setAttribute('dataParams', urlencode($row['dataSetParams']));
|
||
|
|
$dataConnector->setAttribute('js', 'dataSet_' . $row['dataSetId'] . '.js');
|
||
|
|
|
||
|
|
// Add criteria notes
|
||
|
|
if (count($criteriaNodes) > 0) {
|
||
|
|
foreach ($criteriaNodes as $criteriaNode) {
|
||
|
|
$dataConnector->appendChild($criteriaNode);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$dataConnectorNodes->appendChild($dataConnector);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add the overlay nodes if we had any
|
||
|
|
if ($overlayNodes != null) {
|
||
|
|
$layoutElements->appendChild($overlayNodes);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add Actions nodes if we had any
|
||
|
|
if ($actionNodes != null) {
|
||
|
|
$layoutElements->appendChild($actionNodes);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add Data Connector nodes if we had any
|
||
|
|
if ($dataConnectorNodes != null) {
|
||
|
|
$layoutElements->appendChild($dataConnectorNodes);
|
||
|
|
}
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$this->getLog()->error('Error getting the schedule. ' . $e->getMessage());
|
||
|
|
return new \SoapFault('Sender', 'Unable to get the schedule');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Default Layout
|
||
|
|
try {
|
||
|
|
// is it valid?
|
||
|
|
$defaultLayout = $this->layoutFactory->getById($defaultLayoutId);
|
||
|
|
|
||
|
|
if ($defaultLayout->status >= Status::$STATUS_INVALID) {
|
||
|
|
$this->getLog()->error(sprintf('Player has invalid default Layout. Display = %s, LayoutId = %d',
|
||
|
|
$this->display->display,
|
||
|
|
$defaultLayout->layoutId));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Are we interleaving the default? And is the default valid?
|
||
|
|
if ($this->display->incSchedule == 1 && $defaultLayout->status < Status::$STATUS_INVALID) {
|
||
|
|
// Add as a node at the end of the schedule.
|
||
|
|
$layout = $scheduleXml->createElement("layout");
|
||
|
|
|
||
|
|
$layout->setAttribute("file", $defaultLayoutId);
|
||
|
|
$layout->setAttribute("fromdt", '2000-01-01 00:00:00');
|
||
|
|
$layout->setAttribute("todt", '2030-01-19 00:00:00');
|
||
|
|
$layout->setAttribute("scheduleid", 0);
|
||
|
|
$layout->setAttribute("priority", 0);
|
||
|
|
$layout->setAttribute('duration', $defaultLayout->duration);
|
||
|
|
|
||
|
|
if ($options['dependentsAsNodes'] && array_key_exists($defaultLayoutId, $layoutDependents)) {
|
||
|
|
$dependentNode = $scheduleXml->createElement("dependents");
|
||
|
|
|
||
|
|
foreach ($layoutDependents[$defaultLayoutId] as $storedAs) {
|
||
|
|
$fileNode = $scheduleXml->createElement("file", $storedAs);
|
||
|
|
|
||
|
|
$dependentNode->appendChild($fileNode);
|
||
|
|
}
|
||
|
|
|
||
|
|
$layout->appendChild($dependentNode);
|
||
|
|
}
|
||
|
|
|
||
|
|
$layoutElements->appendChild($layout);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add on the default layout node
|
||
|
|
$default = $scheduleXml->createElement("default");
|
||
|
|
$default->setAttribute("file", $defaultLayoutId);
|
||
|
|
$default->setAttribute('duration', $defaultLayout->duration);
|
||
|
|
|
||
|
|
if ($options['dependentsAsNodes'] && array_key_exists($defaultLayoutId, $layoutDependents)) {
|
||
|
|
$dependentNode = $scheduleXml->createElement("dependents");
|
||
|
|
|
||
|
|
foreach ($layoutDependents[$defaultLayoutId] as $storedAs) {
|
||
|
|
$fileNode = $scheduleXml->createElement("file", $storedAs);
|
||
|
|
|
||
|
|
$dependentNode->appendChild($fileNode);
|
||
|
|
}
|
||
|
|
|
||
|
|
$default->appendChild($dependentNode);
|
||
|
|
}
|
||
|
|
|
||
|
|
$layoutElements->appendChild($default);
|
||
|
|
} catch (\Exception $exception) {
|
||
|
|
$this->getLog()->error('Default Layout Invalid: ' . $exception->getMessage());
|
||
|
|
|
||
|
|
// Add the splash screen on as the default layout (ID 0)
|
||
|
|
$default = $scheduleXml->createElement('default');
|
||
|
|
$default->setAttribute('file', 0);
|
||
|
|
$layoutElements->appendChild($default);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add on a list of global dependants
|
||
|
|
$globalDependents = $scheduleXml->createElement('dependants');
|
||
|
|
|
||
|
|
foreach ($moduleDependents as $dep) {
|
||
|
|
$dependent = $scheduleXml->createElement('file', $dep);
|
||
|
|
$globalDependents->appendChild($dependent);
|
||
|
|
}
|
||
|
|
$layoutElements->appendChild($globalDependents);
|
||
|
|
|
||
|
|
// Format the output
|
||
|
|
$scheduleXml->formatOutput = true;
|
||
|
|
|
||
|
|
$this->getLog()->debug($scheduleXml->saveXML());
|
||
|
|
|
||
|
|
$output = $scheduleXml->saveXML();
|
||
|
|
|
||
|
|
// Cache
|
||
|
|
$cache->set($output);
|
||
|
|
$cache->expiresAt($this->toFilter);
|
||
|
|
$this->getPool()->saveDeferred($cache);
|
||
|
|
|
||
|
|
// Log Bandwidth
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$SCHEDULE, strlen($output));
|
||
|
|
|
||
|
|
return $output;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param $serverKey
|
||
|
|
* @param $hardwareKey
|
||
|
|
* @param $mediaId
|
||
|
|
* @param $type
|
||
|
|
* @param $reason
|
||
|
|
* @return bool|\SoapFault
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \SoapFault
|
||
|
|
*/
|
||
|
|
protected function doBlackList($serverKey, $hardwareKey, $mediaId, $type, $reason)
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param $serverKey
|
||
|
|
* @param $hardwareKey
|
||
|
|
* @param $logXml
|
||
|
|
* @return bool
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \SoapFault
|
||
|
|
*/
|
||
|
|
protected function doSubmitLog($serverKey, $hardwareKey, $logXml)
|
||
|
|
{
|
||
|
|
$this->logProcessor->setRoute('SubmitLog');
|
||
|
|
|
||
|
|
// Sanitize
|
||
|
|
$sanitizer = $this->getSanitizer([
|
||
|
|
'serverKey' => $serverKey,
|
||
|
|
'hardwareKey' => $hardwareKey
|
||
|
|
]);
|
||
|
|
|
||
|
|
$serverKey = $sanitizer->getString('serverKey');
|
||
|
|
$hardwareKey = $sanitizer->getString('hardwareKey');
|
||
|
|
|
||
|
|
// Check the serverKey matches
|
||
|
|
if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) {
|
||
|
|
throw new \SoapFault(
|
||
|
|
'Sender',
|
||
|
|
'The Server key you entered does not match with the server key at this address'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auth this request...
|
||
|
|
if (!$this->authDisplay($hardwareKey)) {
|
||
|
|
throw new \SoapFault('Sender', 'This Display is not authorised.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now that we authenticated the Display, make sure we are sticking to our bandwidth limit
|
||
|
|
if (!$this->checkBandwidth($this->display->displayId)) {
|
||
|
|
throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load the XML into a DOMDocument
|
||
|
|
$document = new \DOMDocument('1.0');
|
||
|
|
|
||
|
|
if (!$document->loadXML($logXml)) {
|
||
|
|
$this->getLog()->error(
|
||
|
|
'Malformed XML from Player, this will be discarded. The Raw XML String provided is: ' . $logXml
|
||
|
|
);
|
||
|
|
$this->getLog()->debug('XML log: ' . $logXml);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Current log level
|
||
|
|
$logLevel = \Xibo\Service\LogService::resolveLogLevel($this->display->getLogLevel());
|
||
|
|
$discardedLogs = 0;
|
||
|
|
|
||
|
|
// Store processed logs in an array
|
||
|
|
$logs = [];
|
||
|
|
|
||
|
|
foreach ($document->documentElement->childNodes as $node) {
|
||
|
|
/* @var \DOMElement $node */
|
||
|
|
// Make sure we don't consider any text nodes
|
||
|
|
if ($node->nodeType == XML_TEXT_NODE) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
// Zero out the common vars
|
||
|
|
$scheduleId = '';
|
||
|
|
$layoutId = '';
|
||
|
|
$mediaId = '';
|
||
|
|
$method = '';
|
||
|
|
$thread = '';
|
||
|
|
$type = '';
|
||
|
|
|
||
|
|
// This will be a bunch of trace nodes
|
||
|
|
$message = $node->textContent;
|
||
|
|
|
||
|
|
// Each element should have a category and a date
|
||
|
|
$date = $node->getAttribute('date');
|
||
|
|
$cat = strtolower($node->getAttribute('category'));
|
||
|
|
|
||
|
|
if ($date == '' || $cat == '') {
|
||
|
|
$this->getLog()->error('Log submitted without a date or category attribute');
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// special handling for event
|
||
|
|
// this will create record in displayevent table
|
||
|
|
// and is not added to the logs.
|
||
|
|
if ($cat == 'event') {
|
||
|
|
$this->createDisplayAlert($node);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Does this meet the current log level?
|
||
|
|
if ($cat == 'error') {
|
||
|
|
$recordLogLevel = Logger::ERROR;
|
||
|
|
$levelName = 'ERROR';
|
||
|
|
} else if ($cat == 'audit' || $cat == 'trace') {
|
||
|
|
$recordLogLevel = Logger::DEBUG;
|
||
|
|
$levelName = 'DEBUG';
|
||
|
|
} else if ($cat == 'debug') {
|
||
|
|
$recordLogLevel = Logger::INFO;
|
||
|
|
$levelName = 'INFO';
|
||
|
|
} else {
|
||
|
|
$recordLogLevel = Logger::NOTICE;
|
||
|
|
$levelName = 'NOTICE';
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($recordLogLevel < $logLevel) {
|
||
|
|
$discardedLogs++;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Adjust the date according to the display timezone
|
||
|
|
$date = $this->adjustDisplayLogDate($date, DateFormatHelper::getSystemFormat());
|
||
|
|
|
||
|
|
// Get the date and the message (all log types have these)
|
||
|
|
foreach ($node->childNodes as $nodeElements) {
|
||
|
|
if ($nodeElements->nodeName == 'scheduleID') {
|
||
|
|
$scheduleId = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'layoutID') {
|
||
|
|
$layoutId = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'mediaID') {
|
||
|
|
$mediaId = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'type') {
|
||
|
|
$type = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'method') {
|
||
|
|
$method = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'message') {
|
||
|
|
$message = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'thread') {
|
||
|
|
if ($nodeElements->textContent != '') {
|
||
|
|
$thread = '[' . $nodeElements->textContent . '] ';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the message is still empty, take the entire node content
|
||
|
|
if ($message == '') {
|
||
|
|
$message = $node->textContent;
|
||
|
|
}
|
||
|
|
// Add the IDs to the message
|
||
|
|
if ($scheduleId != '') {
|
||
|
|
$message .= ' scheduleId: ' . $scheduleId;
|
||
|
|
}
|
||
|
|
if ($layoutId != '') {
|
||
|
|
$message .= ' layoutId: ' . $layoutId;
|
||
|
|
}
|
||
|
|
if ($mediaId != '') {
|
||
|
|
$message .= ' mediaId: ' . $mediaId;
|
||
|
|
}
|
||
|
|
// Trim the page if it is over 50 characters.
|
||
|
|
$page = $thread . $method . $type;
|
||
|
|
|
||
|
|
if (strlen($page) >= 50) {
|
||
|
|
$page = substr($page, 0, 49);
|
||
|
|
}
|
||
|
|
|
||
|
|
$logs[] = [
|
||
|
|
$this->logProcessor->getUid(),
|
||
|
|
$date,
|
||
|
|
'PLAYER',
|
||
|
|
$levelName,
|
||
|
|
$page,
|
||
|
|
'POST',
|
||
|
|
$message,
|
||
|
|
0,
|
||
|
|
$this->display->displayId
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
if (count($logs) > 0) {
|
||
|
|
// Insert
|
||
|
|
$sql = '
|
||
|
|
INSERT INTO `log` (
|
||
|
|
`runNo`,
|
||
|
|
`logdate`,
|
||
|
|
`channel`,
|
||
|
|
`type`,
|
||
|
|
`page`,
|
||
|
|
`function`,
|
||
|
|
`message`,
|
||
|
|
`userid`,
|
||
|
|
`displayid`
|
||
|
|
) VALUES
|
||
|
|
';
|
||
|
|
|
||
|
|
// Build our query
|
||
|
|
$params = [];
|
||
|
|
|
||
|
|
// We're going to make params for each row/column
|
||
|
|
$i = 0;
|
||
|
|
$row = 0;
|
||
|
|
foreach ($logs as $log) {
|
||
|
|
$row++;
|
||
|
|
$sql .= '(';
|
||
|
|
foreach ($log as $field) {
|
||
|
|
$i++;
|
||
|
|
$key = $row . '_' . $i;
|
||
|
|
$sql .= ':' . $key . ',';
|
||
|
|
$params[$key] = $field;
|
||
|
|
}
|
||
|
|
$sql = rtrim($sql, ',');
|
||
|
|
$sql .= '),';
|
||
|
|
}
|
||
|
|
$sql = rtrim($sql, ',');
|
||
|
|
|
||
|
|
// Insert
|
||
|
|
$this->getStore()->update($sql, $params);
|
||
|
|
} else {
|
||
|
|
$this->getLog()->info('0 logs resolved from log package');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($discardedLogs > 0) {
|
||
|
|
$this->getLog()->info(
|
||
|
|
'Discarded ' . $discardedLogs . ' logs.
|
||
|
|
Consider adjusting your display profile log level. Resolved level is ' . $logLevel
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$SUBMITLOG, strlen($logXml));
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param $serverKey
|
||
|
|
* @param $hardwareKey
|
||
|
|
* @param $statXml
|
||
|
|
* @return bool
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \SoapFault
|
||
|
|
*/
|
||
|
|
protected function doSubmitStats($serverKey, $hardwareKey, $statXml)
|
||
|
|
{
|
||
|
|
$this->logProcessor->setRoute('SubmitStats');
|
||
|
|
$sanitizer = $this->getSanitizer([
|
||
|
|
'serverKey' => $serverKey,
|
||
|
|
'hardwareKey' => $hardwareKey
|
||
|
|
]);
|
||
|
|
// Sanitize
|
||
|
|
$serverKey = $sanitizer->getString('serverKey');
|
||
|
|
$hardwareKey = $sanitizer->getString('hardwareKey');
|
||
|
|
|
||
|
|
// Check the serverKey matches
|
||
|
|
if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) {
|
||
|
|
throw new \SoapFault(
|
||
|
|
'Sender',
|
||
|
|
'The Server key you entered does not match with the server key at this address'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auth this request...
|
||
|
|
if (!$this->authDisplay($hardwareKey)) {
|
||
|
|
throw new \SoapFault('Receiver', 'This Display is not authorised.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now that we authenticated the Display, make sure we are sticking to our bandwidth limit
|
||
|
|
if (!$this->checkBandwidth($this->display->displayId)) {
|
||
|
|
throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug('Received XML. ' . $statXml);
|
||
|
|
|
||
|
|
if ($statXml == '') {
|
||
|
|
throw new \SoapFault('Receiver', 'Stat XML is empty.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Store an array of parsed stat data for insert
|
||
|
|
$now = Carbon::now();
|
||
|
|
|
||
|
|
// Get the display timezone to use when adjusting log dates.
|
||
|
|
$defaultTimeZone = $this->getConfig()->getSetting('defaultTimezone');
|
||
|
|
|
||
|
|
// Count stats processed from XML
|
||
|
|
$statCount = 0;
|
||
|
|
|
||
|
|
// Load the XML into a DOMDocument
|
||
|
|
$document = new \DOMDocument('1.0');
|
||
|
|
$document->loadXML($statXml);
|
||
|
|
|
||
|
|
$splashScreenErrorLogged = false;
|
||
|
|
$backgroundWidgetErrorLogged = false;
|
||
|
|
$widgetIdsNotFound = [];
|
||
|
|
$memoryCache = [];
|
||
|
|
|
||
|
|
// Cache of scheduleIds, counts and deleted entities
|
||
|
|
$schedules = [];
|
||
|
|
$campaigns = [];
|
||
|
|
$deletedScheduleIds = [];
|
||
|
|
$deletedCampaignIds = [];
|
||
|
|
|
||
|
|
foreach ($document->documentElement->childNodes as $node) {
|
||
|
|
/* @var \DOMElement $node */
|
||
|
|
// Make sure we don't consider any text nodes
|
||
|
|
if ($node->nodeType == XML_TEXT_NODE) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Each element should have these attributes
|
||
|
|
$fromDt = $node->getAttribute('fromdt');
|
||
|
|
$toDt = $node->getAttribute('todt');
|
||
|
|
$type = strtolower($node->getAttribute('type'));
|
||
|
|
$duration = $node->getAttribute('duration');
|
||
|
|
$count = $node->getAttribute('count');
|
||
|
|
$count = ($count != '') ? (int) $count : 1;
|
||
|
|
|
||
|
|
// Pull out engagements
|
||
|
|
$engagements = [];
|
||
|
|
foreach ($node->childNodes as $nodeElements) {
|
||
|
|
/* @var \DOMElement $nodeElements */
|
||
|
|
if ($nodeElements->nodeName == 'engagements') {
|
||
|
|
$i = 0;
|
||
|
|
foreach ($nodeElements->childNodes as $child) {
|
||
|
|
/* @var \DOMElement $child */
|
||
|
|
if ($child->nodeName == 'engagement') {
|
||
|
|
$engagements[$i]['tag'] = $child->getAttribute('tag');
|
||
|
|
$engagements[$i]['duration'] = (int) $child->getAttribute('duration');
|
||
|
|
$engagements[$i]['count'] = (int) $child->getAttribute('count');
|
||
|
|
$i++;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Validate
|
||
|
|
// --------
|
||
|
|
// Check we have the minimum required data
|
||
|
|
if ($fromDt == '' || $toDt == '' || $type == '') {
|
||
|
|
$this->getLog()->info('Stat submitted without the fromdt, todt or type attributes.');
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Exactly the same dates are not supported
|
||
|
|
if ($fromDt == $toDt) {
|
||
|
|
$this->getLog()->debug('Ignoring a Stat record because the fromDt ('
|
||
|
|
. $fromDt. ') and toDt (' . $toDt. ') are the same');
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Adjust the date according to the display timezone
|
||
|
|
// stats are returned in player local date/time
|
||
|
|
// the CMS will have been configured with that Player's timezone, so we can convert accordingly.
|
||
|
|
try {
|
||
|
|
// From date
|
||
|
|
$fromDt = ($this->display->timeZone != null)
|
||
|
|
? Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $fromDt, $this->display->timeZone)
|
||
|
|
->tz($defaultTimeZone)
|
||
|
|
: Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $fromDt);
|
||
|
|
|
||
|
|
// To date
|
||
|
|
$toDt = ($this->display->timeZone != null)
|
||
|
|
? Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $toDt, $this->display->timeZone)
|
||
|
|
->tz($defaultTimeZone)
|
||
|
|
: Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $toDt);
|
||
|
|
|
||
|
|
// Do we need to set the duration of this record (we will do for older individually collected stats)
|
||
|
|
if ($duration == '') {
|
||
|
|
$duration = $toDt->diffInSeconds($fromDt);
|
||
|
|
}
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
// Protect against the date format being unreadable
|
||
|
|
$this->getLog()->error('Stat with a from or to date that cannot be understood. fromDt: '
|
||
|
|
. $fromDt . ', toDt: ' . $toDt . '. E = ' . $e->getMessage());
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// From date cannot be ahead of to date
|
||
|
|
if ($fromDt > $toDt) {
|
||
|
|
$this->getLog()->debug('Ignoring a Stat record because the fromDt ('
|
||
|
|
. $fromDt . ') is greater than toDt (' . $toDt . ')');
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// check maximum retention period against stat date, do not record if it's older than max stat age
|
||
|
|
$maxAge = intval($this->getConfig()->getSetting('MAINTENANCE_STAT_MAXAGE'));
|
||
|
|
if ($maxAge != 0) {
|
||
|
|
$maxAgeDate = Carbon::now()->subDays($maxAge);
|
||
|
|
|
||
|
|
if ($toDt->isBefore($maxAgeDate)) {
|
||
|
|
$this->getLog()->debug('Stat older than max retention period, skipping.');
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If the duration is enormous, then we have an erroneous message from the player
|
||
|
|
if ($duration > (86400 * 365)) {
|
||
|
|
$this->getLog()->debug('Dates are too far apart');
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Simple validation end
|
||
|
|
// ---------------------
|
||
|
|
// from here on we need to look things up
|
||
|
|
|
||
|
|
// ScheduleId is supplied to all layout stats, but not event stats.
|
||
|
|
$scheduleId = $node->getAttribute('scheduleid');
|
||
|
|
if (empty($scheduleId)) {
|
||
|
|
$scheduleId = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
$layoutId = $node->getAttribute('layoutid');
|
||
|
|
|
||
|
|
// Ignore the splash screen
|
||
|
|
if ($layoutId == 'splash') {
|
||
|
|
// only logging this message one time
|
||
|
|
if (!$splashScreenErrorLogged) {
|
||
|
|
$splashScreenErrorLogged = true;
|
||
|
|
$this->getLog()->info('Splash Screen Statistic Ignored');
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Slightly confusing behaviour here to support old players without introduction a different call in
|
||
|
|
// XMDS v=5.
|
||
|
|
// MediaId is actually the widgetId (since 1.8) and the mediaId is looked up by this service
|
||
|
|
$widgetId = $node->getAttribute('mediaid');
|
||
|
|
$mediaId = null;
|
||
|
|
|
||
|
|
// Ignore old "background" stat records.
|
||
|
|
if ($widgetId === 'background') {
|
||
|
|
if (!$backgroundWidgetErrorLogged) {
|
||
|
|
$backgroundWidgetErrorLogged = true;
|
||
|
|
$this->getLog()->info('Ignoring old "background" stat record.');
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// The mediaId (really widgetId) might well be null
|
||
|
|
if ($widgetId == 'null' || $widgetId == '') {
|
||
|
|
$widgetId = 0;
|
||
|
|
} else {
|
||
|
|
// Try to get details for this widget
|
||
|
|
try {
|
||
|
|
if (in_array($widgetId, $widgetIdsNotFound)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Do we have it in cache?
|
||
|
|
if (!array_key_exists('w_' . $widgetId, $memoryCache)) {
|
||
|
|
$memoryCache['w_' . $widgetId] = $this->widgetFactory->getMediaByWidgetId($widgetId);
|
||
|
|
}
|
||
|
|
$mediaId = $memoryCache['w_' . $widgetId];
|
||
|
|
|
||
|
|
// If the mediaId is empty, then we can assume we're a stat for a region specific widget
|
||
|
|
if ($mediaId === null) {
|
||
|
|
$type = 'widget';
|
||
|
|
}
|
||
|
|
} catch (NotFoundException $notFoundException) {
|
||
|
|
// Widget isn't found
|
||
|
|
// we can only log this and move on
|
||
|
|
// only logging this message one time
|
||
|
|
if (!in_array($widgetId, $widgetIdsNotFound)) {
|
||
|
|
$widgetIdsNotFound[] = $widgetId;
|
||
|
|
$this->getLog()->error('Stat for a widgetId that doesnt exist: ' . $widgetId);
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$tag = $node->getAttribute('tag');
|
||
|
|
if ($tag == 'null') {
|
||
|
|
$tag = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Cache a count for this scheduleId
|
||
|
|
$parentCampaignId = 0;
|
||
|
|
$parentCampaign = null;
|
||
|
|
|
||
|
|
if ($scheduleId > 0 && !in_array($scheduleId, $deletedScheduleIds)) {
|
||
|
|
try {
|
||
|
|
// Lookup this schedule
|
||
|
|
if (!array_key_exists($scheduleId, $schedules)) {
|
||
|
|
// Look up the campaign.
|
||
|
|
$schedules[$scheduleId] = $this->scheduleFactory->getById($scheduleId);
|
||
|
|
}
|
||
|
|
$parentCampaignId = $schedules[$scheduleId]->parentCampaignId ?? 0;
|
||
|
|
} catch (NotFoundException $notFoundException) {
|
||
|
|
$this->getLog()->error('Schedule with ID ' . $scheduleId . ' no-longer exists');
|
||
|
|
$deletedScheduleIds[] = $scheduleId;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Does this event have a parent campaign?
|
||
|
|
if (!empty($parentCampaignId) && !in_array($parentCampaignId, $deletedCampaignIds)) {
|
||
|
|
try {
|
||
|
|
// Look it up
|
||
|
|
if (!array_key_exists($parentCampaignId, $campaigns)) {
|
||
|
|
$campaigns[$parentCampaignId] = $this->campaignFactory->getById($parentCampaignId);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set the parent campaign so that it is recorded with the stat record
|
||
|
|
$parentCampaign = $campaigns[$parentCampaignId];
|
||
|
|
|
||
|
|
// For a layout stat we should increment the number of plays on the Campaign
|
||
|
|
if ($type === 'layout' && $campaigns[$parentCampaignId]->type === 'ad') {
|
||
|
|
// spend/impressions multiplier for this display
|
||
|
|
$spend = empty($this->display->costPerPlay)
|
||
|
|
? 0
|
||
|
|
: ($count * $this->display->costPerPlay);
|
||
|
|
$impressions = empty($this->display->impressionsPerPlay)
|
||
|
|
? 0
|
||
|
|
: ($count * $this->display->impressionsPerPlay);
|
||
|
|
|
||
|
|
// record
|
||
|
|
$parentCampaign->incrementPlays($count, $spend, $impressions);
|
||
|
|
}
|
||
|
|
} catch (NotFoundException $notFoundException) {
|
||
|
|
$deletedCampaignIds[] = $parentCampaignId;
|
||
|
|
$this->getLog()->error('Campaign with ID ' . $parentCampaignId . ' no-longer exists');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Important - stats will now send display entity instead of displayId
|
||
|
|
$stats = [
|
||
|
|
'type' => $type,
|
||
|
|
'statDate' => $now,
|
||
|
|
'fromDt' => $fromDt,
|
||
|
|
'toDt' => $toDt,
|
||
|
|
'scheduleId' => $scheduleId,
|
||
|
|
'display' => $this->display,
|
||
|
|
'layoutId' => (int) $layoutId,
|
||
|
|
'mediaId' => $mediaId,
|
||
|
|
'tag' => $tag,
|
||
|
|
'widgetId' => (int) $widgetId,
|
||
|
|
'duration' => (int) $duration,
|
||
|
|
'count' => $count,
|
||
|
|
'engagements' => (count($engagements) > 0) ? $engagements : [],
|
||
|
|
'parentCampaignId' => $parentCampaignId,
|
||
|
|
'parentCampaign' => $parentCampaign,
|
||
|
|
];
|
||
|
|
|
||
|
|
$this->getTimeSeriesStore()->addStat($stats);
|
||
|
|
|
||
|
|
$statCount++;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Insert stats
|
||
|
|
if ($statCount > 0) {
|
||
|
|
$this->getTimeSeriesStore()->addStatFinalize();
|
||
|
|
} else {
|
||
|
|
$this->getLog()->info('0 stats resolved from data package');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save ad campaign changes.
|
||
|
|
foreach ($campaigns as $campaign) {
|
||
|
|
if ($campaign->type === 'ad') {
|
||
|
|
$campaign->saveIncrementPlays();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$SUBMITSTATS, strlen($statXml));
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param $serverKey
|
||
|
|
* @param $hardwareKey
|
||
|
|
* @param $inventory
|
||
|
|
* @return bool
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \SoapFault
|
||
|
|
*/
|
||
|
|
protected function doMediaInventory($serverKey, $hardwareKey, $inventory)
|
||
|
|
{
|
||
|
|
$this->logProcessor->setRoute('MediaInventory');
|
||
|
|
$sanitizer = $this->getSanitizer([
|
||
|
|
'serverKey' => $serverKey,
|
||
|
|
'hardwareKey' => $hardwareKey
|
||
|
|
]);
|
||
|
|
// Sanitize
|
||
|
|
$serverKey = $sanitizer->getString('serverKey');
|
||
|
|
$hardwareKey = $sanitizer->getString('hardwareKey');
|
||
|
|
|
||
|
|
// Check the serverKey matches
|
||
|
|
if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) {
|
||
|
|
throw new \SoapFault(
|
||
|
|
'Sender',
|
||
|
|
'The Server key you entered does not match with the server key at this address'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auth this request...
|
||
|
|
if (!$this->authDisplay($hardwareKey)) {
|
||
|
|
throw new \SoapFault('Receiver', 'This Display is not authorised.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now that we authenticated the Display, make sure we are sticking to our bandwidth limit
|
||
|
|
if (!$this->checkBandwidth($this->display->displayId)) {
|
||
|
|
throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug($inventory);
|
||
|
|
|
||
|
|
// Check that the $inventory contains something
|
||
|
|
if ($inventory == '') {
|
||
|
|
throw new \SoapFault('Receiver', 'Inventory Cannot be Empty');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Load the XML into a DOMDocument
|
||
|
|
$document = new \DOMDocument('1.0');
|
||
|
|
$document->loadXML($inventory);
|
||
|
|
|
||
|
|
// Assume we are complete (but we are getting some)
|
||
|
|
$mediaInventoryComplete = 1;
|
||
|
|
|
||
|
|
$xpath = new \DOMXPath($document);
|
||
|
|
$fileNodes = $xpath->query('//file');
|
||
|
|
|
||
|
|
foreach ($fileNodes as $node) {
|
||
|
|
/* @var \DOMElement $node */
|
||
|
|
|
||
|
|
// What type of file?
|
||
|
|
try {
|
||
|
|
$requiredFile = null;
|
||
|
|
switch ($node->getAttribute('type')) {
|
||
|
|
case 'media':
|
||
|
|
$requiredFile = $this->requiredFileFactory->getByDisplayAndMedia(
|
||
|
|
$this->display->displayId,
|
||
|
|
$node->getAttribute('id'),
|
||
|
|
$node->getAttribute('id') < 0 ? 'P' : 'M'
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'layout':
|
||
|
|
$requiredFile = $this->requiredFileFactory->getByDisplayAndLayout(
|
||
|
|
$this->display->displayId,
|
||
|
|
$node->getAttribute('id')
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'resource':
|
||
|
|
$requiredFile = $this->requiredFileFactory->getByDisplayAndWidget(
|
||
|
|
$this->display->displayId,
|
||
|
|
$node->getAttribute('id')
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case 'dependency':
|
||
|
|
$requiredFile = $this->requiredFileFactory->getByDisplayAndDependency(
|
||
|
|
$this->display->displayId,
|
||
|
|
$node->getAttribute('fileType'),
|
||
|
|
$node->getAttribute('id')
|
||
|
|
);
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
$this->getLog()->debug(sprintf(
|
||
|
|
'Skipping unknown node in media inventory: %s - %s.',
|
||
|
|
$node->getAttribute('type'),
|
||
|
|
$node->getAttribute('id')
|
||
|
|
));
|
||
|
|
|
||
|
|
// continue drops out the switch, continue again goes to the top of the foreach
|
||
|
|
continue 2;
|
||
|
|
}
|
||
|
|
|
||
|
|
// File complete?
|
||
|
|
$complete = $node->getAttribute('complete');
|
||
|
|
$requiredFile->complete = $complete;
|
||
|
|
$requiredFile->save();
|
||
|
|
|
||
|
|
// If this item is a 0 then set not complete
|
||
|
|
if ($complete == 0) {
|
||
|
|
$mediaInventoryComplete = 2;
|
||
|
|
}
|
||
|
|
} catch (NotFoundException $e) {
|
||
|
|
$this->getLog()->error('Unable to find file in media inventory: '
|
||
|
|
. $node->getAttribute('type') . '. ' . $node->getAttribute('id'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->display->mediaInventoryStatus = $mediaInventoryComplete;
|
||
|
|
|
||
|
|
// Only call save if this property has actually changed.
|
||
|
|
if ($this->display->hasPropertyChanged('mediaInventoryStatus')) {
|
||
|
|
$this->getLog()->debug('Media Inventory status changed to ' . $this->display->mediaInventoryStatus);
|
||
|
|
|
||
|
|
// If we are complete, then drop the player nonce cache
|
||
|
|
if ($this->display->mediaInventoryStatus == 1) {
|
||
|
|
$this->getLog()->debug('Media Inventory tells us that all downloads are complete');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->display->saveMediaInventoryStatus();
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$MEDIAINVENTORY, strlen($inventory));
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Resource for XMDS v6 and lower
|
||
|
|
* outputs the HTML with data included
|
||
|
|
* @param string $serverKey
|
||
|
|
* @param string $hardwareKey
|
||
|
|
* @param integer $layoutId
|
||
|
|
* @param string $regionId
|
||
|
|
* @param string $mediaId
|
||
|
|
* @param bool $isSupportsDataUrl Does the callee support data URLs in widgets?
|
||
|
|
* @return mixed
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \SoapFault
|
||
|
|
*/
|
||
|
|
protected function doGetResource(
|
||
|
|
$serverKey,
|
||
|
|
$hardwareKey,
|
||
|
|
$layoutId,
|
||
|
|
$regionId,
|
||
|
|
$mediaId,
|
||
|
|
bool $isSupportsDataUrl = false
|
||
|
|
) {
|
||
|
|
$this->logProcessor->setRoute('GetResource');
|
||
|
|
$sanitizer = $this->getSanitizer([
|
||
|
|
'serverKey' => $serverKey,
|
||
|
|
'hardwareKey' => $hardwareKey,
|
||
|
|
'layoutId' => $layoutId,
|
||
|
|
'regionId' => $regionId,
|
||
|
|
'mediaId' => $mediaId
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Sanitize
|
||
|
|
$serverKey = $sanitizer->getString('serverKey');
|
||
|
|
$hardwareKey = $sanitizer->getString('hardwareKey');
|
||
|
|
$layoutId = $sanitizer->getInt('layoutId');
|
||
|
|
$regionId = $sanitizer->getString('regionId');
|
||
|
|
$mediaId = $sanitizer->getString('mediaId');
|
||
|
|
|
||
|
|
// Check the serverKey matches
|
||
|
|
if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) {
|
||
|
|
throw new \SoapFault(
|
||
|
|
'Sender',
|
||
|
|
'The Server key you entered does not match with the server key at this address'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auth this request...
|
||
|
|
if (!$this->authDisplay($hardwareKey)) {
|
||
|
|
throw new \SoapFault('Receiver', 'This Display is not authorised.');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Now that we authenticated the Display, make sure we are sticking to our bandwidth limit
|
||
|
|
if (!$this->checkBandwidth($this->display->displayId)) {
|
||
|
|
throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded');
|
||
|
|
}
|
||
|
|
|
||
|
|
// The MediaId is actually the widgetId
|
||
|
|
try {
|
||
|
|
$requiredFile = $this->requiredFileFactory->getByDisplayAndWidget(
|
||
|
|
$this->display->displayId,
|
||
|
|
$mediaId
|
||
|
|
);
|
||
|
|
|
||
|
|
$region = $this->regionFactory->getById($regionId);
|
||
|
|
$widget = $this->widgetFactory->loadByWidgetId($mediaId);
|
||
|
|
|
||
|
|
// If this is a canvas region we add all our widgets to this.
|
||
|
|
if ($region->type === 'canvas') {
|
||
|
|
// Render a canvas
|
||
|
|
// ---------------
|
||
|
|
// A canvas plays all widgets in the region at once.
|
||
|
|
// none of them will be anything other than elements
|
||
|
|
$widgets = $region->getPlaylist()->widgets;
|
||
|
|
} else {
|
||
|
|
// Render a widget in a region
|
||
|
|
// ---------------------------
|
||
|
|
// We have a widget
|
||
|
|
$widgets = [$widget];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Module is always the first widget
|
||
|
|
$module = $this->moduleFactory->getByType($widget->type);
|
||
|
|
|
||
|
|
// Get all templates
|
||
|
|
$templates = $this->widgetFactory->getTemplatesForWidgets($module, $widgets);
|
||
|
|
|
||
|
|
$renderer = $this->moduleFactory->createWidgetHtmlRenderer();
|
||
|
|
$resource = $renderer->renderOrCache(
|
||
|
|
$region,
|
||
|
|
$widgets,
|
||
|
|
$templates
|
||
|
|
);
|
||
|
|
|
||
|
|
// An array of media we have access to.
|
||
|
|
// Get all linked media for this player and this widget.
|
||
|
|
$media = [];
|
||
|
|
$widgetIds = implode(',', array_map(function ($el) {
|
||
|
|
return $el->widgetId;
|
||
|
|
}, $widgets));
|
||
|
|
|
||
|
|
$sql = '
|
||
|
|
SELECT `media`.mediaId, `media`.storedAs
|
||
|
|
FROM `media`
|
||
|
|
INNER JOIN `lkwidgetmedia`
|
||
|
|
ON `lkwidgetmedia`.mediaId = `media`.mediaId
|
||
|
|
WHERE `lkwidgetmedia`.widgetId IN (' . $widgetIds . ')
|
||
|
|
UNION ALL
|
||
|
|
SELECT `media`.mediaId, `media`.storedAs
|
||
|
|
FROM `media`
|
||
|
|
INNER JOIN `display_media`
|
||
|
|
ON `display_media`.mediaId = `media`.mediaId
|
||
|
|
WHERE `display_media`.displayId = :displayId
|
||
|
|
';
|
||
|
|
|
||
|
|
// There isn't any point using a prepared statement because the widgetIds are substituted at runtime
|
||
|
|
foreach ($this->getStore()->select($sql, [
|
||
|
|
'displayId' => $this->display->displayId
|
||
|
|
]) as $row) {
|
||
|
|
$media[$row['mediaId']] = $row['storedAs'];
|
||
|
|
};
|
||
|
|
|
||
|
|
// If this player doesn't support data URLs, then add the data to this response.
|
||
|
|
$data = [];
|
||
|
|
|
||
|
|
if (!$isSupportsDataUrl) {
|
||
|
|
foreach ($widgets as $widget) {
|
||
|
|
$dataModule = $this->moduleFactory->getByType($widget->type);
|
||
|
|
if ($dataModule->isDataProviderExpected()) {
|
||
|
|
// We only ever return cache.
|
||
|
|
$dataProvider = $dataModule->createDataProvider($widget);
|
||
|
|
$dataProvider->setDisplayProperties(
|
||
|
|
$this->display->latitude ?: $this->getConfig()->getSetting('DEFAULT_LAT'),
|
||
|
|
$this->display->longitude ?: $this->getConfig()->getSetting('DEFAULT_LONG'),
|
||
|
|
$this->display->displayId
|
||
|
|
);
|
||
|
|
|
||
|
|
// Use the cache if we can.
|
||
|
|
try {
|
||
|
|
$widgetDataProviderCache = $this->moduleFactory->createWidgetDataProviderCache();
|
||
|
|
$cacheKey = $this->moduleFactory->determineCacheKey(
|
||
|
|
$dataModule,
|
||
|
|
$widget,
|
||
|
|
$this->display->displayId,
|
||
|
|
$dataProvider,
|
||
|
|
$dataModule->getWidgetProviderOrNull()
|
||
|
|
);
|
||
|
|
|
||
|
|
// We do not pass a modifiedDt in here because we always expect to be cached.
|
||
|
|
if (!$widgetDataProviderCache->decorateWithCache($dataProvider, $cacheKey, null, false)) {
|
||
|
|
throw new NotFoundException('Cache not ready');
|
||
|
|
}
|
||
|
|
|
||
|
|
$widgetData = $widgetDataProviderCache->decorateForPlayer(
|
||
|
|
$this->configService,
|
||
|
|
$this->display,
|
||
|
|
$this->configService->getApiKeyDetails()['encryptionKey'],
|
||
|
|
$dataProvider->getData(),
|
||
|
|
$media,
|
||
|
|
);
|
||
|
|
} catch (GeneralException $exception) {
|
||
|
|
// No data cached yet, exception
|
||
|
|
$this->getLog()->error('getResource: Failed to get data cache for widgetId '
|
||
|
|
. $widget->widgetId . ', e: ' . $exception->getMessage());
|
||
|
|
throw new \SoapFault('Receiver', 'Cache not ready');
|
||
|
|
}
|
||
|
|
|
||
|
|
$data[$widget->widgetId] = [
|
||
|
|
'data' => $widgetData,
|
||
|
|
'meta' => $dataProvider->getMeta()
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Decorate for the player
|
||
|
|
$resource = $renderer->decorateForPlayer(
|
||
|
|
$this->display,
|
||
|
|
$resource,
|
||
|
|
$media,
|
||
|
|
$isSupportsDataUrl,
|
||
|
|
$data,
|
||
|
|
$this->moduleFactory->getAssetsFromTemplates($templates)
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($resource == '') {
|
||
|
|
throw new ControllerNotImplemented();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Log bandwidth
|
||
|
|
$requiredFile->bytesRequested = $requiredFile->bytesRequested + strlen($resource);
|
||
|
|
$requiredFile->save();
|
||
|
|
} catch (NotFoundException) {
|
||
|
|
throw new \SoapFault('Receiver', 'Requested an invalid file.');
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
// Pass soap faults straight through.
|
||
|
|
if ($e instanceof \SoapFault) {
|
||
|
|
throw $e;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->error('Unknown error during getResource. E = ' . $e->getMessage());
|
||
|
|
$this->getLog()->debug($e->getTraceAsString());
|
||
|
|
throw new \SoapFault('Receiver', 'Unable to get the media resource');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Log Bandwidth
|
||
|
|
$this->logBandwidth($this->display->displayId, Bandwidth::$GETRESOURCE, strlen($resource));
|
||
|
|
|
||
|
|
return $resource;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Authenticates the display
|
||
|
|
* @param string $hardwareKey
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
protected function authDisplay($hardwareKey)
|
||
|
|
{
|
||
|
|
try {
|
||
|
|
$this->display = $this->displayFactory->getByLicence($hardwareKey);
|
||
|
|
|
||
|
|
if ($this->display->licensed != 1) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Configure our log processor
|
||
|
|
$this->logProcessor->setDisplay($this->display->displayId, $this->display->isAuditing());
|
||
|
|
|
||
|
|
return true;
|
||
|
|
|
||
|
|
} catch (NotFoundException $e) {
|
||
|
|
$this->getLog()->error($e->getMessage());
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Alert Display Up
|
||
|
|
* assesses whether a notification is required to be sent for this display, and only does something if the
|
||
|
|
* display is currently marked as offline (i.e. it is coming back online again)
|
||
|
|
* this is only called in Register
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
protected function alertDisplayUp(): void
|
||
|
|
{
|
||
|
|
$maintenanceEnabled = $this->getConfig()->getSetting('MAINTENANCE_ENABLED');
|
||
|
|
|
||
|
|
if ($this->display->loggedIn == 0 && !empty($this->display->displayId)) {
|
||
|
|
$this->getLog()->info(sprintf('Display %s was down, now its up.', $this->display->display));
|
||
|
|
|
||
|
|
// Log display up
|
||
|
|
$this->displayEventFactory->createEmpty()->eventEnd($this->display->displayId);
|
||
|
|
|
||
|
|
// Do we need to email?
|
||
|
|
if ($this->display->emailAlert == 1
|
||
|
|
&& ($maintenanceEnabled == 'On' || $maintenanceEnabled == 'Protected')
|
||
|
|
&& $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') == 1
|
||
|
|
) {
|
||
|
|
// Only send alerts during operating hours.
|
||
|
|
if ($this->isInsideOperatingHours()) {
|
||
|
|
$subject = sprintf(__('Recovery for Display %s'), $this->display->display);
|
||
|
|
$body = sprintf(
|
||
|
|
__('Display ID %d is now back online %s'),
|
||
|
|
$this->display->displayId,
|
||
|
|
Carbon::now()->format(DateFormatHelper::getSystemFormat())
|
||
|
|
);
|
||
|
|
|
||
|
|
// Create a notification assigned to system-wide user groups
|
||
|
|
try {
|
||
|
|
$notification = $this->notificationFactory->createSystemNotification(
|
||
|
|
$subject,
|
||
|
|
$body,
|
||
|
|
Carbon::now(),
|
||
|
|
'display',
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get groups which have been configured to receive notifications
|
||
|
|
foreach ($this->userGroupFactory
|
||
|
|
->getDisplayNotificationGroups($this->display->displayGroupId) as $group) {
|
||
|
|
$notification->assignUserGroup($group);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save the notification and insert the links, etc.
|
||
|
|
$notification->save();
|
||
|
|
} catch (\Exception) {
|
||
|
|
$this->getLog()->error(sprintf(
|
||
|
|
'Unable to send email alert for display %s with subject %s and body %s',
|
||
|
|
$this->display->display,
|
||
|
|
$subject,
|
||
|
|
$body
|
||
|
|
));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$this->getLog()->info('Not sending recovery email for Display - '
|
||
|
|
. $this->display->display . ' we are outside of its operating hours');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$this->getLog()->debug(sprintf(
|
||
|
|
'No email required. Email Alert: %d, Enabled: %s, Email Enabled: %s.',
|
||
|
|
$this->display->emailAlert,
|
||
|
|
$maintenanceEnabled,
|
||
|
|
$this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS')
|
||
|
|
));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Is the display currently inside operating hours?
|
||
|
|
* @return bool
|
||
|
|
* @throws \Xibo\Support\Exception\NotFoundException
|
||
|
|
*/
|
||
|
|
private function isInsideOperatingHours(): bool
|
||
|
|
{
|
||
|
|
$dayPartId = $this->display->getSetting('dayPartId', null, ['displayOverride' => true]);
|
||
|
|
|
||
|
|
// If dayPart is configured, check operating hours
|
||
|
|
if ($dayPartId !== null) {
|
||
|
|
try {
|
||
|
|
$dayPart = $this->dayPartFactory->getById($dayPartId);
|
||
|
|
|
||
|
|
$startTimeArray = explode(':', $dayPart->startTime);
|
||
|
|
$startTime = Carbon::now()->setTime(intval($startTimeArray[0]), intval($startTimeArray[1]));
|
||
|
|
|
||
|
|
$endTimeArray = explode(':', $dayPart->endTime);
|
||
|
|
$endTime = Carbon::now()->setTime(intval($endTimeArray[0]), intval($endTimeArray[1]));
|
||
|
|
|
||
|
|
$now = Carbon::now();
|
||
|
|
|
||
|
|
// handle exceptions
|
||
|
|
foreach ($dayPart->exceptions as $exception) {
|
||
|
|
// check if we are on exception day and if so override the startTime and endTime accordingly
|
||
|
|
if ($exception['day'] == Carbon::now()->format('D')) {
|
||
|
|
// Parse the start/end times into the current day.
|
||
|
|
$exceptionsStartTime = explode(':', $exception['start']);
|
||
|
|
$startTime = Carbon::now()->setTime(
|
||
|
|
intval($exceptionsStartTime[0]),
|
||
|
|
intval($exceptionsStartTime[1])
|
||
|
|
);
|
||
|
|
|
||
|
|
$exceptionsEndTime = explode(':', $exception['end']);
|
||
|
|
$endTime = Carbon::now()->setTime(
|
||
|
|
intval($exceptionsEndTime[0]),
|
||
|
|
intval($exceptionsEndTime[1])
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// check if we are inside the operating hours for this display - we use that flag to decide
|
||
|
|
// if we need to create a notification and send an email.
|
||
|
|
return ($now >= $startTime && $now <= $endTime);
|
||
|
|
} catch (NotFoundException) {
|
||
|
|
$this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId '
|
||
|
|
. $this->display->displayId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Otherwise, assume CMS is within operating hours.
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the Client IP Address
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
protected function getIp()
|
||
|
|
{
|
||
|
|
$clientIp = '';
|
||
|
|
|
||
|
|
$keys = array('X_FORWARDED_FOR', 'HTTP_X_FORWARDED_FOR', 'CLIENT_IP', 'REMOTE_ADDR');
|
||
|
|
foreach ($keys as $key) {
|
||
|
|
if (isset($_SERVER[$key]) && filter_var($_SERVER[$key], FILTER_VALIDATE_IP) !== false) {
|
||
|
|
$clientIp = $_SERVER[$key];
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $clientIp;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check we haven't exceeded the bandwidth limits
|
||
|
|
* - Note, display logging doesn't work in here, this is CMS level logging
|
||
|
|
*
|
||
|
|
* @param int $displayId The Display ID
|
||
|
|
* @return bool true if the check passes, false if it fails
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
protected function checkBandwidth($displayId)
|
||
|
|
{
|
||
|
|
// Uncomment to enable auditing.
|
||
|
|
//$this->logProcessor->setDisplay(0, 'debug');
|
||
|
|
|
||
|
|
$this->display = $this->displayFactory->getById($displayId);
|
||
|
|
|
||
|
|
$xmdsLimit = intval($this->getConfig()->getSetting('MONTHLY_XMDS_TRANSFER_LIMIT_KB'));
|
||
|
|
$displayBandwidthLimit = $this->display->bandwidthLimit;
|
||
|
|
|
||
|
|
try {
|
||
|
|
$bandwidthUsage = 0;
|
||
|
|
|
||
|
|
if ($this->bandwidthFactory->isBandwidthExceeded($xmdsLimit, $bandwidthUsage)) {
|
||
|
|
// Bandwidth Exceeded
|
||
|
|
// Create a notification if we don't already have one today for this display.
|
||
|
|
$subject = __('Bandwidth allowance exceeded');
|
||
|
|
$date = Carbon::now();
|
||
|
|
$notifications = $this->notificationFactory->getBySubjectAndDate(
|
||
|
|
$subject,
|
||
|
|
$date->startOfDay()->format('U'),
|
||
|
|
$date->addDay()->startOfDay()->format('U')
|
||
|
|
);
|
||
|
|
|
||
|
|
if (count($notifications) <= 0) {
|
||
|
|
$body = __(
|
||
|
|
sprintf(
|
||
|
|
'Bandwidth allowance of %s exceeded. Used %s',
|
||
|
|
ByteFormatter::format($xmdsLimit * 1024),
|
||
|
|
ByteFormatter::format($bandwidthUsage)
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
$notification = $this->notificationFactory->createSystemNotification(
|
||
|
|
$subject,
|
||
|
|
$body,
|
||
|
|
Carbon::now(),
|
||
|
|
'library'
|
||
|
|
);
|
||
|
|
|
||
|
|
$notification->save();
|
||
|
|
|
||
|
|
$this->getLog()->critical($subject);
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
|
||
|
|
} elseif ($this->bandwidthFactory->isBandwidthExceeded(
|
||
|
|
$displayBandwidthLimit,
|
||
|
|
$bandwidthUsage,
|
||
|
|
$displayId
|
||
|
|
)
|
||
|
|
) {
|
||
|
|
// Bandwidth Exceeded
|
||
|
|
// Create a notification if we don't already have one today for this display.
|
||
|
|
$subject = __(sprintf('Display ID %d exceeded the bandwidth limit', $this->display->displayId));
|
||
|
|
$date = Carbon::now();
|
||
|
|
|
||
|
|
$notifications = $this->notificationFactory->getBySubjectAndDate(
|
||
|
|
$subject,
|
||
|
|
$date->startOfDay()->format('U'),
|
||
|
|
$date->addDay()->startOfDay()->format('U')
|
||
|
|
);
|
||
|
|
if (count($notifications) <= 0) {
|
||
|
|
$body = __(
|
||
|
|
sprintf(
|
||
|
|
'Display bandwidth limit %s exceeded. Used %s for Display Id %d',
|
||
|
|
ByteFormatter::format($displayBandwidthLimit * 1024),
|
||
|
|
ByteFormatter::format($bandwidthUsage),
|
||
|
|
$this->display->displayId
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
$notification = $this->notificationFactory->createSystemNotification(
|
||
|
|
$subject,
|
||
|
|
$body,
|
||
|
|
Carbon::now(),
|
||
|
|
'display'
|
||
|
|
);
|
||
|
|
|
||
|
|
// Add in any displayNotificationGroups, with permissions
|
||
|
|
foreach ($this->userGroupFactory->getDisplayNotificationGroups(
|
||
|
|
$this->display->displayGroupId
|
||
|
|
) as $group) {
|
||
|
|
$notification->assignUserGroup($group);
|
||
|
|
}
|
||
|
|
|
||
|
|
$notification->save();
|
||
|
|
|
||
|
|
$this->getLog()->critical($subject);
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
} else {
|
||
|
|
// Bandwidth not exceeded.
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
$this->getLog()->error($e->getMessage());
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Log Bandwidth Usage
|
||
|
|
* @param int $displayId
|
||
|
|
* @param string $type
|
||
|
|
* @param int $sizeInBytes
|
||
|
|
*/
|
||
|
|
protected function logBandwidth($displayId, $type, $sizeInBytes)
|
||
|
|
{
|
||
|
|
$this->bandwidthFactory->createAndSave($type, $displayId, $sizeInBytes);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add a dependency to the provided DOM element
|
||
|
|
* @param array $rfIds
|
||
|
|
* @param \DOMDocument $requiredFilesXml
|
||
|
|
* @param \DOMElement $fileElements
|
||
|
|
* @param bool $httpDownloads
|
||
|
|
* @param bool $isSupportsDependency
|
||
|
|
* @param Dependency $dependency
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \DOMException
|
||
|
|
*/
|
||
|
|
private function addDependency(
|
||
|
|
array &$rfIds,
|
||
|
|
\DOMDocument $requiredFilesXml,
|
||
|
|
\DOMElement $fileElements,
|
||
|
|
bool $httpDownloads,
|
||
|
|
bool $isSupportsDependency,
|
||
|
|
Dependency $dependency
|
||
|
|
): void {
|
||
|
|
// Create a required file for this dependency
|
||
|
|
$dependencyBasePath = basename($dependency->path);
|
||
|
|
$rfId = $this->requiredFileFactory
|
||
|
|
->createForGetDependency(
|
||
|
|
$this->display->displayId,
|
||
|
|
$dependency->fileType,
|
||
|
|
$dependency->legacyId,
|
||
|
|
$dependency->id,
|
||
|
|
$dependencyBasePath,
|
||
|
|
$dependency->size,
|
||
|
|
$isSupportsDependency
|
||
|
|
)
|
||
|
|
->save()->rfId;
|
||
|
|
|
||
|
|
// Make sure we do not already have it.
|
||
|
|
if (in_array($rfId, $rfIds)) {
|
||
|
|
$this->getLog()->debug('addDependency: ' . $dependency->id . ' already added to XML');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Record this required file's ID as a new one.
|
||
|
|
$rfIds[] = $rfId;
|
||
|
|
|
||
|
|
// Add to RF XML
|
||
|
|
$file = $requiredFilesXml->createElement('file');
|
||
|
|
|
||
|
|
// HTTP downloads?
|
||
|
|
// 3) some dependencies don't support HTTP downloads because they aren't in the library
|
||
|
|
$httpFilePath = null;
|
||
|
|
if ($httpDownloads && $dependency->isAvailableOverHttp) {
|
||
|
|
$httpFilePath = LinkSigner::generateSignedLink(
|
||
|
|
$this->display,
|
||
|
|
$this->configService->getApiKeyDetails()['encryptionKey'],
|
||
|
|
$this->configService->getSetting('CDN_URL'),
|
||
|
|
RequiredFile::$TYPE_DEPENDENCY,
|
||
|
|
$dependency->id,
|
||
|
|
$dependencyBasePath,
|
||
|
|
$dependency->fileType,
|
||
|
|
);
|
||
|
|
|
||
|
|
$file->setAttribute('download', 'http');
|
||
|
|
} else {
|
||
|
|
$file->setAttribute('download', 'xmds');
|
||
|
|
}
|
||
|
|
|
||
|
|
$file->setAttribute('size', $dependency->size);
|
||
|
|
$file->setAttribute('md5', $dependency->md5);
|
||
|
|
$file->setAttribute('saveAs', $dependencyBasePath);
|
||
|
|
|
||
|
|
// 4) earlier versions of XMDS do not support GetDependency, and will therefore need to have their
|
||
|
|
// dependencies added as media nodes.
|
||
|
|
if ($isSupportsDependency) {
|
||
|
|
// Soap7+: GetDependency supported
|
||
|
|
$file->setAttribute('type', 'dependency');
|
||
|
|
$file->setAttribute('fileType', $dependency->fileType);
|
||
|
|
if ($httpFilePath !== null) {
|
||
|
|
$file->setAttribute('path', $httpFilePath);
|
||
|
|
} else {
|
||
|
|
$file->setAttribute('path', $dependencyBasePath);
|
||
|
|
}
|
||
|
|
$file->setAttribute('saveAs', $dependencyBasePath);
|
||
|
|
$file->setAttribute('id', $dependency->id);
|
||
|
|
} else {
|
||
|
|
// We have no choice but to pretend we're a media download.
|
||
|
|
// Soap3/4 are modified to cater for this.
|
||
|
|
// Soap3: players read, send and save using the `path`. Expects `id.ext`.
|
||
|
|
// Soap4: we only have the ID, but we can use HTTP downloads.
|
||
|
|
// Soap5,6,7: use Soap4
|
||
|
|
$file->setAttribute('type', 'media');
|
||
|
|
if ($httpFilePath !== null) {
|
||
|
|
$file->setAttribute('path', $httpFilePath);
|
||
|
|
} else {
|
||
|
|
$file->setAttribute('path', $dependencyBasePath);
|
||
|
|
}
|
||
|
|
$file->setAttribute('id', $dependency->legacyId);
|
||
|
|
$file->setAttribute('fileType', 'media');
|
||
|
|
|
||
|
|
// We need an extra attribute so that we can retrieve the original asset type from cache.
|
||
|
|
$file->setAttribute('realId', $dependency->id);
|
||
|
|
$file->setAttribute('assetType', $dependency->fileType);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add our node
|
||
|
|
$fileElements->appendChild($file);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set Date Filters
|
||
|
|
*/
|
||
|
|
protected function setDateFilters()
|
||
|
|
{
|
||
|
|
// Hour to hour time bands for the query
|
||
|
|
// Rf lookahead is the number of seconds ahead we should consider.
|
||
|
|
// it may well be less than 1 hour, and if so we cannot do hour to hour time bands, we need to do
|
||
|
|
// now, forwards.
|
||
|
|
// Start with now:
|
||
|
|
$fromFilter = Carbon::now();
|
||
|
|
|
||
|
|
// If this Display is in a different timezone, then we need to set that here for these filter criteria
|
||
|
|
if (!empty($this->display->timeZone)) {
|
||
|
|
$fromFilter->setTimezone($this->display->timeZone);
|
||
|
|
}
|
||
|
|
|
||
|
|
// TODO use new sanitizer here
|
||
|
|
//$rfLookAhead = $this->getSanitizer()->int($this->getConfig()->getSetting('REQUIRED_FILES_LOOKAHEAD'));
|
||
|
|
$rfLookAhead = $this->getConfig()->getSetting('REQUIRED_FILES_LOOKAHEAD');
|
||
|
|
if ($rfLookAhead >= 3600) {
|
||
|
|
// Go from the top of this hour
|
||
|
|
$fromFilter
|
||
|
|
->minute(0)
|
||
|
|
->second(0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If we're set to look ahead, then do so - otherwise grab only a 1 hour slice
|
||
|
|
if ($this->getConfig()->getSetting('SCHEDULE_LOOKAHEAD') == 1) {
|
||
|
|
$toFilter = $fromFilter->copy()->addSeconds($rfLookAhead);
|
||
|
|
} else {
|
||
|
|
$toFilter = $fromFilter->copy()->addHour();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Make sure our filters are expressed in CMS time, so that when we run the query we don't lose the timezone
|
||
|
|
$this->localFromFilter = $fromFilter;
|
||
|
|
$this->localToFilter = $toFilter;
|
||
|
|
$this->fromFilter = Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $fromFilter);
|
||
|
|
$this->toFilter = Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $toFilter);
|
||
|
|
|
||
|
|
$this->getLog()->debug(
|
||
|
|
sprintf(
|
||
|
|
'FromDT = %s [%d]. ToDt = %s [%d]',
|
||
|
|
$fromFilter->toRssString(),
|
||
|
|
$fromFilter->format('U'),
|
||
|
|
$toFilter->toRssString(),
|
||
|
|
$toFilter->format('U')
|
||
|
|
)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Adjust the log date according to the Display timezone.
|
||
|
|
* Return current date if we fail.
|
||
|
|
* @param string $date
|
||
|
|
* @param string $format
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
protected function adjustDisplayLogDate(string $date, string $format): string
|
||
|
|
{
|
||
|
|
// Get the display timezone to use when adjusting log dates.
|
||
|
|
$defaultTimeZone = $this->getConfig()->getSetting('defaultTimezone');
|
||
|
|
|
||
|
|
// Adjust the date according to the display timezone
|
||
|
|
try {
|
||
|
|
$date = ($this->display->timeZone != null)
|
||
|
|
? Carbon::createFromFormat(
|
||
|
|
DateFormatHelper::getSystemFormat(),
|
||
|
|
$date,
|
||
|
|
$this->display->timeZone
|
||
|
|
)->tz($defaultTimeZone)
|
||
|
|
: Carbon::createFromFormat(
|
||
|
|
DateFormatHelper::getSystemFormat(),
|
||
|
|
$date
|
||
|
|
);
|
||
|
|
$date = $date->format($format);
|
||
|
|
} catch (\Exception $e) {
|
||
|
|
// Protect against the date format being unreadable
|
||
|
|
$this->getLog()->debug('Date format unreadable on log message: ' . $date);
|
||
|
|
|
||
|
|
// Use now instead
|
||
|
|
$date = Carbon::now()->format($format);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $date;
|
||
|
|
}
|
||
|
|
|
||
|
|
private function createDisplayAlert(\DomElement $alertNode)
|
||
|
|
{
|
||
|
|
$date = $this->adjustDisplayLogDate($alertNode->getAttribute('date'), 'U');
|
||
|
|
$eventType = '';
|
||
|
|
$refId = '';
|
||
|
|
$detail = '';
|
||
|
|
$alertType = '';
|
||
|
|
|
||
|
|
// Get the nodes we are expecting
|
||
|
|
foreach ($alertNode->childNodes as $nodeElements) {
|
||
|
|
if ($nodeElements->nodeName == 'eventType') {
|
||
|
|
$eventType = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'refId') {
|
||
|
|
$refId = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'message') {
|
||
|
|
$detail = $nodeElements->textContent;
|
||
|
|
} else if ($nodeElements->nodeName == 'alertType') {
|
||
|
|
$alertType = $nodeElements->textContent;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// if alerts should provide both start and end or just start
|
||
|
|
if ($alertType == 'both' || $alertType == 'start') {
|
||
|
|
$displayEvent = $this->displayEventFactory->createEmpty();
|
||
|
|
|
||
|
|
// new record populated from the submitLog xml.
|
||
|
|
$displayEvent->displayId = $this->display->displayId;
|
||
|
|
$displayEvent->eventTypeId = $displayEvent->getEventIdFromString($eventType);
|
||
|
|
$displayEvent->eventDate = $date;
|
||
|
|
$displayEvent->start = $date;
|
||
|
|
$displayEvent->end = $alertType == 'both' ? $date : null;
|
||
|
|
$displayEvent->refId = empty($refId) ? null : $refId;
|
||
|
|
$displayEvent->detail = $detail;
|
||
|
|
|
||
|
|
$displayEvent->save();
|
||
|
|
} else if ($alertType == 'end') {
|
||
|
|
// if this event pertain only to end date for an existing event record,
|
||
|
|
// then set the end date for this display and the specified eventType
|
||
|
|
$displayEvent = $this->displayEventFactory->createEmpty();
|
||
|
|
$eventTypeId = $displayEvent->getEventIdFromString($eventType);
|
||
|
|
empty($refId)
|
||
|
|
? $displayEvent->eventEnd($this->display->displayId, $eventTypeId, $detail, $date)
|
||
|
|
: $displayEvent->eventEndByReference($this->display->displayId, $eventTypeId, $refId, $detail);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Collection Interval with offset
|
||
|
|
* calculates an offset for the collection interval based on the displayId and returns it
|
||
|
|
* the offset is plus or minus 10 seconds and will always be the same when given the same displayId
|
||
|
|
* @param int $collectionInterval
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
protected function collectionIntervalWithOffset(int $collectionInterval): int
|
||
|
|
{
|
||
|
|
if ($collectionInterval <= 60) {
|
||
|
|
$offset = $this->display->displayId % 10;
|
||
|
|
return $collectionInterval + ($offset <= 5 ? $offset * -1 : $offset - 5);
|
||
|
|
} else {
|
||
|
|
$offset = $this->display->displayId % 20;
|
||
|
|
return $collectionInterval + ($offset <= 10 ? $offset * -1 : $offset - 10);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|