HEX
Server: Apache
System: Linux server2.voipitup.com.au 4.18.0-553.109.1.lve.el8.x86_64 #1 SMP Thu Mar 5 20:23:46 UTC 2026 x86_64
User: posscale (1027)
PHP: 8.2.30
Disabled: exec,passthru,shell_exec,system
Upload Files
File: /home/posscale/subdomains/xibo/lib/Controller/Display.php
<?php
/*
 * Xibo - Digital Signage - http://www.xibo.org.uk
 * Copyright (C) 2006-2014 Daniel Garner
 *
 * 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 finfo;
use Stash\Interfaces\PoolInterface;
use Xibo\Entity\RequiredFile;
use Xibo\Exception\AccessDeniedException;
use Xibo\Exception\ConfigurationException;
use Xibo\Exception\NotFoundException;
use Xibo\Factory\DisplayEventFactory;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\DisplayProfileFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\LogFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\RequiredFileFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Factory\TagFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\WakeOnLan;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\DateServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Service\PlayerActionServiceInterface;
use Xibo\Service\SanitizerServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\XMR\RekeyAction;
use Xibo\XMR\ScreenShotAction;

/**
 * Class Display
 * @package Xibo\Controller
 */
class Display extends Base
{
    /**
     * @var StorageServiceInterface
     */
    private $store;

    /**
     * @var PoolInterface
     */
    private $pool;

    /**
     * @var PlayerActionServiceInterface
     */
    private $playerAction;

    /**
     * @var DisplayFactory
     */
    private $displayFactory;

    /**
     * @var DisplayGroupFactory
     */
    private $displayGroupFactory;

    /**
     * @var LogFactory
     */
    private $logFactory;

    /**
     * @var LayoutFactory
     */
    private $layoutFactory;

    /**
     * @var DisplayProfileFactory
     */
    private $displayProfileFactory;

    /**
     * @var MediaFactory
     */
    private $mediaFactory;

    /**
     * @var ScheduleFactory
     */
    private $scheduleFactory;

    /** @var  DisplayEventFactory */
    private $displayEventFactory;

    /** @var  RequiredFileFactory */
    private $requiredFileFactory;

    /** @var  TagFactory */
    private $tagFactory;

