Files
Cloud-CMS/lib/Controller/Display.php
Matt Batchelder 05ce0da296 Initial Upload
2025-12-02 10:32:59 -05:00

3197 lines
119 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\Controller;
use Carbon\Carbon;
use GeoJson\Feature\Feature;
use GeoJson\Feature\FeatureCollection;
use GeoJson\Geometry\Point;
use GuzzleHttp\Client;
use Intervention\Image\ImageManagerStatic as Img;
use Respect\Validation\Validator as v;
use RobThree\Auth\TwoFactorAuth;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Xibo\Event\DisplayGroupLoadEvent;
use Xibo\Factory\DayPartFactory;
use Xibo\Factory\DisplayEventFactory;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\DisplayProfileFactory;
use Xibo\Factory\DisplayTypeFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\NotificationFactory;
use Xibo\Factory\PlayerVersionFactory;
use Xibo\Factory\RequiredFileFactory;
use Xibo\Factory\TagFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\Random;
use Xibo\Helper\WakeOnLan;
use Xibo\Service\PlayerActionServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Support\Sanitizer\SanitizerInterface;
use Xibo\XMR\LicenceCheckAction;
use Xibo\XMR\PurgeAllAction;
use Xibo\XMR\RekeyAction;
use Xibo\XMR\ScreenShotAction;
/**
* Class Display
* @package Xibo\Controller
*/
class Display extends Base
{
use DisplayProfileConfigFields;
/**
* @var StorageServiceInterface
*/
private $store;
/**
* @var PoolInterface
*/
private $pool;
/**
* @var PlayerActionServiceInterface
*/
private $playerAction;
/**
* @var DayPartFactory
*/
private $dayPartFactory;
/**
* @var DisplayFactory
*/
private $displayFactory;
/**
* @var DisplayGroupFactory
*/
private $displayGroupFactory;
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var DisplayProfileFactory
*/
private $displayProfileFactory;
/**
* @var DisplayTypeFactory
*/
private $displayTypeFactory;
/** @var DisplayEventFactory */
private $displayEventFactory;
/** @var PlayerVersionFactory */
private $playerVersionFactory;
/** @var RequiredFileFactory */
private $requiredFileFactory;
/** @var TagFactory */
private $tagFactory;
/** @var NotificationFactory */
private $notificationFactory;
/** @var UserGroupFactory */
private $userGroupFactory;
/**
* Set common dependencies.
* @param StorageServiceInterface $store
* @param PoolInterface $pool
* @param PlayerActionServiceInterface $playerAction
* @param DisplayFactory $displayFactory
* @param DisplayGroupFactory $displayGroupFactory
* @param DisplayTypeFactory $displayTypeFactory
* @param LayoutFactory $layoutFactory
* @param DisplayProfileFactory $displayProfileFactory
* @param DisplayEventFactory $displayEventFactory
* @param RequiredFileFactory $requiredFileFactory
* @param TagFactory $tagFactory
* @param NotificationFactory $notificationFactory
* @param UserGroupFactory $userGroupFactory
* @param PlayerVersionFactory $playerVersionFactory
* @param DayPartFactory $dayPartFactory
*/
public function __construct($store, $pool, $playerAction, $displayFactory, $displayGroupFactory, $displayTypeFactory, $layoutFactory, $displayProfileFactory, $displayEventFactory, $requiredFileFactory, $tagFactory, $notificationFactory, $userGroupFactory, $playerVersionFactory, $dayPartFactory)
{
$this->store = $store;
$this->pool = $pool;
$this->playerAction = $playerAction;
$this->displayFactory = $displayFactory;
$this->displayGroupFactory = $displayGroupFactory;
$this->displayTypeFactory = $displayTypeFactory;
$this->layoutFactory = $layoutFactory;
$this->displayProfileFactory = $displayProfileFactory;
$this->displayEventFactory = $displayEventFactory;
$this->requiredFileFactory = $requiredFileFactory;
$this->tagFactory = $tagFactory;
$this->notificationFactory = $notificationFactory;
$this->userGroupFactory = $userGroupFactory;
$this->playerVersionFactory = $playerVersionFactory;
$this->dayPartFactory = $dayPartFactory;
}
/**
* @SWG\Get(
* path="/displayvenue",
* summary="Get Display Venues",
* tags={"displayVenue"},
* operationId="displayVenueSearch",
* @SWG\Response(
* response=200,
* description="a successful response",
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayVenue(Request $request, Response $response)
{
if (!file_exists(PROJECT_ROOT . '/openooh/specification.json')) {
throw new GeneralException(__('OpenOOH specification missing'));
}
$content = file_get_contents(PROJECT_ROOT . '/openooh/specification.json');
$data = json_decode($content, true);
$taxonomy = [];
$i = 0;
foreach ($data['openooh_venue_taxonomy']['specification']['categories'] as $categories) {
$taxonomy[$i]['venueId'] = $categories['enumeration_id'];
$taxonomy[$i]['venueName'] = $categories['name'];
$i++;
foreach ($categories['children'] as $children) {
$taxonomy[$i]['venueId'] = $children['enumeration_id'];
$taxonomy[$i]['venueName'] = $categories['name'] . ' -> ' . $children['name'];
$i++;
if (isset($children['children'])) {
foreach ($children['children'] as $grandchildren) {
$taxonomy[$i]['venueId'] = $grandchildren['enumeration_id'] ;
$taxonomy[$i]['venueName'] = $categories['name'] . ' -> ' . $children['name'] . ' -> ' . $grandchildren['name'] ;
$i++;
}
}
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = count($taxonomy);
$this->getState()->setData($taxonomy);
return $this->render($request, $response);
}
/**
* Include display page template page based on sub page selected
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function displayPage(Request $request, Response $response)
{
// Build a list of display profiles
$displayProfiles = $this->displayProfileFactory->query();
$displayProfiles[] = ['displayProfileId' => -1, 'name' => __('Default')];
// Call to render the template
$this->getState()->template = 'display-page';
$mapConfig = [
'setArea' => [
'lat' => $this->getConfig()->getSetting('DEFAULT_LAT'),
'long' => $this->getConfig()->getSetting('DEFAULT_LONG'),
'zoom' => 7
]
];
$this->getState()->setData([
'mapConfig' => $mapConfig,
'displayProfiles' => $displayProfiles
]);
return $this->render($request, $response);
}
/**
* Display Management Page for an Individual Display
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function displayManage(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display)) {
throw new AccessDeniedException();
}
// Zero out some variables
$dependencies = [];
$layouts = [];
$widgets = [];
$widgetData = [];
$media = [];
$totalCount = 0;
$completeCount = 0;
$totalSize = 0;
$completeSize = 0;
// Show 4 widgets
// Dependencies
$sql = '
SELECT `requiredfile`.*
FROM `requiredfile`
WHERE `requiredfile`.displayId = :displayId
AND `requiredfile`.type = :type
ORDER BY fileType, path
';
foreach ($this->store->select($sql, ['displayId' => $id, 'type' => 'P']) as $row) {
$totalSize = $totalSize + $row['size'];
$totalCount++;
if (intval($row['complete']) === 1) {
$completeSize = $completeSize + $row['size'];
$completeCount = $completeCount + 1;
}
$row = $this->getSanitizer($row);
$dependencies[] = [
'path' => $row->getString('path'),
'fileType' => $row->getString('fileType'),
'bytesRequested' => $row->getInt('bytesRequested'),
'complete' => $row->getInt('complete'),
];
}
// Layouts
$sql = '
SELECT layoutId, layout, `requiredfile`.*
FROM `layout`
INNER JOIN `requiredfile`
ON `requiredfile`.itemId = `layout`.layoutId
WHERE `requiredfile`.displayId = :displayId
AND `requiredfile`.type = :type
ORDER BY layout
';
foreach ($this->store->select($sql, ['displayId' => $id, 'type' => 'L']) as $row) {
$rf = $this->requiredFileFactory->getByDisplayAndLayout($id, $row['layoutId']);
$totalCount++;
if ($rf->complete) {
$completeCount = $completeCount + 1;
}
$rf = $rf->toArray();
$rf['layout'] = $row['layout'];
$layouts[] = $rf;
}
// Media
$sql = '
SELECT mediaId, `name`, fileSize, media.type AS mediaType, storedAs, `requiredfile`.*
FROM `media`
INNER JOIN `requiredfile`
ON `requiredfile`.itemId = `media`.mediaId
WHERE `requiredfile`.displayId = :displayId
AND `requiredfile`.type = :type
ORDER BY `name`
';
foreach ($this->store->select($sql, ['displayId' => $id, 'type' => 'M']) as $row) {
$rf = $this->requiredFileFactory->getByDisplayAndMedia($id, $row['mediaId']);
$totalSize = $totalSize + $row['fileSize'];
$totalCount++;
if ($rf->complete) {
$completeSize = $completeSize + $row['fileSize'];
$completeCount = $completeCount + 1;
}
$rf = $rf->toArray();
$rf['name'] = $row['name'];
$rf['type'] = $row['mediaType'];
$rf['storedAs'] = $row['storedAs'];
$rf['size'] = $row['fileSize'];
$media[] = $rf;
}
// Widgets
$sql = '
SELECT `widget`.`type` AS widgetType,
`widgetoption`.`value` AS widgetName,
`widget`.`widgetId`,
`requiredfile`.*
FROM `widget`
INNER JOIN `requiredfile`
ON `requiredfile`.itemId = `widget`.widgetId
LEFT OUTER JOIN `widgetoption`
ON `widgetoption`.widgetId = `widget`.widgetId
AND `widgetoption`.option = \'name\'
WHERE `requiredfile`.`displayId` = :displayId
AND `requiredfile`.`type` IN (\'W\', \'D\')
ORDER BY `widgetoption`.value, `widget`.type, `widget`.widgetId
';
foreach ($this->store->select($sql, ['displayId' => $id]) as $row) {
$row = $this->getSanitizer($row);
$entry = [];
$entry['type'] = $row->getString('widgetType');
$entry['widgetName'] = $row->getString('widgetName');
$entry['widgetType'] = $row->getString('widgetType');
if ($row->getString('type') === 'W') {
$rf = $this->requiredFileFactory->getByDisplayAndWidget($id, $row->getInt('widgetId'));
$totalCount++;
if ($rf->complete) {
$completeCount = $completeCount + 1;
}
$widgets[] = array_merge($entry, $rf->toArray());
} else {
$entry['widgetId'] = $row->getInt('widgetId');
$entry['bytesRequested'] = $row->getInt('bytesRequested');
$widgetData[] = $entry;
}
}
// Widget for file status
// Decide what our units are going to be, based on the size
$suffixes = array('bytes', 'k', 'M', 'G', 'T');
$base = (int)floor(log($totalSize) / log(1024));
if ($base < 0) {
$base = 0;
}
$units = $suffixes[$base] ?? '';
$this->getLog()->debug(sprintf('Base for size is %d and suffix is %s', $base, $units));
// Call to render the template
$this->getState()->template = 'display-page-manage';
$this->getState()->setData([
'requiredFiles' => [],
'display' => $display,
'timeAgo' => Carbon::createFromTimestamp($display->lastAccessed)->diffForHumans(),
'errorSearch' => http_build_query([
'displayId' => $display->displayId,
'type' => 'ERROR',
'fromDt' => Carbon::now()->subHours(12)->format(DateFormatHelper::getSystemFormat()),
'toDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
]),
'inventory' => [
'dependencies' => $dependencies,
'layouts' => $layouts,
'media' => $media,
'widgets' => $widgets,
'widgetData' => $widgetData,
],
'status' => [
'units' => $units,
'countComplete' => $completeCount,
'countRemaining' => $totalCount - $completeCount,
'sizeComplete' => round((double)$completeSize / (pow(1024, $base)), 2),
'sizeRemaining' => round((double)($totalSize - $completeSize) / (pow(1024, $base)), 2),
],
'defaults' => [
'fromDate' => Carbon::now()->startOfMonth()->format(DateFormatHelper::getSystemFormat()),
'fromDateOneDay' => Carbon::now()->subDay()->format(DateFormatHelper::getSystemFormat()),
'toDate' => Carbon::now()->endOfMonth()->format(DateFormatHelper::getSystemFormat())
]
]);
return $this->render($request, $response);
}
/**
* Get display filters
* @param SanitizerInterface $parsedQueryParams
* @return array
*/
public function getFilters(SanitizerInterface $parsedQueryParams): array
{
return [
'displayId' => $parsedQueryParams->getInt('displayId'),
'display' => $parsedQueryParams->getString('display'),
'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
'macAddress' => $parsedQueryParams->getString('macAddress'),
'license' => $parsedQueryParams->getString('hardwareKey'),
'displayGroupId' => $parsedQueryParams->getInt('displayGroupId'),
'clientVersion' => $parsedQueryParams->getString('clientVersion'),
'clientType' => $parsedQueryParams->getString('clientType'),
'clientCode' => $parsedQueryParams->getString('clientCode'),
'customId' => $parsedQueryParams->getString('customId'),
'authorised' => $parsedQueryParams->getInt('authorised'),
'displayProfileId' => $parsedQueryParams->getInt('displayProfileId'),
'tags' => $parsedQueryParams->getString('tags'),
'exactTags' => $parsedQueryParams->getCheckbox('exactTags'),
'showTags' => true,
'clientAddress' => $parsedQueryParams->getString('clientAddress'),
'mediaInventoryStatus' => $parsedQueryParams->getInt('mediaInventoryStatus'),
'loggedIn' => $parsedQueryParams->getInt('loggedIn'),
'lastAccessed' => $parsedQueryParams->getDate('lastAccessed')?->format('U'),
'displayGroupIdMembers' => $parsedQueryParams->getInt('displayGroupIdMembers'),
'orientation' => $parsedQueryParams->getString('orientation'),
'commercialLicence' => $parsedQueryParams->getInt('commercialLicence'),
'folderId' => $parsedQueryParams->getInt('folderId'),
'logicalOperator' => $parsedQueryParams->getString('logicalOperator'),
'logicalOperatorName' => $parsedQueryParams->getString('logicalOperatorName'),
'bounds' => $parsedQueryParams->getString('bounds'),
'syncGroupId' => $parsedQueryParams->getInt('syncGroupId'),
'syncGroupIdMembers' => $parsedQueryParams->getInt('syncGroupIdMembers'),
'xmrRegistered' => $parsedQueryParams->getInt('xmrRegistered'),
'isPlayerSupported' => $parsedQueryParams->getInt('isPlayerSupported'),
'displayGroupIds' => $parsedQueryParams->getIntArray('displayGroupIds'),
];
}
/**
* Grid of Displays
*
* @SWG\Get(
* path="/display",
* operationId="displaySearch",
* tags={"display"},
* summary="Display Search",
* description="Search Displays for this User",
* @SWG\Parameter(
* name="displayId",
* in="query",
* description="Filter by Display Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="displayGroupId",
* in="query",
* description="Filter by DisplayGroup Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="display",
* in="query",
* description="Filter by Display Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="tags",
* in="query",
* description="Filter by tags",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="exactTags",
* in="query",
* description="A flag indicating whether to treat the tags filter as an exact match",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="logicalOperator",
* in="query",
* description="When filtering by multiple Tags, which logical operator should be used? AND|OR",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="macAddress",
* in="query",
* description="Filter by Mac Address",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="hardwareKey",
* in="query",
* description="Filter by Hardware Key",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="clientVersion",
* in="query",
* description="Filter by Client Version",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="clientType",
* in="query",
* description="Filter by Client Type",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="clientCode",
* in="query",
* description="Filter by Client Code",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="embed",
* in="query",
* description="Embed related data, namely displaygroups. A comma separated list of child objects to embed.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="authorised",
* in="query",
* description="Filter by authorised flag",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="displayProfileId",
* in="query",
* description="Filter by Display Profile",
* type="integer",
* required=false
* ),
* * @SWG\Parameter(
* name="mediaInventoryStatus",
* in="query",
* description="Filter by Display Status ( 1 - up to date, 2 - downloading, 3 - Out of date)",
* type="integer",
* required=false
* ),
* * @SWG\Parameter(
* name="loggedIn",
* in="query",
* description="Filter by Logged In flag",
* type="integer",
* required=false
* ),
* * @SWG\Parameter(
* name="lastAccessed",
* in="query",
* description="Filter by Display Last Accessed date, expects date in Y-m-d H:i:s format",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="query",
* description="Filter by Folder ID",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="xmrRegistered",
* in="query",
* description="Filter by whether XMR is registed (1 or 0)",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="isPlayerSupported",
* in="query",
* description="Filter by whether the player is supported (1 or 0)",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Display")
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ConfigurationException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function grid(Request $request, Response $response)
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
// Embed?
$embed = ($parsedQueryParams->getString('embed') != null)
? explode(',', $parsedQueryParams->getString('embed'))
: [];
$filter = $this->getFilters($parsedQueryParams);
// Get a list of displays
$displays = $this->displayFactory->query(
$this->gridRenderSort($parsedQueryParams),
$this->gridRenderFilter($filter, $parsedQueryParams)
);
// Get all Display Profiles
$displayProfiles = [];
foreach ($this->displayProfileFactory->query() as $displayProfile) {
$displayProfiles[$displayProfile->displayProfileId] = $displayProfile->name;
}
// validate displays so we get a realistic view of the table
$this->validateDisplays($displays);
foreach ($displays as $display) {
/* @var \Xibo\Entity\Display $display */
if (in_array('displaygroups', $embed)) {
$display->load();
} else {
$display->excludeProperty('displayGroups');
}
if (in_array('overrideconfig', $embed)) {
$display->includeProperty('overrideConfig');
}
$display->setUnmatchedProperty(
'bandwidthLimitFormatted',
ByteFormatter::format($display->bandwidthLimit * 1024)
);
// Current layout from cache
$display->getCurrentLayoutId($this->pool, $this->layoutFactory);
if ($this->isApi($request)) {
$display->lastAccessed =
Carbon::createFromTimestamp($display->lastAccessed)->format(DateFormatHelper::getSystemFormat());
$display->auditingUntil = ($display->auditingUntil == 0)
? 0
: Carbon::createFromTimestamp($display->auditingUntil)->format(DateFormatHelper::getSystemFormat());
continue;
}
// use try and catch here to cover scenario
// when there is no default display profile set for any of the existing display types.
$displayProfileName = '';
try {
$defaultDisplayProfile = $this->displayProfileFactory->getDefaultByType($display->clientType);
$displayProfileName = $defaultDisplayProfile->name;
} catch (NotFoundException) {
$this->getLog()->debug('No default Display Profile set for Display type ' . $display->clientType);
}
// Add in the display profile information
$display->setUnmatchedProperty(
'displayProfile',
(!array_key_exists($display->displayProfileId, $displayProfiles))
? $displayProfileName . __(' (Default)')
: $displayProfiles[$display->displayProfileId]
);
$display->includeProperty('buttons');
// Format the storage available / total space
$display->setUnmatchedProperty(
'storageAvailableSpaceFormatted',
ByteFormatter::format($display->storageAvailableSpace)
);
$display->setUnmatchedProperty(
'storageTotalSpaceFormatted',
ByteFormatter::format($display->storageTotalSpace)
);
$display->setUnmatchedProperty(
'storagePercentage',
($display->storageTotalSpace == 0)
? 0
: round($display->storageAvailableSpace / $display->storageTotalSpace * 100.0, 2)
);
// Set some text for the display status
$display->setUnmatchedProperty('statusDescription', match ($display->mediaInventoryStatus) {
1 => __('Display is up to date'),
2 => __('Display is downloading new files'),
3 => __('Display is out of date but has not yet checked in with the server'),
default => __('Unknown Display Status'),
});
// Commercial Licence
$display->setUnmatchedProperty('commercialLicenceDescription', match ($display->commercialLicence) {
1 => __('Display is fully licensed'),
2 => __('Display is on a trial licence'),
default => __('Display is not licensed'),
});
if ($display->clientCode < 400) {
$commercialLicenceDescription = $display->getUnmatchedProperty('commercialLicenceDescription');
$commercialLicenceDescription .= ' ('
. __('The status will be updated with each Commercial Licence check') . ')';
$display->setUnmatchedProperty('commercialLicenceDescription', $commercialLicenceDescription);
}
// Thumbnail
$display->setUnmatchedProperty('thumbnail', '');
// If we aren't logged in, and we are showThumbnail == 2, then show a circle
if (file_exists($this->getConfig()->getSetting('LIBRARY_LOCATION') . 'screenshots/'
. $display->displayId . '_screenshot.jpg')) {
$display->setUnmatchedProperty(
'thumbnail',
$this->urlFor($request, 'display.screenShot', [
'id' => $display->displayId
]) . '?' . Random::generateString()
);
}
$display->setUnmatchedProperty(
'teamViewerLink',
(!empty($display->teamViewerSerial))
? 'https://start.teamviewer.com/' . $display->teamViewerSerial
: ''
);
$display->setUnmatchedProperty(
'webkeyLink',
(!empty($display->webkeySerial))
? 'https://device.webkeyapp.com/phone?publicid=' . $display->webkeySerial
: ''
);
// Is a transfer to another CMS in progress?
$display->setUnmatchedProperty('isCmsTransferInProgress', (!empty($display->newCmsAddress)));
// Edit and Delete buttons first
if ($this->getUser()->featureEnabled('displays.modify')
&& $this->getUser()->checkEditable($display)
) {
// Manage
$display->buttons[] = [
'id' => 'display_button_manage',
'url' => $this->urlFor($request, 'display.manage', ['id' => $display->displayId]),
'text' => __('Manage'),
'external' => true
];
$display->buttons[] = ['divider' => true];
// Edit
$display->buttons[] = [
'id' => 'display_button_edit',
'url' => $this->urlFor($request, 'display.edit.form', ['id' => $display->displayId]),
'text' => __('Edit')
];
}
// Delete
if ($this->getUser()->featureEnabled('displays.modify')
&& $this->getUser()->checkDeleteable($display)
) {
$deleteButton = [
'id' => 'display_button_delete',
'url' => $this->urlFor($request, 'display.delete.form', ['id' => $display->displayId]),
'text' => __('Delete')
];
// We only include this in dev mode, because users have complained that it is too powerful a feature
// to have in the core product.
if (Environment::isDevMode()) {
$deleteButton['multi-select'] = true;
$deleteButton['dataAttributes'] = [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'display.delete',
['id' => $display->displayId]
)
],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'display_button_delete'],
['name' => 'sort-group', 'value' => 1],
['name' => 'text', 'value' => __('Delete')],
['name' => 'rowtitle', 'value' => $display->display]
];
}
$display->buttons[] = $deleteButton;
}
if ($this->getUser()->featureEnabled('displays.modify')
&& ($this->getUser()->checkEditable($display) || $this->getUser()->checkDeleteable($display))
) {
$display->buttons[] = ['divider' => true];
}
if ($this->getUser()->featureEnabled('displays.modify')
&& $this->getUser()->checkEditable($display)
) {
// Authorise
$display->buttons[] = [
'id' => 'display_button_authorise',
'url' => $this->urlFor($request, 'display.authorise.form', ['id' => $display->displayId]),
'text' => __('Authorise'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'display.authorise',
['id' => $display->displayId]
)
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'display_button_authorise'],
['name' => 'sort-group', 'value' => 2],
['name' => 'text', 'value' => __('Toggle Authorise')],
['name' => 'rowtitle', 'value' => $display->display]
]
];
// Default Layout
$display->buttons[] = [
'id' => 'display_button_defaultlayout',
'url' => $this->urlFor($request, 'display.defaultlayout.form', ['id' => $display->displayId]),
'text' => __('Default Layout'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'display.defaultlayout',
['id' => $display->displayId]
)
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'display_button_defaultlayout'],
['name' => 'sort-group', 'value' => 2],
['name' => 'text', 'value' => __('Set Default Layout')],
['name' => 'rowtitle', 'value' => $display->display],
['name' => 'form-callback', 'value' => 'setDefaultMultiSelectFormOpen']
]
];
if ($this->getUser()->featureEnabled('folder.view')) {
// Select Folder
$display->buttons[] = [
'id' => 'displaygroup_button_selectfolder',
'url' => $this->urlFor(
$request,
'displayGroup.selectfolder.form',
['id' => $display->displayGroupId]
),
'text' => __('Select Folder'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'displayGroup.selectfolder',
['id' => $display->displayGroupId]
)
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'displaygroup_button_selectfolder'],
['name' => 'sort-group', 'value' => 2],
['name' => 'text', 'value' => __('Move to Folder')],
['name' => 'rowtitle', 'value' => $display->display],
['name' => 'form-callback', 'value' => 'moveFolderMultiSelectFormOpen']
]
];
}
if (in_array($display->clientType, ['android', 'lg', 'sssp', 'chromeOS'])) {
$display->buttons[] = array(
'id' => 'display_button_checkLicence',
'url' => $this->urlFor($request, 'display.licencecheck.form', ['id' => $display->displayId]),
'text' => __('Check Licence'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'display.licencecheck',
['id' => $display->displayId]
)
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'display_button_checkLicence'],
['name' => 'sort-group', 'value' => 2],
['name' => 'text', 'value' => __('Check Licence')],
['name' => 'rowtitle', 'value' => $display->display]
]
);
}
$display->buttons[] = ['divider' => true];
}
// Schedule
if ($this->getUser()->featureEnabled('schedule.add')
&& ($this->getUser()->checkEditable($display)
|| $this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 1)
) {
$display->buttons[] = array(
'id' => 'display_button_schedule',
'url' => $this->urlFor(
$request,
'schedule.add.form',
['id' => $display->displayGroupId, 'from' => 'DisplayGroup']
),
'text' => __('Schedule')
);
}
// Check if limited view access is allowed
if (($this->getUser()->featureEnabled('displays.modify') && $this->getUser()->checkEditable($display))
|| $this->getUser()->featureEnabled('displays.limitedView')
) {
if ($this->getUser()->checkEditable($display)) {
if ($this->getUser()->featureEnabled('layout.view')) {
$display->buttons[] = [
'id' => 'display_button_layouts_jump',
'linkType' => '_self',
'external' => true,
'url' => $this->urlFor($request, 'layout.view')
. '?activeDisplayGroupId=' . $display->displayGroupId,
'text' => __('Jump to Scheduled Layouts')
];
}
// File Associations
$display->buttons[] = array(
'id' => 'displaygroup_button_fileassociations',
'url' => $this->urlFor($request, 'displayGroup.media.form', ['id' => $display->displayGroupId]),
'text' => __('Assign Files')
);
// Layout Assignments
$display->buttons[] = array(
'id' => 'displaygroup_button_layout_associations',
'url' => $this->urlFor($request, 'displayGroup.layout.form', ['id' => $display->displayGroupId]),
'text' => __('Assign Layouts')
);
}
// Screen Shot
$display->buttons[] = [
'id' => 'display_button_requestScreenShot',
'url' => $this->urlFor($request, 'display.screenshot.form', ['id' => $display->displayId]),
'text' => __('Request Screen Shot'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'display.requestscreenshot',
['id' => $display->displayId]
)
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'sort-group', 'value' => 3],
['name' => 'id', 'value' => 'display_button_requestScreenShot'],
['name' => 'text', 'value' => __('Request Screen Shot')],
['name' => 'rowtitle', 'value' => $display->display]
]
];
// Collect Now
$display->buttons[] = [
'id' => 'display_button_collectNow',
'url' => $this->urlFor(
$request,
'displayGroup.collectNow.form',
['id' => $display->displayGroupId]
),
'text' => __('Collect Now'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'displayGroup.action.collectNow',
['id' => $display->displayGroupId]
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'sort-group', 'value' => 3],
['name' => 'id', 'value' => 'display_button_collectNow'],
['name' => 'text', 'value' => __('Collect Now')],
['name' => 'rowtitle', 'value' => $display->display]
]
];
if ($this->getUser()->checkEditable($display)) {
// Trigger webhook
$display->buttons[] = [
'id' => 'display_button_trigger_webhook',
'url' => $this->urlFor(
$request,
'displayGroup.trigger.webhook.form',
['id' => $display->displayGroupId]
),
'text' => __('Trigger a web hook'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'displayGroup.action.trigger.webhook',
['id' => $display->displayGroupId]
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'display_button_trigger_webhook'],
['name' => 'sort-group', 'value' => 3],
['name' => 'text', 'value' => __('Trigger a web hook')],
['name' => 'rowtitle', 'value' => $display->display],
['name' => 'form-callback', 'value' => 'triggerWebhookMultiSelectFormOpen']
]
];
if ($this->getUser()->isSuperAdmin()) {
$display->buttons[] = [
'id' => 'display_button_purgeAll',
'url' => $this->urlFor($request, 'display.purge.all.form', ['id' => $display->displayId]),
'text' => __('Purge All')
];
}
$display->buttons[] = ['divider' => true];
}
}
if ($this->getUser()->featureEnabled('displays.modify')
&& $this->getUser()->checkPermissionsModifyable($display)
) {
// Display Groups
$display->buttons[] = array(
'id' => 'display_button_group_membership',
'url' => $this->urlFor($request, 'display.membership.form', ['id' => $display->displayId]),
'text' => __('Display Groups')
);
// Permissions
$display->buttons[] = [
'id' => 'display_button_group_permissions',
'url' => $this->urlFor(
$request,
'user.permissions.form',
['entity' => 'DisplayGroup', 'id' => $display->displayGroupId]
),
'text' => __('Share'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi',
['entity' => 'DisplayGroup', 'id' => $display->displayGroupId]
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'display_button_group_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $display->display],
['name' => 'sort-group', 'value' => 4],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
[
'name' => 'custom-handler-url',
'value' => $this->urlFor(
$request,
'user.permissions.multi.form',
['entity' => 'DisplayGroup']
)
],
['name' => 'content-id-name', 'value' => 'displayGroupId']
]
];
}
// Check if limited view access is allowed
if (($this->getUser()->featureEnabled('displays.modify') && $this->getUser()->checkEditable($display))
|| $this->getUser()->featureEnabled('displays.limitedView')
) {
if ($this->getUser()->checkPermissionsModifyable($display)) {
$display->buttons[] = ['divider' => true];
}
if ($this->getUser()->checkEditable($display)) {
// Wake On LAN
$display->buttons[] = array(
'id' => 'display_button_wol',
'url' => $this->urlFor($request, 'display.wol.form', ['id' => $display->displayId]),
'text' => __('Wake on LAN')
);
}
// Send Command
$display->buttons[] = [
'id' => 'displaygroup_button_command',
'url' => $this->urlFor($request, 'displayGroup.command.form', ['id' => $display->displayGroupId]),
'text' => __('Send Command'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'displayGroup.action.command',
['id' => $display->displayGroupId]
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'displaygroup_button_command'],
['name' => 'text', 'value' => __('Send Command')],
['name' => 'sort-group', 'value' => 3],
['name' => 'rowtitle', 'value' => $display->display],
['name' => 'form-callback', 'value' => 'sendCommandMultiSelectFormOpen']
]
];
if ($this->getUser()->checkEditable($display)) {
$display->buttons[] = ['divider' => true];
$display->buttons[] = [
'id' => 'display_button_move_cms',
'url' => $this->urlFor($request, 'display.moveCms.form', ['id' => $display->displayId]),
'text' => __('Transfer to another CMS'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'display.moveCms',
['id' => $display->displayId]
)
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'display_button_move_cms'],
['name' => 'text', 'value' => __('Transfer to another CMS')],
['name' => 'sort-group', 'value' => 5],
['name' => 'rowtitle', 'value' => $display->display],
['name' => 'form-callback', 'value' => 'setMoveCmsMultiSelectFormOpen']
]
];
$display->buttons[] = [
'multi-select' => true,
'multiSelectOnly' => true, // Show button only on multi-select menu
'id' => 'display_button_set_bandwidth',
'dataAttributes' => [
[
'name' => 'commit-url',
'value' => $this->urlFor(
$request,
'display.setBandwidthLimitMultiple'
)
],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'display_button_set_bandwidth'],
['name' => 'text', 'value' => __('Set Bandwidth')],
['name' => 'rowtitle', 'value' => $display->display],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
[
'name' => 'custom-handler-url',
'value' => $this->urlFor($request, 'display.setBandwidthLimitMultiple.form')
],
['name' => 'content-id-name', 'value' => 'displayId']
]
];
if ($display->getUnmatchedProperty('isCmsTransferInProgress', false)) {
$display->buttons[] = [
'id' => 'display_button_move_cancel',
'url' => $this->urlFor($request, 'display.moveCmsCancel.form', ['id' => $display->displayId]),
'text' => __('Cancel CMS Transfer'),
];
}
}
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->displayFactory->countLast();
$this->getState()->setData($displays);
return $this->render($request, $response);
}
/**
* Displays on map
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
*/
public function displayMap(Request $request, Response $response)
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
$filter = $this->getFilters($parsedQueryParams);
// Get a list of displays
$displays = $this->displayFactory->query(null, $filter);
$results = [];
$status = [
'1' => __('Up to date'),
'2' => __('Downloading'),
'3' => __('Out of date')
];
// Get all Display Profiles
$displayProfiles = [];
foreach ($this->displayProfileFactory->query() as $displayProfile) {
$displayProfiles[$displayProfile->displayProfileId] = $displayProfile->name;
}
foreach ($displays as $display) {
// use try and catch here to cover scenario when there is no default display profile set for any of the existing display types.
$displayProfileName = '';
try {
$defaultDisplayProfile = $this->displayProfileFactory->getDefaultByType($display->clientType);
$displayProfileName = $defaultDisplayProfile->name;
} catch (NotFoundException $e) {
$this->getLog()->debug('No default Display Profile set for Display type ' . $display->clientType);
}
// Add in the display profile information
$display->setUnmatchedProperty(
'displayProfile',
(!array_key_exists($display->displayProfileId, $displayProfiles))
? $displayProfileName . __(' (Default)')
: $displayProfiles[$display->displayProfileId]
);
$properties = [
'display' => $display->display,
'status' => $display->mediaInventoryStatus ? $status[$display->mediaInventoryStatus] : __('Unknown'),
'mediaInventoryStatus' => $display->mediaInventoryStatus,
'orientation' => ucwords($display->orientation ?: __('Unknown')),
'displayId' => $display->getId(),
'licensed' => $display->licensed,
'loggedIn' => $display->loggedIn,
'displayProfile' => $display->getUnmatchedProperty('displayProfile'),
'resolution' => $display->resolution,
'lastAccessed' => $display->lastAccessed,
];
if (file_exists($this->getConfig()->getSetting('LIBRARY_LOCATION') . 'screenshots/' . $display->displayId . '_screenshot.jpg')) {
$properties['thumbnail'] = $this->urlFor($request, 'display.screenShot', ['id' => $display->displayId]) . '?' . Random::generateString();
}
$longitude = ($display->longitude) ?: $this->getConfig()->getSetting('DEFAULT_LONG');
$latitude = ($display->latitude) ?: $this->getConfig()->getSetting('DEFAULT_LAT');
$geo = new Point([(double)$longitude, (double)$latitude]);
$results[] = new Feature($geo, $properties);
}
return $response->withJson(new FeatureCollection($results));
}
/**
* Edit Display Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function editForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id, true);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
// We have permission - load
$display->load();
// Dates
$auditingUntilIso = !empty($display->auditingUntil)
? Carbon::createFromTimestamp($display->auditingUntil)->format(DateFormatHelper::getSystemFormat())
: null;
$display->setUnmatchedProperty('auditingUntilIso', $auditingUntilIso);
// display profile dates
$displayProfile = $display->getDisplayProfile();
// Get the settings from the profile
$profile = $display->getSettings();
$displayTypes = $this->displayTypeFactory->query();
$elevateLogsUntil = $displayProfile->getSetting('elevateLogsUntil');
$elevateLogsUntilIso = !empty($elevateLogsUntil)
? Carbon::createFromTimestamp($elevateLogsUntil)->format(DateFormatHelper::getSystemFormat())
: null;
$displayProfile->setUnmatchedProperty('elevateLogsUntilIso', $elevateLogsUntilIso);
// Get a list of timezones
$timeZones = [];
foreach (DateFormatHelper::timezoneList() as $key => $value) {
$timeZones[] = ['id' => $key, 'value' => $value];
}
// Get the currently assigned default layout
try {
$layouts = (($display->defaultLayoutId != null) ? [$this->layoutFactory->getById($display->defaultLayoutId)] : []);
} catch (NotFoundException $notFoundException) {
$layouts = [];
}
// Player Version Setting
$versionId = $display->getSetting('versionMediaId', null, ['displayOnly' => true]);
$profileVersionId = $display->getDisplayProfile()->getSetting('versionMediaId');
$playerVersions = [];
// Daypart - Operating Hours
$dayPartId = $display->getSetting('dayPartId', null, ['displayOnly' => true]);
$profileDayPartId = $display->getDisplayProfile()->getSetting('dayPartId');
$dayparts = [];
// Get the Player Version for this display profile type
if ($versionId !== null) {
try {
$playerVersions[] = $this->playerVersionFactory->getById($versionId);
} catch (NotFoundException $e) {
$this->getLog()->debug('Unknown versionId set on Display Profile for displayId ' . $display->displayId);
}
}
if ($versionId !== $profileVersionId && $profileVersionId !== null) {
try {
$playerVersions[] = $this->playerVersionFactory->getById($profileVersionId);
} catch (NotFoundException $e) {
$this->getLog()->debug('Unknown versionId set on Display Profile for displayId ' . $display->displayId);
}
}
if ($dayPartId !== null) {
try {
$dayparts[] = $this->dayPartFactory->getById($dayPartId);
} catch (NotFoundException $e) {
$this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' . $display->displayId);
}
}
if ($dayPartId !== $profileDayPartId && $profileDayPartId !== null) {
try {
$dayparts[] = $this->dayPartFactory->getById($profileDayPartId);
} catch (NotFoundException $e) {
$this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' . $display->displayId);
}
}
// A list of languages
// Build an array of supported languages
$languages = [];
$localeDir = PROJECT_ROOT . '/locale';
foreach (array_map('basename', glob($localeDir . '/*.mo')) as $lang) {
// Trim the .mo off the end
$lang = str_replace('.mo', '', $lang);
$languages[] = ['id' => $lang, 'value' => $lang];
}
$this->getState()->template = 'display-form-edit';
$this->getState()->setData([
'display' => $display,
'displayProfile' => $displayProfile,
'lockOptions' => json_decode($display->getDisplayProfile()->getSetting('lockOptions', '[]'), true),
'layouts' => $layouts,
'profiles' => $this->displayProfileFactory->query(null, array('type' => $display->clientType)),
'settings' => $profile,
'timeZones' => $timeZones,
'displayLockName' => ($this->getConfig()->getSetting('DISPLAY_LOCK_NAME_TO_DEVICENAME') == 1),
'versions' => $playerVersions,
'displayTypes' => $displayTypes,
'dayParts' => $dayparts,
'languages' => $languages,
'isWolDisabled' => defined('ACCOUNT_ID'),
]);
return $this->render($request, $response);
}
/**
* Delete form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function deleteForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkDeleteable($display)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'display-form-delete';
$this->getState()->setData([
'display' => $display,
]);
return $this->render($request, $response);
}
/**
* Display Edit
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/display/{displayId}",
* operationId="displayEdit",
* tags={"display"},
* summary="Display Edit",
* description="Edit a Display",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="display",
* in="formData",
* description="The Display Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="A description of the Display",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="tags",
* in="formData",
* description="A comma separated list of tags for this item",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="auditingUntil",
* in="formData",
* description="A date this Display records auditing information until.",
* type="string",
* format="date-time",
* required=false
* ),
* @SWG\Parameter(
* name="defaultLayoutId",
* in="formData",
* description="A Layout ID representing the Default Layout for this Display.",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="licensed",
* in="formData",
* description="Flag indicating whether this display is licensed.",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="license",
* in="formData",
* description="The hardwareKey to use as the licence key for this Display",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="incSchedule",
* in="formData",
* description="Flag indicating whether the Default Layout should be included in the Schedule",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="emailAlert",
* in="formData",
* description="Flag indicating whether the Display generates up/down email alerts.",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="alertTimeout",
* in="formData",
* description="How long in seconds should this display wait before alerting when it hasn't connected. Override for the collection interval.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="wakeOnLanEnabled",
* in="formData",
* description="Flag indicating if Wake On LAN is enabled for this Display",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="wakeOnLanTime",
* in="formData",
* description="A h:i string representing the time that the Display should receive its Wake on LAN command",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="broadCastAddress",
* in="formData",
* description="The BroadCast Address for this Display - used by Wake On LAN",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="secureOn",
* in="formData",
* description="The secure on configuration for this Display",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="cidr",
* in="formData",
* description="The CIDR configuration for this Display",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="latitude",
* in="formData",
* description="The Latitude of this Display",
* type="number",
* required=false
* ),
* @SWG\Parameter(
* name="longitude",
* in="formData",
* description="The Longitude of this Display",
* type="number",
* required=false
* ),
* @SWG\Parameter(
* name="timeZone",
* in="formData",
* description="The timezone for this display, or empty to use the CMS timezone",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="languages",
* in="formData",
* description="An array of languages supported in this display location",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="displayProfileId",
* in="formData",
* description="The Display Settings Profile ID",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="displayTypeId",
* in="formData",
* description="The Display Type ID of this Display",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="screenSize",
* in="formData",
* description="The screen size of this Display",
* type="number",
* required=false
* ),
* @SWG\Parameter(
* name="venueId",
* in="formData",
* description="The Venue ID of this Display",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="address",
* in="formData",
* description="The Location Address of this Display",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="isMobile",
* in="formData",
* description="Is this Display mobile?",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="isOutdoor",
* in="formData",
* description="Is this Display Outdoor?",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="costPerPlay",
* in="formData",
* description="The Cost Per Play of this Display",
* type="number",
* required=false
* ),
* @SWG\Parameter(
* name="impressionsPerPlay",
* in="formData",
* description="The Impressions Per Play of this Display",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="customId",
* in="formData",
* description="The custom ID (an Id of any external system) of this Display",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ref1",
* in="formData",
* description="Reference 1",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ref2",
* in="formData",
* description="Reference 2",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ref3",
* in="formData",
* description="Reference 3",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ref4",
* in="formData",
* description="Reference 4",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ref5",
* in="formData",
* description="Reference 5",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="clearCachedData",
* in="formData",
* description="Clear all Cached data for this display",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="rekeyXmr",
* in="formData",
* description="Clear the cached XMR configuration and send a rekey",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="teamViewerSerial",
* in="formData",
* description="The TeamViewer serial number for this Display, if applicable",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="webkeySerial",
* in="formData",
* description="The Webkey serial number for this Display, if applicable",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this object should be assigned to",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Display")
* )
* )
*/
function edit(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id, true);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
// Update properties
if ($this->getConfig()->getSetting('DISPLAY_LOCK_NAME_TO_DEVICENAME') == 0) {
$display->display = $sanitizedParams->getString('display');
}
$display->load();
$display->description = $sanitizedParams->getString('description');
$display->displayTypeId = $sanitizedParams->getInt('displayTypeId');
$display->venueId = $sanitizedParams->getInt('venueId');
$display->address = $sanitizedParams->getString('address');
$display->isMobile = $sanitizedParams->getCheckbox('isMobile');
$languages = $sanitizedParams->getArray('languages');
if (empty($languages)) {
$display->languages = null;
} else {
$display->languages = implode(',', $languages);
}
$display->screenSize = $sanitizedParams->getInt('screenSize');
$display->auditingUntil = $sanitizedParams->getDate('auditingUntil')?->format('U');
$display->defaultLayoutId = $sanitizedParams->getInt('defaultLayoutId');
$display->licensed = $sanitizedParams->getInt('licensed');
$display->license = $sanitizedParams->getString('license');
$display->incSchedule = $sanitizedParams->getInt('incSchedule');
$display->emailAlert = $sanitizedParams->getInt('emailAlert');
$display->alertTimeout = $sanitizedParams->getCheckbox('alertTimeout');
$display->latitude = $sanitizedParams->getDouble('latitude');
$display->longitude = $sanitizedParams->getDouble('longitude');
$display->timeZone = $sanitizedParams->getString('timeZone');
$display->displayProfileId = $sanitizedParams->getInt('displayProfileId');
$display->bandwidthLimit = $sanitizedParams->getInt('bandwidthLimit', ['default' => 0]);
$display->teamViewerSerial = $sanitizedParams->getString('teamViewerSerial');
$display->webkeySerial = $sanitizedParams->getString('webkeySerial');
$display->folderId = $sanitizedParams->getInt('folderId', ['default' => $display->folderId]);
$display->isOutdoor = $sanitizedParams->getCheckbox('isOutdoor');
$display->costPerPlay = $sanitizedParams->getDouble('costPerPlay');
$display->impressionsPerPlay = $sanitizedParams->getDouble('impressionsPerPlay');
$display->customId = $sanitizedParams->getString('customId');
$display->ref1 = $sanitizedParams->getString('ref1');
$display->ref2 = $sanitizedParams->getString('ref2');
$display->ref3 = $sanitizedParams->getString('ref3');
$display->ref4 = $sanitizedParams->getString('ref4');
$display->ref5 = $sanitizedParams->getString('ref5');
// Wake on Lan
if (defined('ACCOUNT_ID')) {
// WOL is not allowed on a Xibo Cloud CMS
// Force disable, but leave the other settings as they are.
$display->wakeOnLanEnabled = 0;
} else {
$display->wakeOnLanEnabled = $sanitizedParams->getCheckbox('wakeOnLanEnabled');
$display->wakeOnLanTime = $sanitizedParams->getString('wakeOnLanTime');
$display->broadCastAddress = $sanitizedParams->getString('broadCastAddress');
$display->secureOn = $sanitizedParams->getString('secureOn');
$display->cidr = $sanitizedParams->getString('cidr');
}
// Get the display profile and use that to pull in any overrides
// start with an empty config
$display->overrideConfig = $this->editConfigFields(
$display->getDisplayProfile(),
$sanitizedParams,
[],
$display
);
// Tags are stored on the displaygroup, we're just passing through here
if ($this->getUser()->featureEnabled('tag.tagging')) {
if (is_array($sanitizedParams->getParam('tags'))) {
$tags = $this->tagFactory->tagsFromJson($sanitizedParams->getArray('tags'));
} else {
$tags = $this->tagFactory->tagsFromString($sanitizedParams->getString('tags'));
}
$display->tags = $tags;
}
// Should we invalidate this display?
if ($display->hasPropertyChanged('defaultLayoutId')) {
$display->notify();
} elseif ($sanitizedParams->getCheckbox('clearCachedData', ['default' => 1]) == 1) {
// Remove the cache if the display licenced state has changed
$this->pool->deleteItem($display->getCacheKey());
}
// Should we rekey?
if ($sanitizedParams->getCheckbox('rekeyXmr', ['default' => 0]) == 1) {
// Queue the rekey action first (before we clear the channel and key)
$this->playerAction->sendAction($display, new RekeyAction());
// Clear the config.
$display->xmrChannel = null;
$display->xmrPubKey = null;
}
$display->save();
if ($this->isApi($request)) {
$display->lastAccessed = Carbon::createFromTimestamp($display->lastAccessed)
->format(DateFormatHelper::getSystemFormat());
$display->auditingUntil = ($display->auditingUntil == 0)
? 0
: Carbon::createFromTimestamp($display->auditingUntil)->format(DateFormatHelper::getSystemFormat());
}
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $display->display),
'id' => $display->displayId,
'data' => $display
]);
return $this->render($request, $response);
}
/**
* Delete a display
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Delete(
* path="/display/{displayId}",
* operationId="displayDelete",
* tags={"display"},
* summary="Display Delete",
* description="Delete a Display",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
function delete(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkDeleteable($display)) {
throw new AccessDeniedException();
}
if ($display->isLead()) {
throw new InvalidArgumentException(
__('Cannot delete a Lead Display of a Sync Group'),
);
}
$display->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $display->display),
'id' => $display->displayId,
'data' => $display
]);
return $this->render($request, $response);
}
/**
* Member of Display Groups Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function membershipForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
// Groups we are assigned to
$groupsAssigned = $this->displayGroupFactory->getByDisplayId($display->displayId);
$this->getState()->template = 'display-form-membership';
$this->getState()->setData([
'display' => $display,
'extra' => [
'displayGroupsAssigned' => $groupsAssigned
],
]);
return $this->render($request, $response);
}
/**
* Set Bandwidth to one or more displays
* @param Request $request
* @param Response $response
* @param $ids
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function setBandwidthLimitMultipleForm(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// Check if the array of ids is passed
if ($sanitizedParams->getString('ids') == '') {
throw new InvalidArgumentException(__('The array of ids is empty!'));
}
// Get array of ids
$ids = $sanitizedParams->getString('ids');
$this->getState()->template = 'display-form-set-bandwidth';
$this->getState()->setData([
'ids' => $ids,
]);
return $this->render($request, $response);
}
/**
* Set Bandwidth to one or more displays
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function setBandwidthLimitMultiple(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// Get array of ids
$ids = ($sanitizedParams->getString('ids') != '') ? explode(',', $sanitizedParams->getString('ids')) : [];
$bandwidthLimit = intval($sanitizedParams->getString('bandwidthLimit'));
$bandwidthLimitUnits = $sanitizedParams->getString('bandwidthLimitUnits');
// Check if the array of ids is passed
if (count($ids) == 0) {
throw new InvalidArgumentException(__('The array of ids is empty!'));
}
// Check if the bandwidth value has something
if ($bandwidthLimit == '') {
throw new InvalidArgumentException(__('The array of ids is empty!'));
}
// convert bandwidth to kb based on form units
if ($bandwidthLimitUnits == 'mb') {
$bandwidthLimit = $bandwidthLimit * 1024;
} elseif ($bandwidthLimitUnits == 'gb') {
$bandwidthLimit = $bandwidthLimit * 1024 * 1024;
}
// display group ids to be updated
$displayGroupIds = [];
foreach ($ids as $id) {
// get display
$display = $this->displayFactory->getById($id);
// check if the display is accessible by user
if (!$this->getUser()->checkViewable($display)) {
throw new AccessDeniedException();
}
$displayGroupIds[] = $display->displayGroupId;
}
// update bandwidth limit to the array of ids
$this->displayGroupFactory->setBandwidth($bandwidthLimit, $displayGroupIds);
// Audit Log message
$this->getLog()->audit('DisplayGroup', 0, 'Batch update of bandwidth limit for ' . count($displayGroupIds) . ' items', [
'bandwidthLimit' => $bandwidthLimit,
'displayGroupIds' => $displayGroupIds
]);
// Return
$this->getState()->hydrate([
'httpCode' => 204,
'message' => __('Displays Updated')
]);
return $this->render($request, $response);
}
/**
* Assign Display to Display Groups
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function assignDisplayGroup(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
// Go through each ID to assign
foreach ($sanitizedParams->getIntArray('displayGroupId', ['default' => []]) as $displayGroupId) {
$displayGroup = $this->displayGroupFactory->getById($displayGroupId);
$displayGroup->load();
$this->getDispatcher()->dispatch(new DisplayGroupLoadEvent($displayGroup), DisplayGroupLoadEvent::$NAME);
if (!$this->getUser()->checkEditable($displayGroup)) {
throw new AccessDeniedException(__('Access Denied to DisplayGroup'));
}
$displayGroup->assignDisplay($display);
$displayGroup->save(['validate' => false]);
}
// Have we been provided with unassign id's as well?
foreach ($sanitizedParams->getIntArray('unassignDisplayGroupId', ['default' => []]) as $displayGroupId) {
$displayGroup = $this->displayGroupFactory->getById($displayGroupId);
$displayGroup->load();
$this->getDispatcher()->dispatch(new DisplayGroupLoadEvent($displayGroup), DisplayGroupLoadEvent::$NAME);
if (!$this->getUser()->checkEditable($displayGroup)) {
throw new AccessDeniedException(__('Access Denied to DisplayGroup'));
}
$displayGroup->unassignDisplay($display);
$displayGroup->save(['validate' => false]);
}
// Queue display to check for cache updates
$display->notify();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('%s assigned to Display Groups'), $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
/**
* Output a screen shot
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function screenShot(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
// Allow limited view access
if (!$this->getUser()->checkViewable($display) && !$this->getUser()->featureEnabled('displays.limitedView')) {
throw new AccessDeniedException();
}
// The request will output its own content, disable framework
$this->setNoOutput(true);
// Output an image if present, otherwise not found image.
$file = 'screenshots/' . $id . '_screenshot.jpg';
// File upload directory.. get this from the settings object
$library = $this->getConfig()->getSetting('LIBRARY_LOCATION');
$fileName = $library . $file;
if (!file_exists($fileName)) {
$fileName = $this->getConfig()->uri('forms/filenotfound.gif');
}
Img::configure(array('driver' => 'gd'));
$img = Img::make($fileName);
$date = $display->getCurrentScreenShotTime($this->pool);
if ($date != '') {
$img
->rectangle(0, 0, 110, 15, function ($draw) {
$draw->background('#ffffff');
})
->text($date, 10, 10);
}
// Cache headers
header('Cache-Control: no-store, no-cache, must-revalidate');
header('Pragma: no-cache');
header('Expires: 0');
// Disable any buffering to prevent OOM errors.
while (ob_get_level() > 0) {
ob_end_clean();
}
$response->write($img->encode());
$response = $response->withHeader('Content-Type', $img->mime());
return $this->render($request, $response);
}
/**
* Request ScreenShot form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function requestScreenShotForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
// Allow limited view access
if (!$this->getUser()->checkViewable($display) && !$this->getUser()->featureEnabled('displays.limitedView')) {
throw new AccessDeniedException();
}
// Work out the next collection time based on the last accessed date/time and the collection interval
if ($display->lastAccessed == 0) {
$nextCollect = __('once it has connected for the first time');
} else {
$collectionInterval = $display->getSetting('collectInterval', 300);
$nextCollect = Carbon::createFromTimestamp($display->lastAccessed)
->addSeconds($collectionInterval)
->diffForHumans();
}
$this->getState()->template = 'display-form-request-screenshot';
$this->getState()->autoSubmit = $this->getAutoSubmit('displayRequestScreenshotForm');
$this->getState()->setData([
'display' => $display,
'nextCollect' => $nextCollect,
]);
return $this->render($request, $response);
}
/**
* Request ScreenShot
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/display/requestscreenshot/{displayId}",
* operationId="displayRequestScreenshot",
* tags={"display"},
* summary="Request Screen Shot",
* description="Notify the display that the CMS would like a screen shot to be sent.",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Display")
* )
* )
*/
public function requestScreenShot(Request $request, Response $response, $id): Response
{
$display = $this->displayFactory->getById($id);
// Allow limited view access
if (!$this->getUser()->checkViewable($display) && !$this->getUser()->featureEnabled('displays.limitedView')) {
throw new AccessDeniedException();
}
$display->screenShotRequested = 1;
$display->save(['validate' => false, 'audit' => false]);
if (!empty($display->xmrChannel)) {
$this->playerAction->sendAction($display, new ScreenShotAction());
}
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Request sent for %s'), $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
/**
* Form for wake on Lan
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function wakeOnLanForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display)) {
throw new AccessDeniedException();
}
if ($display->macAddress == '') {
throw new InvalidArgumentException(
__('This display has no mac address recorded against it yet. Make sure the display is running.'),
'macAddress'
);
}
$this->getState()->template = 'display-form-wakeonlan';
$this->getState()->setData([
'display' => $display,
]);
return $this->render($request, $response);
}
/**
* Wake this display using a WOL command
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Post(
* path="/display/wol/{displayId}",
* operationId="displayWakeOnLan",
* tags={"display"},
* summary="Issue WOL",
* description="Send a Wake On LAN packet to this Display",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function wakeOnLan(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display)) {
throw new AccessDeniedException();
}
if ($display->macAddress == '' || $display->broadCastAddress == '') {
throw new InvalidArgumentException(
__('This display has no mac address recorded against it yet. Make sure the display is running.')
);
}
$this->getLog()->notice(
'About to send WOL packet to '
. $display->broadCastAddress . ' with Mac Address ' . $display->macAddress
);
WakeOnLan::TransmitWakeOnLan(
$display->macAddress,
$display->secureOn,
$display->broadCastAddress,
$display->cidr,
'9',
$this->getLog()
);
$display->lastWakeOnLanCommandSent = Carbon::now()->format('U');
$display->save(['validate' => false]);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Wake on Lan sent for %s'), $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
/**
* Validate the display list
* @param \Xibo\Entity\Display[] $displays
* @throws \Xibo\Support\Exception\GeneralException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function validateDisplays(array $displays): void
{
// Get the global time out (overrides the alert time out on the display if 0)
$globalTimeout = $this->getConfig()->getSetting('MAINTENANCE_ALERT_TOUT') * 60;
$emailAlerts = ($this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') == 1);
$alwaysAlert = ($this->getConfig()->getSetting('MAINTENANCE_ALWAYS_ALERT') == 1);
foreach ($displays as $display) {
// Should we test against the collection interval or the preset alert timeout?
if ($display->alertTimeout == 0 && $display->clientType != '') {
$timeoutToTestAgainst = ((double)$display->getSetting('collectInterval', $globalTimeout)) * 1.1;
} else {
$timeoutToTestAgainst = $globalTimeout;
}
// Store the timeout to test against
$timeOut = $display->lastAccessed + $timeoutToTestAgainst;
// If the last time we accessed is less than now minus the timeout
if ($timeOut < Carbon::now()->format('U')) {
$this->getLog()->debug('Timed out display. Last Accessed: '
. date('Y-m-d h:i:s', $display->lastAccessed) . '. Time out: ' . date('Y-m-d h:i:s', $timeOut));
// Is this the first time this display has gone "off-line"
$displayOffline = ($display->loggedIn == 1);
// If this is the first switch (i.e. the row was logged in before)
if ($displayOffline) {
// Update the display and set it as logged out
$display->loggedIn = 0;
$display->save(\Xibo\Entity\Display::$saveOptionsMinimum);
// Log the down event
$event = $this->displayEventFactory->createEmpty();
$event->displayId = $display->displayId;
$event->start = $display->lastAccessed;
// eventTypeId 1 is for Display up/down events.
$event->eventTypeId = 1;
$event->save();
}
$dayPartId = $display->getSetting('dayPartId');
$operatingHours = true;
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();
// exceptions
foreach ($dayPart->exceptions as $exception) {
// check if we are on exception day and if so override the start and endtime accordingly
if ($exception['day'] == Carbon::now()->format('D')) {
$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.
if (($now >= $startTime && $now <= $endTime)) {
$operatingHours = true;
} else {
$operatingHours = false;
}
} catch (NotFoundException) {
$this->getLog()->debug(
'Unknown dayPartId set on Display Profile for displayId ' . $display->displayId
);
}
}
// Should we create a notification
if ($emailAlerts && $display->emailAlert == 1 && ($displayOffline || $alwaysAlert)) {
// Alerts enabled for this display
// Display just gone offline, or always alert
// Fields for email
// for displays without dayPartId set, this is always true,
// otherwise we check if we are inside the operating hours set for this display
if ($operatingHours) {
$subject = sprintf(__('Alert for Display %s'), $display->display);
$body = sprintf(
__('Display ID %d is offline since %s.'),
$display->displayId,
Carbon::createFromTimestamp($display->lastAccessed)
->format(DateFormatHelper::getSystemFormat())
);
// Add to system
$notification = $this->notificationFactory->createSystemNotification(
$subject,
$body,
Carbon::now(),
'display'
);
// Add in any displayNotificationGroups, with permissions
foreach ($this->userGroupFactory
->getDisplayNotificationGroups($display->displayGroupId) as $group) {
$notification->assignUserGroup($group);
}
$notification->save();
} else {
$this->getLog()->info('Not sending email down alert for Display - ' . $display->display
. ' we are outside of its operating hours');
}
} elseif ($displayOffline) {
$this->getLog()->info('Not sending an email for offline display - emailAlert = '
. $display->emailAlert . ', alwaysAlert = ' . $alwaysAlert);
}
}
}
}
/**
* Show the authorise form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function authoriseForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'display-form-authorise';
$this->getState()->autoSubmit = $this->getAutoSubmit('displayAuthoriseForm');
$this->getState()->setData([
'display' => $display
]);
return $this->render($request, $response);
}
/**
* Toggle Authorise on this Display
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/display/authorise/{displayId}",
* operationId="displayToggleAuthorise",
* tags={"display"},
* summary="Toggle authorised",
* description="Toggle authorised for the Display.",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function toggleAuthorise(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
$display->licensed = ($display->licensed == 1) ? 0 : 1;
$display->save(['validate' => false]);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Authorised set to %d for %s'), $display->licensed, $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function defaultLayoutForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
// Get the currently assigned default layout
try {
$layouts = (($display->defaultLayoutId != null) ? [$this->layoutFactory->getById($display->defaultLayoutId)] : []);
} catch (NotFoundException $notFoundException) {
$layouts = [];
}
$this->getState()->template = 'display-form-defaultlayout';
$this->getState()->setData([
'display' => $display,
'layouts' => $layouts
]);
return $this->render($request, $response);
}
/**
* Set the Default Layout for this Display
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/display/defaultlayout/{displayId}",
* operationId="displayDefaultLayout",
* tags={"display"},
* summary="Set Default Layout",
* description="Set the default Layout on this Display",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="layoutId",
* in="formData",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function setDefaultLayout(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
$layoutId = $this->getSanitizer($request->getParams())->getInt('layoutId');
$layout = $this->layoutFactory->getById($layoutId);
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException();
}
$display->defaultLayoutId = $layoutId;
$display->save(['validate' => false]);
if ($display->hasPropertyChanged('defaultLayoutId')) {
$display->notify();
}
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Default Layout with name %s set for %s'), $layout->layout, $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function moveCmsForm(Request $request, Response $response, $id)
{
if ($this->getUser()->twoFactorTypeId != 2) {
throw new AccessDeniedException('This action requires active Google Authenticator Two Factor authentication');
}
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'display-form-moveCms';
$this->getState()->setData([
'display' => $display,
'newCmsAddress' => $display->newCmsAddress,
'newCmsKey' => $display->newCmsKey
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \RobThree\Auth\TwoFactorAuthException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function moveCms(Request $request, Response $response, $id)
{
if ($this->getUser()->twoFactorTypeId != 2) {
throw new AccessDeniedException('This action requires active Google Authenticator Two Factor authentication');
}
$display = $this->displayFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
// Two Factor Auth
$issuerSettings = $this->getConfig()->getSetting('TWOFACTOR_ISSUER');
$appName = $this->getConfig()->getThemeConfig('app_name');
if ($issuerSettings !== '') {
$issuer = $issuerSettings;
} else {
$issuer = $appName;
}
$authenticationCode = $sanitizedParams->getString('twoFactorCode');
$tfa = new TwoFactorAuth($issuer);
$result = $tfa->verifyCode($this->getUser()->twoFactorSecret, $authenticationCode, 3);
if ($result) {
// get the new CMS Address and Key from the form.
$newCmsAddress = $sanitizedParams->getString('newCmsAddress');
$newCmsKey = $sanitizedParams->getString('newCmsKey');
// validate the URL
if (!v::url()->notEmpty()->validate(urldecode($newCmsAddress)) || !filter_var($newCmsAddress, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException(__('Provided CMS URL is invalid'), 'newCmsUrl');
}
if (!v::stringType()->length(1, 1000)->validate($newCmsAddress)) {
throw new InvalidArgumentException(__('New CMS URL can have maximum of 1000 characters'), 'newCmsUrl');
}
if ($newCmsKey == '') {
throw new InvalidArgumentException(__('Provided CMS Key is invalid'), 'newCmsKey');
}
// we are successfully authenticated, get new CMS address and Key and save the Display record.
$display->newCmsAddress = $newCmsAddress;
$display->newCmsKey = $newCmsKey;
$display->save();
} else {
throw new InvalidArgumentException(__('Invalid Two Factor Authentication Code'), 'twoFactorCode');
}
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws NotFoundException
*/
public function moveCmsCancelForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'display-form-moveCmsCancel';
$this->getState()->setData([
'display' => $display
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @param $id
* @throws NotFoundException
* @throws GeneralException
*/
public function moveCmsCancel(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkEditable($display)) {
throw new AccessDeniedException();
}
$display->newCmsAddress = '';
$display->newCmsKey = '';
$display->save();
$this->getState()->hydrate([
'message' => sprintf(__('Cancelled CMS Transfer for %s'), $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function addViaCodeForm(Request $request, Response $response)
{
$this->getState()->template = 'display-form-addViaCode';
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function addViaCode(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$user_code = $sanitizedParams->getString('user_code');
$cmsAddress = (new HttpsDetect())->getBaseUrl($request);
$cmsKey = $this->getConfig()->getSetting('SERVER_KEY');
if ($user_code == '') {
throw new InvalidArgumentException(__('Code cannot be empty'), 'code');
}
$guzzle = new Client();
try {
// When the valid code is submitted, it will be sent along with CMS Address and Key to Authentication Service maintained by Xibo Signage Ltd.
// The Player will then call the service with the same code to retrieve the CMS details.
// On success, the details will be removed from the Authentication Service.
$guzzleRequest = $guzzle->request(
'POST',
'https://auth.signlicence.co.uk/addDetails',
$this->getConfig()->getGuzzleProxy([
'form_params' => [
'user_code' => $user_code,
'cmsAddress' => $cmsAddress,
'cmsKey' => $cmsKey,
]
])
);
$data = json_decode($guzzleRequest->getBody(), true);
$this->getState()->hydrate([
'message' => $data['message']
]);
} catch (\Exception $e) {
$this->getLog()->debug($e->getMessage());
throw new InvalidArgumentException(__('The code provided does not match. Please double-check the code shown on the device you are trying to connect.'), 'user_code');
}
return $this->render($request, $response);
}
/**
* Check commercial licence form
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function checkLicenceForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'display-form-licence-check';
$this->getState()->autoSubmit = $this->getAutoSubmit('displayLicenceCheckForm');
$this->getState()->setData([
'display' => $display
]);
return $this->render($request, $response);
}
/**
* Check commercial licence
*
* @SWG\Put(
* summary="Licence Check",
* path="/display/licenceCheck/{displayId}",
* operationId="displayLicenceCheck",
* tags={"display"},
* description="Ask this Player to check its Commercial Licence",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function checkLicence(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display)) {
throw new AccessDeniedException();
}
if (empty($display->xmrChannel)) {
throw new InvalidArgumentException(__('XMR is not configured for this Display'), 'xmrChannel');
}
$this->playerAction->sendAction($display, new LicenceCheckAction());
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Request sent for %s'), $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/display/status/{id}",
* operationId="displayStatus",
* tags={"display"},
* summary="Display Status",
* description="Get the display status window for this Display.",
* @SWG\Parameter(
* name="id",
* in="path",
* description="Display Id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(type="string")
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param int $id displayId
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\AccessDeniedException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
*/
public function statusWindow(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display)) {
throw new AccessDeniedException();
}
return $response->withJson($display->getStatusWindow($this->pool));
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function purgeAllForm(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display) || !$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException();
}
$this->getState()->template = 'display-form-purge-all';
$this->getState()->setData([
'display' => $display
]);
return $this->render($request, $response);
}
/**
* Purge All
*
* @SWG\Put(
* summary="Purge All",
* path="/display/purgeAll/{displayId}",
* operationId="displayPurgeAll",
* tags={"display"},
* description="Ask this Player to purge all Media from its local storage and request fresh files from CMS.",
* @SWG\Parameter(
* name="displayId",
* in="path",
* description="The Display ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function purgeAll(Request $request, Response $response, $id)
{
$display = $this->displayFactory->getById($id);
if (!$this->getUser()->checkViewable($display) || !$this->getUser()->isSuperAdmin()) {
throw new AccessDeniedException();
}
if (empty($display->xmrChannel)) {
throw new InvalidArgumentException(__('XMR is not configured for this Display'), 'xmrChannel');
}
$this->playerAction->sendAction($display, new PurgeAllAction());
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Request sent for %s'), $display->display),
'id' => $display->displayId
]);
return $this->render($request, $response);
}
}