    /**
     * Set common dependencies.
     * @param LogServiceInterface $log
     * @param SanitizerServiceInterface $sanitizerService
     * @param \Xibo\Helper\ApplicationState $state
     * @param \Xibo\Entity\User $user
     * @param \Xibo\Service\HelpServiceInterface $help
     * @param DateServiceInterface $date
     * @param ConfigServiceInterface $config
     * @param StorageServiceInterface $store
     * @param PoolInterface $pool
     * @param PlayerActionServiceInterface $playerAction
     * @param DisplayFactory $displayFactory
     * @param DisplayGroupFactory $displayGroupFactory
     * @param LogFactory $logFactory
     * @param LayoutFactory $layoutFactory
     * @param DisplayProfileFactory $displayProfileFactory
     * @param MediaFactory $mediaFactory
     * @param ScheduleFactory $scheduleFactory
     * @param DisplayEventFactory $displayEventFactory
     * @param RequiredFileFactory $requiredFileFactory
     * @param TagFactory $tagFactory
     */
    public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $store, $pool, $playerAction, $displayFactory, $displayGroupFactory, $logFactory, $layoutFactory, $displayProfileFactory, $mediaFactory, $scheduleFactory, $displayEventFactory, $requiredFileFactory, $tagFactory)
    {
        $this->setCommonDependencies($log, $sanitizerService, $state, $user, $help, $date, $config);

        $this->store = $store;
        $this->pool = $pool;
        $this->playerAction = $playerAction;
        $this->displayFactory = $displayFactory;
        $this->displayGroupFactory = $displayGroupFactory;
        $this->logFactory = $logFactory;
        $this->layoutFactory = $layoutFactory;
        $this->displayProfileFactory = $displayProfileFactory;
        $this->mediaFactory = $mediaFactory;
        $this->scheduleFactory = $scheduleFactory;
        $this->displayEventFactory = $displayEventFactory;
        $this->requiredFileFactory = $requiredFileFactory;
        $this->tagFactory = $tagFactory;
    }

    /**
     * Include display page template page based on sub page selected
     */
    function displayPage()
    {
        // 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';
        $this->getState()->setData([
            'displayGroups' => $this->displayGroupFactory->query(),
            'displayProfiles' => $displayProfiles
        ]);
    }

    /**
     * Display Management Page for an Individual Display
     * @param int $displayId
     * @throws \Xibo\Exception\NotFoundException
     */
    function displayManage($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        if (!$this->getUser()->checkViewable($display))
            throw new AccessDeniedException();

        // Zero out some variables
        $layouts = [];
        $widgets = [];
        $media = [];
        $totalCount = 0;
        $completeCount = 0;
        $totalSize = 0;
        $completeSize = 0;


        // Show 3 widgets
        $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' => $displayId, 'type' => 'L']) as $row) {
            /** @var RequiredFile $rf */
            $rf = $this->requiredFileFactory->getByDisplayAndLayout($displayId, $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' => $displayId, 'type' => 'M']) as $row) {
            /** @var RequiredFile $rf */
            $rf = $this->requiredFileFactory->getByDisplayAndMedia($displayId, $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,
                IFNULL(`widgetoption`.value, `widget`.type) 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 = :type
          ORDER BY IFNULL(`widgetoption`.value, `widget`.type)
        ';

        foreach ($this->store->select($sql, ['displayId' => $displayId, 'type' => 'W']) as $row) {
            /** @var RequiredFile $rf */
            $rf = $this->requiredFileFactory->getByDisplayAndWidget($displayId, $row['widgetId']);

            $totalCount++;

            if ($rf->complete) {
                $completeCount = $completeCount + 1;
            }

            $rf = $rf->toArray();
            $rf['type'] = $row['widgetType'];
            $rf['widgetName'] = $row['widgetName'];
            $widgets[] = $rf;
        }

        // 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 = (isset($suffixes[$base]) ? $suffixes[$base] : '');
        $this->getLog()->debug('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' => $this->getDate()->parse($display->lastAccessed, 'U')->diffForHumans(),
            'errorSearch' => http_build_query([
                'displayId' => $display->displayId,
                'type' => 'ERROR',
                'fromDt' => $this->getDate()->getLocalDate($this->getDate()->parse()->subHours(12)),
                'toDt' => $this->getDate()->getLocalDate()
            ]),
            'inventory' => [
                'layouts' => $layouts,
                'media' => $media,
                'widgets' => $widgets
            ],
            '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' => $this->getDate()->getLocalDate(time() - (86400 * 35)),
                'fromDateOneDay' => $this->getDate()->getLocalDate(time() - 86400),
                'toDate' => $this->getDate()->getLocalDate()
            ]
        ]);
    }

    /**
     * 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="formData",
     *      description="Filter by Display Id",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="displayGroupId",
     *      in="formData",
     *      description="Filter by DisplayGroup Id",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="display",
     *      in="formData",
     *      description="Filter by Display Name",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="macAddress",
     *      in="formData",
     *      description="Filter by Mac Address",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="hardwareKey",
     *      in="formData",
     *      description="Filter by Hardware Key",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="clientVersion",
     *      in="formData",
     *      description="Filter by Client Version",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="embed",
     *      in="formData",
     *      description="Embed related data, namely displaygroups. A comma separated list of child objects to embed.",
     *      type="string",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="authorised",
     *      in="formData",
     *      description="Filter by authorised flag",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Parameter(
     *      name="displayProfileId",
     *      in="formData",
     *      description="Filter by Display Profile",
     *      type="integer",
     *      required=false
     *   ),
     *  @SWG\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(
     *          type="array",
     *          @SWG\Items(ref="#/definitions/Display")
     *      )
     *  )
     * )
     */
    function grid()
    {
        // Embed?
        $embed = ($this->getSanitizer()->getString('embed') != null) ? explode(',', $this->getSanitizer()->getString('embed')) : [];

        $filter = [
            'displayId' => $this->getSanitizer()->getInt('displayId'),
            'display' => $this->getSanitizer()->getString('display'),
            'macAddress' => $this->getSanitizer()->getString('macAddress'),
            'license' => $this->getSanitizer()->getString('hardwareKey'),
            'displayGroupId' => $this->getSanitizer()->getInt('displayGroupId'),
            'clientVersion' => $this->getSanitizer()->getString('clientVersion'),
            'authorised' => $this->getSanitizer()->getInt('authorised'),
            'displayProfileId' => $this->getSanitizer()->getInt('displayProfileId'),
            'tags' => $this->getSanitizer()->getString('tags'),
            'exactTags' => $this->getSanitizer()->getCheckbox('exactTags'),
            'showTags' => true,
        ];

        // Get a list of displays
        $displays = $this->displayFactory->query($this->gridRenderSort(), $this->gridRenderFilter($filter));

        // 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');
            }

            // Current layout from cache
            $display->setChildObjectDependencies($this->layoutFactory, $this->mediaFactory, $this->scheduleFactory);
            $display->setCurrentLayoutId($this->pool);

            if ($this->isApi())
                break;

            // Add in the display profile information
            $display->displayProfile = (!array_key_exists($display->displayProfileId, $displayProfiles)) ? __('Default') : $displayProfiles[$display->displayProfileId];

            $display->includeProperty('buttons');

            // Format the storage available / total space
            $display->storageAvailableSpaceFormatted = ByteFormatter::format($display->storageAvailableSpace);
            $display->storageTotalSpaceFormatted = ByteFormatter::format($display->storageTotalSpace);
            $display->storagePercentage = ($display->storageTotalSpace == 0) ? 0 : round($display->storageAvailableSpace / $display->storageTotalSpace * 100.0, 2);

            // Set some text for the display status
            switch ($display->mediaInventoryStatus) {
                case 1:
                    $display->statusDescription = __('Display is up to date');
                    break;

                case 2:
                    $display->statusDescription = __('Display is downloading new files');
                    break;

                case 3:
                    $display->statusDescription = __('Display is out of date but has not yet checked in with the server');
                    break;

                default:
                    $display->statusDescription = __('Unknown Display Status');
            }

            // Thumbnail
            $display->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->thumbnail = $this->urlFor('display.screenShot', ['id' => $display->displayId]);
            }

            // Edit and Delete buttons first
            if ($this->getUser()->checkEditable($display)) {

                // Manage
                $display->buttons[] = array(
                    'id' => 'display_button_manage',
                    'url' => $this->urlFor('display.manage', ['id' => $display->displayId]),
                    'text' => __('Manage'),
                    'external' => true
                );

                $display->buttons[] = ['divider' => true];

                // Edit
                $display->buttons[] = array(
                    'id' => 'display_button_edit',
                    'url' => $this->urlFor('display.edit.form', ['id' => $display->displayId]),
                    'text' => __('Edit')
                );
            }

            // Delete
            if ($this->getUser()->checkDeleteable($display)) {
                $display->buttons[] = array(
                    'id' => 'display_button_delete',
                    'url' => $this->urlFor('display.delete.form', ['id' => $display->displayId]),
                    'text' => __('Delete')
                );
            }

            if ($this->getUser()->checkEditable($display) || $this->getUser()->checkDeleteable($display)) {
                $display->buttons[] = ['divider' => true];
            }

            // Schedule Now
            if ($this->getUser()->checkEditable($display) || $this->getConfig()->GetSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 'Yes') {
                $display->buttons[] = array(
                    'id' => 'display_button_schedulenow',
                    'url' => $this->urlFor('schedule.now.form', ['id' => $display->displayGroupId, 'from' => 'DisplayGroup']),
                    'text' => __('Schedule Now')
                );
            }

            if ($this->getUser()->checkEditable($display)) {

                // File Associations
                $display->buttons[] = array(
                    'id' => 'displaygroup_button_fileassociations',
                    'url' => $this->urlFor('displayGroup.media.form', ['id' => $display->displayGroupId]),
                    'text' => __('Assign Files')
                );

                // Layout Assignments
                $display->buttons[] = array(
                    'id' => 'displaygroup_button_layout_associations',
                    'url' => $this->urlFor('displayGroup.layout.form', ['id' => $display->displayGroupId]),
                    'text' => __('Assign Layouts')
                );

                // Screen Shot
                $display->buttons[] = array(
                    'id' => 'display_button_requestScreenShot',
                    'url' => $this->urlFor('display.screenshot.form', ['id' => $display->displayId]),
                    'text' => __('Request Screen Shot'),
                    'multi-select' => true,
                    'dataAttributes' => array(
                        array('name' => 'commit-url', 'value' => $this->urlFor('display.requestscreenshot', ['id' => $display->displayId])),
                        array('name' => 'commit-method', 'value' => 'put'),
                        array('name' => 'id', 'value' => 'display_button_requestScreenShot'),
                        array('name' => 'text', 'value' => __('Request Screen Shot')),
                        array('name' => 'rowtitle', 'value' => $display->display)
                    )
                );

                // Collect Now
                $display->buttons[] = array(
                    'id' => 'display_button_collectNow',
                    'url' => $this->urlFor('displayGroup.collectNow.form', ['id' => $display->displayGroupId]),
                    'text' => __('Collect Now'),
                    'multi-select' => true,
                    'dataAttributes' => array(
                        array('name' => 'commit-url', 'value' => $this->urlFor('displayGroup.action.collectNow', ['id' => $display->displayGroupId])),
                        array('name' => 'commit-method', 'value' => 'post'),
                        array('name' => 'id', 'value' => 'display_button_collectNow'),
                        array('name' => 'text', 'value' => __('Collect Now')),
                        array('name' => 'rowtitle', 'value' => $display->display)
                    )
                );

                $display->buttons[] = ['divider' => true];
            }

            if ($this->getUser()->checkPermissionsModifyable($display)) {

                // Display Groups
                $display->buttons[] = array(
                    'id' => 'display_button_group_membership',
                    'url' => $this->urlFor('display.membership.form', ['id' => $display->displayId]),
                    'text' => __('Display Groups')
                );

                // Permissions
                $display->buttons[] = array(
                    'id' => 'display_button_group_permissions',
                    'url' => $this->urlFor('user.permissions.form', ['entity' => 'DisplayGroup', 'id' => $display->displayGroupId]),
                    'text' => __('Permissions')
                );

                // Version Information
                $display->buttons[] = array(
                    'id' => 'display_button_version_instructions',
                    'url' => $this->urlFor('displayGroup.version.form', ['id' => $display->displayGroupId]),
                    'text' => __('Version Information')
                );
            }

            if ($this->getUser()->checkEditable($display)) {

                if ($this->getUser()->checkPermissionsModifyable($display))
                    $display->buttons[] = ['divider' => true];

                // Wake On LAN
                $display->buttons[] = array(
                    'id' => 'display_button_wol',
                    'url' => $this->urlFor('display.wol.form', ['id' => $display->displayId]),
                    'text' => __('Wake on LAN')
                );

                $display->buttons[] = array(
                    'id' => 'displaygroup_button_command',
                    'url' => $this->urlFor('displayGroup.command.form', ['id' => $display->displayGroupId]),
                    'text' => __('Send Command')
                );
            }
        }

        $this->getState()->template = 'grid';
        $this->getState()->recordsTotal = $this->displayFactory->countLast();
        $this->getState()->setData($displays);
    }

    /**
     * Edit Display Form
     * @param int $displayId
     */
    function editForm($displayId)
    {
        $display = $this->displayFactory->getById($displayId, true);

        if (!$this->getUser()->checkEditable($display))
            throw new AccessDeniedException();

        // Time format for display
        $timeFormat = $this->getDate()->extractTimeFormat($this->getConfig()->GetSetting('DATE_FORMAT'));

        // Dates
        $display->auditingUntilIso = $this->getDate()->getLocalDate($display->auditingUntil);

        // Get the settings from the profile
        $profile = $display->getSettings();

        // Go through each one, and see if it is a drop down
        for ($i = 0; $i < count($profile); $i++) {
            // Always update the value string with the source value
            $profile[$i]['valueString'] = $profile[$i]['value'];

            // Overwrite the value string when we are dealing with dropdowns
            if ($profile[$i]['fieldType'] == 'dropdown') {
                // Update our value
                foreach ($profile[$i]['options'] as $option) {
                    if ($option['id'] == $profile[$i]['value'])
                        $profile[$i]['valueString'] = $option['value'];
                }
            } else if ($profile[$i]['fieldType'] == 'timePicker') {
                // Determine the value and its format
                if ($profile[$i]['value'] == null || $profile[$i]['value'] == '0') {
                    // Empty (new profile)
                    $profile[$i]['valueString'] = $this->getDate()->parse('00:00', 'H:i')->format($timeFormat);
                } else {
                    // A format has been set
                    $format = (strlen($profile[$i]['value']) == 5) ? 'H:i' : 'H:i:s';
                    try {
                        $profile[$i]['valueString'] = $this->getDate()->parse($profile[$i]['value'], $format)->format($timeFormat);
                    } catch (\InvalidArgumentException $invalidArgumentException) {
                        $this->getLog()->error('Display Profile contains an invalid time format, expecting ' . $format . ' value is ' . $profile[$i]['value']);
                        $profile[$i]['valueString'] = '00:00';
                    }
                }
            }
        }

        // Get a list of timezones
        $timeZones = [];
        foreach ($this->getDate()->timezoneList() as $key => $value) {
            $timeZones[] = ['id' => $key, 'value' => $value];
        }

        $layouts = $this->layoutFactory->query(null, ['retired' => 0]);

        if ($display->defaultLayoutId != null) {
            try {
                $layouts = array_merge([$this->layoutFactory->getById($display->defaultLayoutId)], $layouts);
            } catch (NotFoundException $e) {
                $this->getLog()->error('Default layoutId ' . $display->defaultLayoutId . ' not found for displayId ' . $display->displayId);
            }
        }

        $this->getState()->template = 'display-form-edit';
        $this->getState()->setData([
            'display' => $display,
            '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),
            'help' => $this->getHelp()->link('Display', 'Edit')
        ]);
    }

    /**
     * Delete form
     * @param int $displayId
     */
    function deleteForm($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        if (!$this->getUser()->checkDeleteable($display))
            throw new AccessDeniedException();

        $this->getState()->template = 'display-form-delete';
        $this->getState()->setData([
            'display' => $display,
            'help' => $this->getHelp()->link('Display', 'Delete')
        ]);
    }

    /**
     * Display Edit
     * @param int $displayId
     *
     * @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="displayProfileId",
     *      in="formData",
     *      description="The Display Settings Profile ID",
     *      type="integer",
     *      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\Response(
     *      response=200,
     *      description="successful operation",
     *      @SWG\Schema(ref="#/definitions/Display")
     *  )
     * )
     */
    function edit($displayId)
    {
        $display = $this->displayFactory->getById($displayId, true);

        if (!$this->getUser()->checkEditable($display))
            throw new AccessDeniedException();

        // Track the default layout
        $defaultLayoutId = $display->defaultLayoutId;

        // Update properties
        if ($this->getConfig()->GetSetting('DISPLAY_LOCK_NAME_TO_DEVICENAME') == 0)
            $display->display = $this->getSanitizer()->getString('display');

        $display->description = $this->getSanitizer()->getString('description');
        $display->auditingUntil = $this->getSanitizer()->getDate('auditingUntil');
        $display->defaultLayoutId = $this->getSanitizer()->getInt('defaultLayoutId');
        $display->licensed = $this->getSanitizer()->getInt('licensed');
        $display->license = $this->getSanitizer()->getString('license');
        $display->incSchedule = $this->getSanitizer()->getInt('incSchedule');
        $display->emailAlert = $this->getSanitizer()->getInt('emailAlert');
        $display->alertTimeout = $this->getSanitizer()->getCheckbox('alertTimeout');
        $display->wakeOnLanEnabled = $this->getSanitizer()->getCheckbox('wakeOnLanEnabled');
        $display->wakeOnLanTime = $this->getSanitizer()->getString('wakeOnLanTime');
        $display->broadCastAddress = $this->getSanitizer()->getString('broadCastAddress');
        $display->secureOn = $this->getSanitizer()->getString('secureOn');
        $display->cidr = $this->getSanitizer()->getString('cidr');
        $display->latitude = $this->getSanitizer()->getDouble('latitude');
        $display->longitude = $this->getSanitizer()->getDouble('longitude');
        $display->timeZone = $this->getSanitizer()->getString('timeZone');
        $display->displayProfileId = $this->getSanitizer()->getInt('displayProfileId');

        // Tags are stored on the displaygroup, we're just passing through here
        $display->tags = $this->tagFactory->tagsFromString($this->getSanitizer()->getString('tags'));

        if ($display->auditingUntil !== null)
            $display->auditingUntil = $display->auditingUntil->format('U');

        // Should we invalidate this display?
        if ($defaultLayoutId != $display->defaultLayoutId) {
            $display->notify();
        } else if ($this->getSanitizer()->getCheckbox('clearCachedData', 1) == 1) {
            // Remove the cache if the display licenced state has changed
            $this->pool->deleteItem($display->getCacheKey());
        }

        // Should we rekey?
        if ($this->getSanitizer()->getCheckbox('rekeyXmr', 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->setChildObjectDependencies($this->layoutFactory, $this->mediaFactory, $this->scheduleFactory);
        $display->save();

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Edited %s'), $display->display),
            'id' => $display->displayId,
            'data' => $display
        ]);
    }

    /**
     * Delete a display
     * @param int $displayId
     *
     * @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($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        if (!$this->getUser()->checkDeleteable($display))
            throw new AccessDeniedException();

        $display->setChildObjectDependencies($this->layoutFactory, $this->mediaFactory, $this->scheduleFactory);
        $display->delete();

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => sprintf(__('Deleted %s'), $display->display),
            'id' => $display->displayId,
            'data' => $display
        ]);
    }

    /**
     * Member of Display Groups Form
     * @param int $displayId
     */
    public function membershipForm($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        if (!$this->getUser()->checkEditable($display))
            throw new AccessDeniedException();

        // Groups we are assigned to
        $groupsAssigned = $this->displayGroupFactory->getByDisplayId($display->displayId);

        // All Groups
        $allGroups = $this->displayGroupFactory->getByIsDynamic(0);

        // The available users are all users except users already in assigned users
        $checkboxes = array();

        foreach ($allGroups as $group) {
            /* @var \Xibo\Entity\DisplayGroup $group */
            // Check to see if it exists in $usersAssigned
            $exists = false;
            foreach ($groupsAssigned as $groupAssigned) {
                /* @var \Xibo\Entity\DisplayGroup $groupAssigned */
                if ($groupAssigned->displayGroupId == $group->displayGroupId) {
                    $exists = true;
                    break;
                }
            }

            // Store this checkbox
            $checkbox = array(
                'id' => $group->displayGroupId,
                'name' => $group->displayGroup,
                'value_checked' => (($exists) ? 'checked' : '')
            );

            $checkboxes[] = $checkbox;
        }

        $this->getState()->template = 'display-form-membership';
        $this->getState()->setData([
            'display' => $display,
            'checkboxes' => $checkboxes,
            'help' =>  $this->getHelp()->link('Display', 'Members')
        ]);
    }

    /**
     * Assign Display to Display Groups
     * @param int $displayId
     * @throws \Xibo\Exception\NotFoundException
     */
    public function assignDisplayGroup($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        if (!$this->getUser()->checkEditable($display))
            throw new AccessDeniedException();

        // Go through each ID to assign
        foreach ($this->getSanitizer()->getIntArray('displayGroupId') as $displayGroupId) {
            $displayGroup = $this->displayGroupFactory->getById($displayGroupId);
            $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory);

            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 ($this->getSanitizer()->getIntArray('unassignDisplayGroupId') as $displayGroupId) {
            $displayGroup = $this->displayGroupFactory->getById($displayGroupId);
            $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory);

            if (!$this->getUser()->checkEditable($displayGroup))
                throw new AccessDeniedException(__('Access Denied to DisplayGroup'));

            $displayGroup->unassignDisplay($display);
            $displayGroup->save(['validate' => false]);
        }

        // Return
        $this->getState()->hydrate([
            'httpStatus' => 204,
            'message' => sprintf(__('%s assigned to Display Groups'), $display->display),
            'id' => $display->displayId
        ]);
    }

    /**
     * Output a screen shot
     * @param int $displayId
     */
    public function screenShot($displayId)
    {
        $this->setNoOutput(true);

        // Output an image if present, otherwise not found image.
        $file = 'screenshots/' . $displayId . '_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');
        }

        $size = filesize($fileName);

        $fi = new finfo(FILEINFO_MIME_TYPE);
        $mime = $fi->file($fileName);
        header("Content-Type: {$mime}");

        // Output a header
        header('Cache-Control: no-cache, must-revalidate');
        header('Content-Length: ' . $size);

        // Return the file with PHP
        // Disable any buffering to prevent OOM errors.
        @ob_end_clean();
        @ob_end_flush();
        readfile($fileName);
    }

    /**
     * Request ScreenShot form
     * @param int $displayId
     */
    public function requestScreenShotForm($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        if (!$this->getUser()->checkViewable($display))
            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('collectionInterval', 5);
            $nextCollect = $this->getDate()->parse($display->lastAccessed, 'U')->addMinutes($collectionInterval)->diffForHumans();
        }

        $this->getState()->template = 'display-form-request-screenshot';
        $this->getState()->setData([
            'display' => $display,
            'nextCollect' => $nextCollect,
            'help' =>  $this->getHelp()->link('Display', 'ScreenShot')
        ]);
    }

    /**
     * Request ScreenShot
     * @param int $displayId
     * @throws \InvalidArgumentException if XMR is not configured
     * @throws ConfigurationException if XMR cannot be contacted
     *
     * @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($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        if (!$this->getUser()->checkViewable($display))
            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
        ]);
    }

    /**
     * Form for wake on Lan
     * @param int $displayId
     */
    public function wakeOnLanForm($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        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.'));

        $this->getState()->template = 'display-form-wakeonlan';
        $this->getState()->setData([
            'display' => $display,
            'help' =>  $this->getHelp()->link('Display', 'WakeOnLan')
        ]);
    }

    /**
     * Wake this display using a WOL command
     * @param int $displayId
     *
     * @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($displayId)
    {
        $display = $this->displayFactory->getById($displayId);

        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 = time();
        $display->save(['validate' => false]);

        // Return
        $this->getState()->hydrate([
            'message' => sprintf(__('Wake on Lan sent for %s'), $display->display),
            'id' => $display->displayId
        ]);
    }

    /**
     * Validate the display list
     * @param array[Display] $displays
     * @return array[Display]
     */
    public function validateDisplays($displays)
    {
        $timedOutDisplays = [];

        // Get the global time out (overrides the alert time out on the display if 0)
        $globalTimeout = $this->getConfig()->GetSetting('MAINTENANCE_ALERT_TOUT') * 60;

        foreach ($displays as $display) {
            /* @var \Xibo\Entity\Display $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 time out to test against
            $timeOut = $display->lastAccessed + $timeoutToTestAgainst;

            // If the last time we accessed is less than now minus the time out
            if ($timeOut < time()) {
                $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));

                // If this is the first switch (i.e. the row was logged in before)
                if ($display->loggedIn == 1) {

                    // Update the display and set it as logged out
                    $display->loggedIn = 0;
                    $display->save(\Xibo\Entity\Display::$saveOptionsMinimum);

                    // We put it back again (in memory only)
                    // this is then used to indicate whether or not this is the first time this display has gone
                    // offline (for anything that uses the timedOutDisplays return
                    $display->loggedIn = 1;

                    // Log the down event
                    $event = $this->displayEventFactory->createEmpty();
                    $event->displayId = $display->displayId;
                    $event->start = $display->lastAccessed;
                    $event->save();
                }

                // Store this row
                $timedOutDisplays[] = $display;
            }
        }

        return $timedOutDisplays;
    }
}