3364 lines
134 KiB
PHP
3364 lines
134 KiB
PHP
<?php
|
|
/*
|
|
* Copyright (C) 2024 Xibo Signage Ltd
|
|
*
|
|
* Xibo - Digital Signage - https://xibosignage.com
|
|
*
|
|
* This file is part of Xibo.
|
|
*
|
|
* Xibo is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
*
|
|
* Xibo is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
namespace Xibo\Factory;
|
|
|
|
use Carbon\Carbon;
|
|
use Stash\Invalidation;
|
|
use Stash\Pool;
|
|
use Xibo\Entity\DataSet;
|
|
use Xibo\Entity\Folder;
|
|
use Xibo\Entity\Layout;
|
|
use Xibo\Entity\Module;
|
|
use Xibo\Entity\Playlist;
|
|
use Xibo\Entity\Region;
|
|
use Xibo\Entity\User;
|
|
use Xibo\Entity\Widget;
|
|
use Xibo\Helper\DateFormatHelper;
|
|
use Xibo\Helper\Environment;
|
|
use Xibo\Service\ConfigServiceInterface;
|
|
use Xibo\Service\MediaServiceInterface;
|
|
use Xibo\Support\Exception\DuplicateEntityException;
|
|
use Xibo\Support\Exception\GeneralException;
|
|
use Xibo\Support\Exception\InvalidArgumentException;
|
|
use Xibo\Support\Exception\NotFoundException;
|
|
use Xibo\Widget\SubPlaylistItem;
|
|
|
|
/**
|
|
* Class LayoutFactory
|
|
* @package Xibo\Factory
|
|
*/
|
|
class LayoutFactory extends BaseFactory
|
|
{
|
|
use TagTrait;
|
|
|
|
/**
|
|
* @var ConfigServiceInterface
|
|
*/
|
|
private $config;
|
|
|
|
/** @var \Stash\Interfaces\PoolInterface */
|
|
private $pool;
|
|
|
|
/**
|
|
* @var PermissionFactory
|
|
*/
|
|
private $permissionFactory;
|
|
|
|
/**
|
|
* @var RegionFactory
|
|
*/
|
|
private $regionFactory;
|
|
|
|
/**
|
|
* @var TagFactory
|
|
*/
|
|
private $tagFactory;
|
|
|
|
/**
|
|
* @var CampaignFactory
|
|
*/
|
|
private $campaignFactory;
|
|
|
|
/**
|
|
* @var MediaFactory
|
|
*/
|
|
private $mediaFactory;
|
|
|
|
/**
|
|
* @var ModuleFactory
|
|
*/
|
|
private $moduleFactory;
|
|
|
|
/**
|
|
* @var ModuleTemplateFactory
|
|
*/
|
|
private $moduleTemplateFactory;
|
|
|
|
/**
|
|
* @var ResolutionFactory
|
|
*/
|
|
private $resolutionFactory;
|
|
|
|
/**
|
|
* @var WidgetFactory
|
|
*/
|
|
private $widgetFactory;
|
|
|
|
/**
|
|
* @var WidgetOptionFactory
|
|
*/
|
|
private $widgetOptionFactory;
|
|
|
|
/** @var WidgetAudioFactory */
|
|
private $widgetAudioFactory;
|
|
|
|
/** @var PlaylistFactory */
|
|
private $playlistFactory;
|
|
|
|
/** @var ActionFactory */
|
|
private $actionFactory;
|
|
|
|
/** @var FolderFactory */
|
|
private $folderFactory;
|
|
/**
|
|
* @var FontFactory
|
|
*/
|
|
private $fontFactory;
|
|
|
|
/**
|
|
* Construct a factory
|
|
* @param User $user
|
|
* @param UserFactory $userFactory
|
|
* @param ConfigServiceInterface $config
|
|
* @param PermissionFactory $permissionFactory
|
|
* @param RegionFactory $regionFactory
|
|
* @param TagFactory $tagFactory
|
|
* @param CampaignFactory $campaignFactory
|
|
* @param MediaFactory $mediaFactory
|
|
* @param ModuleFactory $moduleFactory
|
|
* @param ModuleTemplateFactory $moduleTemplateFactory
|
|
* @param ResolutionFactory $resolutionFactory
|
|
* @param WidgetFactory $widgetFactory
|
|
* @param WidgetOptionFactory $widgetOptionFactory
|
|
* @param PlaylistFactory $playlistFactory
|
|
* @param WidgetAudioFactory $widgetAudioFactory
|
|
* @param ActionFactory $actionFactory
|
|
* @param FolderFactory $folderFactory
|
|
* @param FontFactory $fontFactory
|
|
*/
|
|
public function __construct(
|
|
$user,
|
|
$userFactory,
|
|
$config,
|
|
$permissionFactory,
|
|
$regionFactory,
|
|
$tagFactory,
|
|
$campaignFactory,
|
|
$mediaFactory,
|
|
$moduleFactory,
|
|
$moduleTemplateFactory,
|
|
$resolutionFactory,
|
|
$widgetFactory,
|
|
$widgetOptionFactory,
|
|
$playlistFactory,
|
|
$widgetAudioFactory,
|
|
$actionFactory,
|
|
$folderFactory,
|
|
FontFactory $fontFactory,
|
|
private readonly WidgetDataFactory $widgetDataFactory
|
|
) {
|
|
$this->setAclDependencies($user, $userFactory);
|
|
$this->config = $config;
|
|
$this->permissionFactory = $permissionFactory;
|
|
$this->regionFactory = $regionFactory;
|
|
$this->tagFactory = $tagFactory;
|
|
$this->campaignFactory = $campaignFactory;
|
|
$this->mediaFactory = $mediaFactory;
|
|
$this->moduleFactory = $moduleFactory;
|
|
$this->moduleTemplateFactory = $moduleTemplateFactory;
|
|
$this->resolutionFactory = $resolutionFactory;
|
|
$this->widgetFactory = $widgetFactory;
|
|
$this->widgetOptionFactory = $widgetOptionFactory;
|
|
$this->playlistFactory = $playlistFactory;
|
|
$this->widgetAudioFactory = $widgetAudioFactory;
|
|
$this->actionFactory = $actionFactory;
|
|
$this->folderFactory = $folderFactory;
|
|
$this->fontFactory = $fontFactory;
|
|
}
|
|
|
|
/**
|
|
* Create an empty layout
|
|
* @return Layout
|
|
*/
|
|
public function createEmpty()
|
|
{
|
|
return new Layout(
|
|
$this->getStore(),
|
|
$this->getLog(),
|
|
$this->getDispatcher(),
|
|
$this->config,
|
|
$this->permissionFactory,
|
|
$this->regionFactory,
|
|
$this->tagFactory,
|
|
$this->campaignFactory,
|
|
$this,
|
|
$this->mediaFactory,
|
|
$this->moduleFactory,
|
|
$this->moduleTemplateFactory,
|
|
$this->playlistFactory,
|
|
$this->actionFactory,
|
|
$this->folderFactory,
|
|
$this->fontFactory
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Create Layout from Resolution
|
|
* @param int $resolutionId
|
|
* @param int $ownerId
|
|
* @param string $name
|
|
* @param string $description
|
|
* @param string|array $tags
|
|
* @param string $code
|
|
* @param bool $addRegion
|
|
* @return Layout
|
|
*
|
|
* @throws GeneralException
|
|
* @throws InvalidArgumentException
|
|
* @throws NotFoundException
|
|
*/
|
|
public function createFromResolution($resolutionId, $ownerId, $name, $description, $tags, $code, $addRegion = true)
|
|
{
|
|
$resolution = $this->resolutionFactory->getById($resolutionId);
|
|
|
|
// Create a new Layout
|
|
$layout = $this->createEmpty();
|
|
$layout->width = $resolution->width;
|
|
$layout->height = $resolution->height;
|
|
$layout->orientation = ($layout->width >= $layout->height) ? 'landscape' : 'portrait';
|
|
|
|
// Set the properties
|
|
$layout->layout = $name;
|
|
$layout->description = $description;
|
|
$layout->backgroundzIndex = 0;
|
|
$layout->backgroundColor = '#000000';
|
|
$layout->code = $code;
|
|
|
|
// Set the owner
|
|
$layout->setOwner($ownerId);
|
|
|
|
// Create some tags
|
|
if (is_array($tags)) {
|
|
$layout->updateTagLinks($tags);
|
|
} else {
|
|
$layout->updateTagLinks($this->tagFactory->tagsFromString($tags));
|
|
}
|
|
|
|
// Add a blank, full screen region
|
|
if ($addRegion) {
|
|
$layout->regions[] = $this->regionFactory->create(
|
|
'zone',
|
|
$ownerId,
|
|
$name . '-1',
|
|
$layout->width,
|
|
$layout->height,
|
|
0,
|
|
0
|
|
);
|
|
}
|
|
|
|
return $layout;
|
|
}
|
|
|
|
/**
|
|
* @param \Xibo\Entity\Layout $layout
|
|
* @param string $type
|
|
* @param int $width
|
|
* @param int $height
|
|
* @param int $top
|
|
* @param int $left
|
|
* @return \Xibo\Entity\Layout
|
|
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
|
*/
|
|
public function addRegion(Layout $layout, string $type, int $width, int $height, int $top, int $left): Layout
|
|
{
|
|
$layout->regions[] = $this->regionFactory->create(
|
|
$type,
|
|
$layout->ownerId,
|
|
$layout->layout . '-' . count($layout->regions),
|
|
$width,
|
|
$height,
|
|
$top,
|
|
$left
|
|
);
|
|
|
|
return $layout;
|
|
}
|
|
|
|
/**
|
|
* Load a layout by its ID
|
|
* @param int $layoutId
|
|
* @return Layout The Layout
|
|
* @throws NotFoundException
|
|
*/
|
|
public function loadById($layoutId)
|
|
{
|
|
// Get the layout
|
|
$layout = $this->getById($layoutId);
|
|
// Load the layout
|
|
$layout->load();
|
|
|
|
return $layout;
|
|
}
|
|
|
|
/**
|
|
* Loads only the layout information
|
|
* @param int $layoutId
|
|
* @return Layout
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getById($layoutId)
|
|
{
|
|
if (empty($layoutId)) {
|
|
throw new NotFoundException(__('LayoutId is 0'));
|
|
}
|
|
|
|
$layouts = $this->query(null, array('disableUserCheck' => 1, 'layoutId' => $layoutId, 'excludeTemplates' => -1, 'retired' => -1));
|
|
|
|
if (count($layouts) <= 0) {
|
|
throw new NotFoundException(__('Layout not found'));
|
|
}
|
|
|
|
// Set our layout
|
|
return $layouts[0];
|
|
}
|
|
|
|
/**
|
|
* Get CampaignId from layout history
|
|
* @param int $layoutId
|
|
* @return int campaignId
|
|
* @throws InvalidArgumentException
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getCampaignIdFromLayoutHistory($layoutId)
|
|
{
|
|
if ($layoutId == null) {
|
|
throw new InvalidArgumentException(__('Invalid Input'), 'layoutId');
|
|
}
|
|
|
|
$row = $this->getStore()->select('SELECT campaignId FROM `layouthistory` WHERE layoutId = :layoutId LIMIT 1', ['layoutId' => $layoutId]);
|
|
|
|
if (count($row) <= 0) {
|
|
throw new NotFoundException(__('Layout does not exist'));
|
|
}
|
|
|
|
return intval($row[0]['campaignId']);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get layout by layout history
|
|
* @param int $layoutId
|
|
* @return Layout
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByLayoutHistory($layoutId)
|
|
{
|
|
// Get a Layout by its Layout HistoryId
|
|
$layouts = $this->query(null, array('disableUserCheck' => 1, 'layoutHistoryId' => $layoutId, 'excludeTemplates' => -1, 'retired' => -1));
|
|
|
|
if (count($layouts) <= 0) {
|
|
throw new NotFoundException(__('Layout not found'));
|
|
}
|
|
|
|
// Set our layout
|
|
return $layouts[0];
|
|
}
|
|
|
|
/**
|
|
* Get latest layoutId by CampaignId from layout history
|
|
* @param int campaignId
|
|
* @return int layoutId
|
|
* @throws InvalidArgumentException
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getLatestLayoutIdFromLayoutHistory($campaignId)
|
|
{
|
|
if ($campaignId == null) {
|
|
throw new InvalidArgumentException(__('Invalid Input'), 'campaignId');
|
|
}
|
|
|
|
$row = $this->getStore()->select('SELECT MAX(layoutId) AS layoutId FROM `layouthistory` WHERE campaignId = :campaignId ', ['campaignId' => $campaignId]);
|
|
|
|
if (count($row) <= 0) {
|
|
throw new NotFoundException(__('Layout does not exist'));
|
|
}
|
|
|
|
// Set our Layout ID
|
|
return intval($row[0]['layoutId']);
|
|
}
|
|
|
|
/**
|
|
* Loads only the layout information
|
|
* @param int $layoutId
|
|
* @return Layout
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByParentId($layoutId)
|
|
{
|
|
if (empty($layoutId)) {
|
|
throw new NotFoundException();
|
|
}
|
|
|
|
$layouts = $this->query(null, array('disableUserCheck' => 1, 'parentId' => $layoutId, 'excludeTemplates' => -1, 'retired' => -1));
|
|
|
|
if (count($layouts) <= 0) {
|
|
throw new NotFoundException(__('Layout not found'));
|
|
}
|
|
|
|
// Set our layout
|
|
return $layouts[0];
|
|
}
|
|
|
|
/**
|
|
* Get a Layout by its Layout Specific Campaign OwnerId
|
|
* @param int $campaignId
|
|
* @return Layout
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByParentCampaignId($campaignId)
|
|
{
|
|
if ($campaignId == 0)
|
|
throw new NotFoundException();
|
|
|
|
$layouts = $this->query(null, array('disableUserCheck' => 1, 'ownerCampaignId' => $campaignId, 'excludeTemplates' => -1, 'retired' => -1));
|
|
|
|
if (count($layouts) <= 0) {
|
|
throw new NotFoundException(__('Layout not found'));
|
|
}
|
|
|
|
// Set our layout
|
|
return $layouts[0];
|
|
}
|
|
|
|
/**
|
|
* Get by OwnerId
|
|
* @param int $ownerId
|
|
* @return Layout[]
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByOwnerId($ownerId)
|
|
{
|
|
return $this->query(null, array('userId' => $ownerId, 'excludeTemplates' => -1, 'retired' => -1, 'showDrafts' => 1));
|
|
}
|
|
|
|
/**
|
|
* Get by CampaignId
|
|
* @param int $campaignId
|
|
* @param bool $permissionsCheck Should we check permissions?
|
|
* @param bool $includeDrafts Should we include draft Layouts in the results?
|
|
* @return Layout[]
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByCampaignId($campaignId, $permissionsCheck = true, $includeDrafts = false)
|
|
{
|
|
return $this->query(['displayOrder'], [
|
|
'campaignId' => $campaignId,
|
|
'excludeTemplates' => -1,
|
|
'retired' => -1,
|
|
'disableUserCheck' => $permissionsCheck ? 0 : 1,
|
|
'showDrafts' => $includeDrafts ? 1 : 0
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get by RegionId
|
|
* @param int $regionId
|
|
* @param bool $permissionsCheck Should we check permissions?
|
|
* @return Layout
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByRegionId($regionId, $permissionsCheck = true)
|
|
{
|
|
$layouts = $this->query(['displayOrder'], [
|
|
'regionId' => $regionId,
|
|
'excludeTemplates' => -1,
|
|
'retired' => -1,
|
|
'disableUserCheck' => $permissionsCheck ? 0 : 1,
|
|
'showDrafts' => 1
|
|
]);
|
|
|
|
if (count($layouts) <= 0) {
|
|
throw new NotFoundException(__('Layout not found'));
|
|
}
|
|
|
|
// Set our layout
|
|
return $layouts[0];
|
|
}
|
|
|
|
/**
|
|
* Get by Display Group Id
|
|
* @param int $displayGroupId
|
|
* @return Layout[]
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByDisplayGroupId($displayGroupId)
|
|
{
|
|
if ($displayGroupId == null) {
|
|
return [];
|
|
}
|
|
|
|
return $this->query(null, ['disableUserCheck' => 1, 'displayGroupId' => $displayGroupId]);
|
|
}
|
|
|
|
/**
|
|
* Get by Background Image Id
|
|
* @param int $backgroundImageId
|
|
* @return Layout[]
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByBackgroundImageId($backgroundImageId)
|
|
{
|
|
return $this->query(null, ['disableUserCheck' => 1, 'backgroundImageId' => $backgroundImageId, 'showDrafts' => 1]);
|
|
}
|
|
|
|
/**
|
|
* @param string $tag
|
|
* @return Layout[]
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByTag($tag)
|
|
{
|
|
return $this->query(null, ['disableUserCheck' => 1, 'tags' => $tag, 'exactTags' => 1]);
|
|
}
|
|
|
|
/**
|
|
* Get by Code identifier
|
|
* @param string $code
|
|
* @return Layout
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getByCode($code)
|
|
{
|
|
$layouts = $this->query(null, ['disableUserCheck' => 1, 'code' => $code, 'excludeTemplates' => -1, 'retired' => -1]);
|
|
|
|
if (count($layouts) <= 0) {
|
|
throw new NotFoundException(__('Layout not found'));
|
|
}
|
|
|
|
// Set our layout
|
|
return $layouts[0];
|
|
}
|
|
|
|
/**
|
|
* @param string $type
|
|
* @param int $id
|
|
* @param array $properties
|
|
* @return Layout|null
|
|
* @throws NotFoundException
|
|
*/
|
|
public function getLinkedFullScreenLayout(string $type, int $id, array $properties = []): ?Layout
|
|
{
|
|
$params = [
|
|
'campaignType' => $type
|
|
];
|
|
|
|
if ($type === 'media') {
|
|
$params['mediaId'] = $id;
|
|
} else if ($type === 'playlist') {
|
|
$params['playlistId'] = $id;
|
|
}
|
|
|
|
if (!empty($properties)) {
|
|
$params = array_merge($params, $properties);
|
|
}
|
|
|
|
$layouts = $this->query(null, $params);
|
|
|
|
if (count($layouts) <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return $layouts[0];
|
|
}
|
|
|
|
/**
|
|
* @param int $campaignId
|
|
* @return int|null
|
|
*/
|
|
public function getLinkedFullScreenMediaId(int $campaignId): ?int
|
|
{
|
|
$mediaId = $this->getStore()->select('SELECT `lkwidgetmedia`.mediaId
|
|
FROM region
|
|
INNER JOIN playlist
|
|
ON playlist.regionId = region.regionId
|
|
INNER JOIN lkplaylistplaylist
|
|
ON lkplaylistplaylist.parentId = playlist.playlistId
|
|
INNER JOIN widget
|
|
ON widget.playlistId = lkplaylistplaylist.childId
|
|
INNER JOIN lkwidgetmedia
|
|
ON widget.widgetId = lkwidgetmedia.widgetId
|
|
INNER JOIN `lkcampaignlayout` lkcl
|
|
ON lkcl.layoutid = region.layoutid AND lkcl.CampaignID = :campaignId',
|
|
['campaignId' => $campaignId]
|
|
);
|
|
|
|
if (count($mediaId) <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return $mediaId[0]['mediaId'];
|
|
}
|
|
|
|
/**
|
|
* @param int $campaignId
|
|
* @return int|null
|
|
*/
|
|
public function getLinkedFullScreenPlaylistId(int $campaignId): ?int
|
|
{
|
|
$playlistId = $this->getStore()->select('SELECT `lkplaylistplaylist`.childId AS playlistId
|
|
FROM region
|
|
INNER JOIN playlist
|
|
ON `playlist`.regionId = `region`.regionId
|
|
INNER JOIN lkplaylistplaylist
|
|
ON `lkplaylistplaylist`.parentId = `playlist`.playlistId
|
|
INNER JOIN `lkcampaignlayout` lkcl
|
|
ON lkcl.layoutid = region.layoutid
|
|
AND lkcl.CampaignID = :campaignId',
|
|
['campaignId' => $campaignId]
|
|
);
|
|
|
|
if (count($playlistId) <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return $playlistId[0]['playlistId'];
|
|
}
|
|
|
|
/**
|
|
* Load a layout by its XLF
|
|
* @param string $layoutXlf
|
|
* @param null $layout
|
|
* @return \Xibo\Entity\Layout
|
|
* @throws \Xibo\Support\Exception\GeneralException
|
|
*/
|
|
public function loadByXlf($layoutXlf, $layout = null)
|
|
{
|
|
$this->getLog()->debug('Loading Layout by XLF');
|
|
|
|
// New Layout
|
|
if ($layout == null) {
|
|
$layout = $this->createEmpty();
|
|
}
|
|
|
|
// Parse the XML and fill in the details for this layout
|
|
$document = new \DOMDocument();
|
|
if ($document->loadXML($layoutXlf) === false) {
|
|
throw new InvalidArgumentException(__('Layout import failed, invalid xlf supplied'));
|
|
}
|
|
|
|
$layout->schemaVersion = (int)$document->documentElement->getAttribute('schemaVersion');
|
|
$layout->width = $document->documentElement->getAttribute('width');
|
|
$layout->height = $document->documentElement->getAttribute('height');
|
|
$layout->backgroundColor = $document->documentElement->getAttribute('bgcolor');
|
|
$layout->backgroundzIndex = (int)$document->documentElement->getAttribute('zindex');
|
|
|
|
// Xpath to use when getting media
|
|
$xpath = new \DOMXPath($document);
|
|
|
|
// Populate Region Nodes
|
|
foreach ($document->getElementsByTagName('region') as $regionNode) {
|
|
/* @var \DOMElement $regionNode */
|
|
$this->getLog()->debug('Found Region');
|
|
|
|
// Get the ownerId
|
|
$regionOwnerId = $regionNode->getAttribute('userId');
|
|
if ($regionOwnerId == null) {
|
|
$regionOwnerId = $layout->ownerId;
|
|
}
|
|
|
|
// Create the region
|
|
// we only import from XLF for older layouts which only had playlist type regions.
|
|
// we start assuming this will be a playlist and update it later if necessary
|
|
$region = $this->regionFactory->create(
|
|
'playlist',
|
|
$regionOwnerId,
|
|
$regionNode->getAttribute('name'),
|
|
(double)$regionNode->getAttribute('width'),
|
|
(double)$regionNode->getAttribute('height'),
|
|
(double)$regionNode->getAttribute('top'),
|
|
(double)$regionNode->getAttribute('left'),
|
|
(int)$regionNode->getAttribute('zindex')
|
|
);
|
|
|
|
// Use the regionId locally to parse the rest of the XLF
|
|
$region->tempId = $regionNode->getAttribute('id');
|
|
|
|
// Set the region name if empty
|
|
if ($region->name == '') {
|
|
$region->name = count($layout->regions) + 1;
|
|
// make sure we have a string as the region name, otherwise sanitizer will get confused.
|
|
$region->name = (string)$region->name;
|
|
}
|
|
// Populate Playlists (XLF doesn't contain any playlists)
|
|
$playlist = $this->playlistFactory->create($region->name, $regionOwnerId);
|
|
|
|
// Populate region options.
|
|
foreach ($xpath->query('//region[@id="' . $region->tempId . '"]/options') as $regionOptionsNode) {
|
|
/* @var \DOMElement $regionOptionsNode */
|
|
foreach ($regionOptionsNode->childNodes as $regionOption) {
|
|
/* @var \DOMElement $regionOption */
|
|
$region->setOptionValue($regionOption->nodeName, $regionOption->textContent);
|
|
}
|
|
}
|
|
|
|
// Get all widgets
|
|
foreach ($xpath->query('//region[@id="' . $region->tempId . '"]/media') as $mediaNode) {
|
|
/* @var \DOMElement $mediaNode */
|
|
|
|
$mediaOwnerId = $mediaNode->getAttribute('userId');
|
|
if ($mediaOwnerId == null) {
|
|
$mediaOwnerId = $regionOwnerId;
|
|
}
|
|
$widget = $this->widgetFactory->createEmpty();
|
|
$widget->type = $mediaNode->getAttribute('type');
|
|
$widget->ownerId = $mediaOwnerId;
|
|
$widget->duration = $mediaNode->getAttribute('duration');
|
|
$widget->useDuration = $mediaNode->getAttribute('useDuration');
|
|
// Additional check for importing layouts from 1.7 series, where the useDuration did not exist
|
|
$widget->useDuration = ($widget->useDuration === '') ? 1 : $widget->useDuration;
|
|
$widget->tempId = $mediaNode->getAttribute('fileId');
|
|
$widget->schemaVersion = (int)$mediaNode->getAttribute('schemaVersion');
|
|
$widgetId = $mediaNode->getAttribute('id');
|
|
|
|
// Widget from/to dates.
|
|
$widget->fromDt = ($mediaNode->getAttribute('fromDt') === '')
|
|
? Widget::$DATE_MIN
|
|
: $mediaNode->getAttribute('fromDt');
|
|
$widget->toDt = ($mediaNode->getAttribute('toDt') === '')
|
|
? Widget::$DATE_MAX
|
|
: $mediaNode->getAttribute('toDt');
|
|
|
|
$this->setWidgetExpiryDatesOrDefault($widget);
|
|
|
|
//
|
|
// Get all widget options
|
|
//
|
|
$xpathQuery = '//region[@id="' . $region->tempId . '"]/media[@id="' . $widgetId . '"]/options';
|
|
foreach ($xpath->query($xpathQuery) as $optionsNode) {
|
|
/* @var \DOMElement $optionsNode */
|
|
foreach ($optionsNode->childNodes as $mediaOption) {
|
|
/* @var \DOMElement $mediaOption */
|
|
$widgetOption = $this->widgetOptionFactory->createEmpty();
|
|
$widgetOption->type = 'attrib';
|
|
$widgetOption->option = $mediaOption->nodeName;
|
|
$widgetOption->value = $mediaOption->textContent;
|
|
|
|
$widget->widgetOptions[] = $widgetOption;
|
|
|
|
// Convert the module type of known legacy widgets
|
|
if ($widget->type == 'ticker'
|
|
&& $widgetOption->option == 'sourceId'
|
|
&& $widgetOption->value == '2'
|
|
) {
|
|
$widget->type = 'datasetticker';
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->getLog()->debug(sprintf(
|
|
'Added %d options with xPath query: %s',
|
|
count($widget->widgetOptions),
|
|
$xpathQuery
|
|
));
|
|
|
|
// Check legacy types from conditions, set widget type and upgrade
|
|
try {
|
|
$module = $this->prepareWidgetAndGetModule($widget);
|
|
} catch (NotFoundException) {
|
|
// Skip this widget
|
|
$this->getLog()->info('loadByJson: ' . $widget->type . ' could not be found or resolved');
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// Get the MediaId associated with this widget (using the URI)
|
|
//
|
|
if ($module->regionSpecific == 0) {
|
|
$this->getLog()->debug('Library Widget, getting mediaId');
|
|
|
|
if (empty($widget->tempId)) {
|
|
$this->getLog()->debug(sprintf(
|
|
'FileId node is empty, setting tempId from uri option. Options: %s',
|
|
json_encode($widget->widgetOptions)
|
|
));
|
|
$mediaId = explode('.', $widget->getOptionValue('uri', '0.*'));
|
|
$widget->tempId = $mediaId[0];
|
|
}
|
|
|
|
$this->getLog()->debug('Assigning mediaId %d', $widget->tempId);
|
|
$widget->assignMedia($widget->tempId);
|
|
}
|
|
|
|
//
|
|
// Get all widget raw content
|
|
//
|
|
$rawNodes = $xpath->query('//region[@id="' . $region->tempId . '"]/media[@id="' . $widgetId . '"]/raw');
|
|
foreach ($rawNodes as $rawNode) {
|
|
/* @var \DOMElement $rawNode */
|
|
// Get children
|
|
foreach ($rawNode->childNodes as $mediaOption) {
|
|
/* @var \DOMElement $mediaOption */
|
|
if ($mediaOption->textContent == null) {
|
|
continue;
|
|
}
|
|
$widgetOption = $this->widgetOptionFactory->createEmpty();
|
|
$widgetOption->type = 'cdata';
|
|
$widgetOption->option = $mediaOption->nodeName;
|
|
$widgetOption->value = $mediaOption->textContent;
|
|
|
|
$widget->widgetOptions[] = $widgetOption;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Audio
|
|
//
|
|
$rawNodes = $xpath
|
|
->query('//region[@id="' . $region->tempId . '"]/media[@id="' . $widgetId . '"]/audio');
|
|
foreach ($rawNodes as $rawNode) {
|
|
/* @var \DOMElement $rawNode */
|
|
// Get children
|
|
foreach ($rawNode->childNodes as $audioNode) {
|
|
/* @var \DOMElement $audioNode */
|
|
if ($audioNode->textContent == null) {
|
|
continue;
|
|
}
|
|
$audioMediaId = $audioNode->getAttribute('mediaId');
|
|
|
|
if (empty($audioMediaId)) {
|
|
// Try to parse it from the text content
|
|
$audioMediaId = explode('.', $audioNode->textContent)[0];
|
|
}
|
|
|
|
$widgetAudio = $this->widgetAudioFactory->createEmpty();
|
|
$widgetAudio->mediaId = $audioMediaId;
|
|
$widgetAudio->volume = $audioNode->getAttribute('volume');
|
|
$widgetAudio->loop = $audioNode->getAttribute('loop');
|
|
|
|
$widget->assignAudio($widgetAudio);
|
|
}
|
|
}
|
|
|
|
// Add the widget to the playlist
|
|
$playlist->assignWidget($widget);
|
|
}
|
|
|
|
// See if this region can be converted to a frame or zone (it is already a playlist)
|
|
if (count($playlist->widgets) === 1) {
|
|
$region->type = 'frame';
|
|
} else if (count($playlist->widgets) === 0) {
|
|
$region->type = 'zone';
|
|
}
|
|
|
|
// Assign Playlist to the Region
|
|
$region->regionPlaylist = $playlist;
|
|
|
|
// Assign the region to the Layout
|
|
$layout->regions[] = $region;
|
|
}
|
|
|
|
$this->getLog()->debug(sprintf('Finished loading layout - there are %d regions.', count($layout->regions)));
|
|
|
|
// Load any existing tags
|
|
if (!is_array($layout->tags)) {
|
|
$layout->tags = $this->tagFactory->tagsFromString($layout->tags);
|
|
}
|
|
|
|
foreach ($xpath->query('//tags/tag') as $tagNode) {
|
|
/* @var \DOMElement $tagNode */
|
|
if (trim($tagNode->textContent) == '') {
|
|
continue;
|
|
}
|
|
$layout->tags[] = $this->tagFactory->tagFromString($tagNode->textContent);
|
|
}
|
|
|
|
// The parsed, finished layout
|
|
return $layout;
|
|
}
|
|
|
|
/**
|
|
* @param $layoutJson
|
|
* @param $playlistJson
|
|
* @param $nestedPlaylistJson
|
|
* @param Folder $folder
|
|
* @param null $layout
|
|
* @param bool $importTags
|
|
* @return array
|
|
* @throws DuplicateEntityException
|
|
* @throws GeneralException
|
|
* @throws InvalidArgumentException
|
|
* @throws NotFoundException
|
|
*/
|
|
public function loadByJson($layoutJson, $playlistJson, $nestedPlaylistJson, Folder $folder, $layout = null, $importTags = false): array
|
|
{
|
|
$this->getLog()->debug('Loading Layout by JSON');
|
|
|
|
// New Layout
|
|
if ($layout == null) {
|
|
$layout = $this->createEmpty();
|
|
}
|
|
|
|
$playlists = [];
|
|
$oldIds = [];
|
|
$newIds = [];
|
|
$widgets = [];
|
|
|
|
$layout->schemaVersion = (int)$layoutJson['layoutDefinitions']['schemaVersion'];
|
|
$layout->width = $layoutJson['layoutDefinitions']['width'];
|
|
$layout->height = $layoutJson['layoutDefinitions']['height'];
|
|
$layout->backgroundColor = $layoutJson['layoutDefinitions']['backgroundColor'];
|
|
$layout->backgroundzIndex = (int)$layoutJson['layoutDefinitions']['backgroundzIndex'];
|
|
$layout->actions = [];
|
|
$layout->autoApplyTransitions = $layoutJson['layoutDefinitions']['autoApplyTransitions'] ?? 0;
|
|
$actions = $layoutJson['layoutDefinitions']['actions'] ?? [];
|
|
|
|
foreach ($actions as $action) {
|
|
$newAction = $this->actionFactory->create(
|
|
$action['triggerType'],
|
|
$action['triggerCode'],
|
|
$action['actionType'],
|
|
'importLayout',
|
|
$action['sourceId'],
|
|
$action['target'],
|
|
$action['targetId'],
|
|
$action['widgetId'],
|
|
$action['layoutCode'],
|
|
$action['layoutId'] ?? null
|
|
);
|
|
$newAction->save(['validate' => false]);
|
|
}
|
|
|
|
|
|
// Nested Playlists are Playlists which exist below the first level of Playlists in Sub-Playlist Widgets
|
|
// we need to import and save them first.
|
|
if ($nestedPlaylistJson != null) {
|
|
$this->getLog()->debug('Layout import, creating nested Playlists from JSON, there are ' . count($nestedPlaylistJson) . ' Playlists to create');
|
|
|
|
// create all nested Playlists, save their widgets to key=>value array
|
|
foreach ($nestedPlaylistJson as $nestedPlaylist) {
|
|
$newPlaylist = $this->playlistFactory->createEmpty()->hydrate($nestedPlaylist);
|
|
$newPlaylist->tags = [];
|
|
|
|
// Populate tags
|
|
if ($nestedPlaylist['tags'] !== null && count($nestedPlaylist['tags']) > 0 && $importTags) {
|
|
foreach ($nestedPlaylist['tags'] as $tag) {
|
|
$newPlaylist->tags[] = $this->tagFactory->tagFromString(
|
|
$tag['tag'] . (!empty($tag['value']) ? '|' . $tag['value'] : '')
|
|
);
|
|
}
|
|
}
|
|
|
|
$oldIds[] = $newPlaylist->playlistId;
|
|
$widgets[$newPlaylist->playlistId] = $newPlaylist->widgets;
|
|
|
|
$this->setOwnerAndSavePlaylist($newPlaylist, $folder);
|
|
|
|
$newIds[] = $newPlaylist->playlistId;
|
|
}
|
|
|
|
$combined = array_combine($oldIds, $newIds);
|
|
|
|
// this function will go through all widgets assigned to the nested Playlists, create the widgets, adjust the Ids and return an array of Playlists
|
|
// then the Playlists array is used later on to adjust mediaIds if needed
|
|
$playlists = $this->createNestedPlaylistWidgets($widgets, $combined, $playlists);
|
|
|
|
$this->getLog()->debug('Finished creating nested playlists there are ' . count($playlists) . ' Playlists created');
|
|
}
|
|
|
|
$drawers = (array_key_exists('drawers', $layoutJson['layoutDefinitions'])) ? $layoutJson['layoutDefinitions']['drawers'] : [];
|
|
|
|
// merge Layout Regions and Drawers into one array.
|
|
$allRegions = array_merge($layoutJson['layoutDefinitions']['regions'], $drawers);
|
|
|
|
// Populate Region Nodes
|
|
foreach ($allRegions as $regionJson) {
|
|
$this->getLog()->debug('Found Region');
|
|
|
|
// Get the ownerId
|
|
$regionOwnerId = $regionJson['ownerId'];
|
|
if ($regionOwnerId == null) {
|
|
$regionOwnerId = $layout->ownerId;
|
|
}
|
|
|
|
$regionIsDrawer = isset($regionJson['isDrawer']) ? (int)$regionJson['isDrawer'] : 0;
|
|
$regionWidgets = $regionJson['regionPlaylist']['widgets'] ?? [];
|
|
|
|
// Do we have a region type specified (i.e. is the export from v4)
|
|
// Or determine the region type based on how many widgets we have and whether we're the drawer
|
|
if (!empty($regionJson['type'] ?? null)) {
|
|
$regionType = $regionJson['type'];
|
|
} else if ($regionIsDrawer === 1) {
|
|
$regionType = 'drawer';
|
|
} else if (count($regionWidgets) === 1 && !$this->hasSubPlaylist($regionWidgets)) {
|
|
$regionType = 'frame';
|
|
} else if (count($regionWidgets) === 0) {
|
|
$regionType = 'zone';
|
|
} else {
|
|
$regionType = 'playlist';
|
|
}
|
|
|
|
// Create the region
|
|
$region = $this->regionFactory->create(
|
|
$regionType,
|
|
$regionOwnerId,
|
|
$regionJson['name'],
|
|
(double)$regionJson['width'],
|
|
(double)$regionJson['height'],
|
|
(double)$regionJson['top'],
|
|
(double)$regionJson['left'],
|
|
(int)$regionJson['zIndex'],
|
|
$regionIsDrawer
|
|
);
|
|
|
|
// Use the regionId locally to parse the rest of the JSON
|
|
$region->tempId = $regionJson['tempId'] ?? $regionJson['regionId'];
|
|
|
|
// Set the region name if empty
|
|
if ($region->name == '') {
|
|
$region->name = count($layout->regions) + 1;
|
|
// make sure we have a string as the region name, otherwise sanitizer will get confused.
|
|
$region->name = (string)$region->name;
|
|
}
|
|
|
|
// Populate Playlists
|
|
$playlist = $this->playlistFactory->create($region->name, $regionOwnerId);
|
|
|
|
// interactive Actions
|
|
$actions = $regionJson['actions'] ?? [];
|
|
foreach ($actions as $action) {
|
|
$newAction = $this->actionFactory->create(
|
|
$action['triggerType'],
|
|
$action['triggerCode'],
|
|
$action['actionType'],
|
|
'importRegion',
|
|
$action['sourceId'],
|
|
$action['target'],
|
|
$action['targetId'],
|
|
$action['widgetId'],
|
|
$action['layoutCode'],
|
|
$action['layoutId'] ?? null
|
|
);
|
|
$newAction->save(['validate' => false]);
|
|
}
|
|
|
|
foreach ($regionJson['regionOptions'] as $regionOption) {
|
|
$region->setOptionValue($regionOption['option'], $regionOption['value']);
|
|
}
|
|
|
|
// Get all widgets
|
|
foreach ($regionWidgets as $mediaNode) {
|
|
$mediaOwnerId = $mediaNode['ownerId'];
|
|
if ($mediaOwnerId == null) {
|
|
$mediaOwnerId = $regionOwnerId;
|
|
}
|
|
|
|
$widget = $this->widgetFactory->createEmpty();
|
|
$widget->type = $mediaNode['type'];
|
|
$widget->ownerId = $mediaOwnerId;
|
|
$widget->duration = $mediaNode['duration'];
|
|
$widget->useDuration = $mediaNode['useDuration'];
|
|
$widget->tempId = (int)implode(',', $mediaNode['mediaIds']);
|
|
$widget->tempWidgetId = $mediaNode['widgetId'];
|
|
$widget->schemaVersion = isset($mediaNode['schemaVersion']) ? (int)$mediaNode['schemaVersion'] : 1;
|
|
|
|
// Widget from/to dates.
|
|
$widget->fromDt = ($mediaNode['fromDt'] === '') ? Widget::$DATE_MIN : $mediaNode['fromDt'];
|
|
$widget->toDt = ($mediaNode['toDt'] === '') ? Widget::$DATE_MAX : $mediaNode['toDt'];
|
|
|
|
$this->setWidgetExpiryDatesOrDefault($widget);
|
|
|
|
$this->getLog()->debug('Adding Widget to object model. ' . $widget);
|
|
|
|
// Prepare widget options
|
|
foreach ($mediaNode['widgetOptions'] as $optionsNode) {
|
|
$widgetOption = $this->widgetOptionFactory->createEmpty();
|
|
$widgetOption->type = $optionsNode['type'];
|
|
$widgetOption->option = $optionsNode['option'];
|
|
$widgetOption->value = $optionsNode['value'];
|
|
$widget->widgetOptions[] = $widgetOption;
|
|
}
|
|
|
|
// Resolve the module
|
|
try {
|
|
$module = $this->prepareWidgetAndGetModule($widget);
|
|
} catch (NotFoundException) {
|
|
// Skip this widget
|
|
$this->getLog()->info('loadByJson: ' . $widget->type . ' could not be found or resolved');
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// Get the MediaId associated with this widget
|
|
//
|
|
if ($module->regionSpecific == 0) {
|
|
$this->getLog()->debug('Library Widget, getting mediaId');
|
|
|
|
$this->getLog()->debug(sprintf('Assigning mediaId %d', $widget->tempId));
|
|
$widget->assignMedia($widget->tempId);
|
|
}
|
|
|
|
// if we have any elements with mediaIds, make sure we assign them here
|
|
if ($module->type === 'global' && !empty($mediaNode['mediaIds'])) {
|
|
foreach ($mediaNode['mediaIds'] as $mediaId) {
|
|
$this->getLog()->debug(sprintf('Assigning mediaId %d to element', $mediaId));
|
|
$widget->assignMedia($mediaId);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Audio
|
|
//
|
|
foreach ($mediaNode['audio'] as $audioNode) {
|
|
if ($audioNode == []) {
|
|
continue;
|
|
}
|
|
|
|
$widgetAudio = $this->widgetAudioFactory->createEmpty();
|
|
$widgetAudio->mediaId = $audioNode['mediaId'];
|
|
$widgetAudio->volume = $audioNode['volume'];
|
|
$widgetAudio->loop = $audioNode['loop'];
|
|
$widget->assignAudio($widgetAudio);
|
|
}
|
|
|
|
// Sub-Playlist widgets with Playlists
|
|
if ($widget->type == 'subplaylist') {
|
|
$widgets = [];
|
|
$this->getLog()->debug(
|
|
'Layout import, creating layout Playlists from JSON, there are ' .
|
|
count($playlistJson) . ' Playlists to create'
|
|
);
|
|
|
|
// Get the subplaylists from widget option
|
|
$subPlaylistsOption = json_decode($widget->getOptionValue('subPlaylists', '[]'), true);
|
|
|
|
foreach ($playlistJson as $playlistDetail) {
|
|
$newPlaylist = $this->playlistFactory->createEmpty()->hydrate($playlistDetail);
|
|
$newPlaylist->tags = [];
|
|
|
|
// Populate tags
|
|
if ($playlistDetail['tags'] !== null && count($playlistDetail['tags']) > 0 && $importTags) {
|
|
foreach ($playlistDetail['tags'] as $tag) {
|
|
$newPlaylist->tags[] = $this->tagFactory->tagFromString(
|
|
$tag['tag'] . (!empty($tag['value']) ? '|' . $tag['value'] : '')
|
|
);
|
|
}
|
|
}
|
|
|
|
// Check to see if it matches our Sub-Playlist widget config
|
|
foreach ($subPlaylistsOption as $subPlaylistItem) {
|
|
if ($newPlaylist->playlistId === intval($subPlaylistItem['playlistId'])) {
|
|
// Store the oldId to swap permissions later
|
|
$oldIds[] = $newPlaylist->playlistId;
|
|
|
|
// Store the Widgets on the Playlist
|
|
$widgets[$newPlaylist->playlistId] = $newPlaylist->widgets;
|
|
|
|
// Save a new Playlist and capture the Id
|
|
$this->setOwnerAndSavePlaylist($newPlaylist, $folder);
|
|
|
|
$newIds[] = $newPlaylist->playlistId;
|
|
}
|
|
}
|
|
}
|
|
|
|
$combined = array_combine($oldIds, $newIds);
|
|
|
|
$playlists = $this->createNestedPlaylistWidgets($widgets, $combined, $playlists);
|
|
$updatedSubPlaylists = [];
|
|
foreach ($combined as $old => $new) {
|
|
foreach ($subPlaylistsOption as $subPlaylistItem) {
|
|
if (intval($subPlaylistItem['playlistId']) === $old) {
|
|
$subPlaylistItem['playlistId'] = $new;
|
|
$updatedSubPlaylists[] = $subPlaylistItem;
|
|
}
|
|
}
|
|
}
|
|
|
|
$widget->setOptionValue('subPlaylists', 'attrib', json_encode($updatedSubPlaylists));
|
|
}
|
|
|
|
// Add the widget to the regionPlaylist
|
|
$playlist->assignWidget($widget);
|
|
|
|
// interactive Actions
|
|
$actions = $mediaNode['actions'] ?? [];
|
|
foreach ($actions as $action) {
|
|
$newAction = $this->actionFactory->create(
|
|
$action['triggerType'],
|
|
$action['triggerCode'],
|
|
$action['actionType'],
|
|
'importWidget',
|
|
$action['sourceId'],
|
|
$action['target'],
|
|
$action['targetId'],
|
|
$action['widgetId'],
|
|
$action['layoutCode'],
|
|
$action['layoutId'] ?? null
|
|
);
|
|
$newAction->save(['validate' => false]);
|
|
}
|
|
}
|
|
|
|
// Assign Playlist to the Region
|
|
$region->regionPlaylist = $playlist;
|
|
|
|
// Assign the region to the Layout
|
|
if ($region->isDrawer === 1) {
|
|
$layout->drawers[] = $region;
|
|
} else {
|
|
$layout->regions[] = $region;
|
|
}
|
|
}
|
|
|
|
$this->getLog()->debug(sprintf('Finished loading layout - there are %d regions.', count($layout->regions)));
|
|
|
|
$this->getLog()->debug(sprintf('Finished loading layout - there are %d drawer regions.', count($layout->drawers)));
|
|
|
|
if ($importTags) {
|
|
foreach ($layoutJson['layoutDefinitions']['tags'] as $tagNode) {
|
|
if ($tagNode == []) {
|
|
continue;
|
|
}
|
|
|
|
$layout->assignTag($this->tagFactory->tagFromString(
|
|
$tagNode['tag'] . (!empty($tagNode['value']) ? '|' . $tagNode['value'] : '')
|
|
));
|
|
}
|
|
}
|
|
|
|
// The parsed, finished layout
|
|
return [$layout, $playlists];
|
|
}
|
|
|
|
/**
|
|
* Create Layout from ZIP File
|
|
* @param string $zipFile
|
|
* @param string $layoutName
|
|
* @param int $userId
|
|
* @param int $template Are we importing a layout to be used as a template?
|
|
* @param int $replaceExisting
|
|
* @param int $importTags
|
|
* @param bool $useExistingDataSets
|
|
* @param bool $importDataSetData
|
|
* @param DataSetFactory $dataSetFactory
|
|
* @param string $tags
|
|
* @param MediaServiceInterface $mediaService
|
|
* @param int $folderId
|
|
* @param bool $isSystemTags Should we add the system tags (currently the "imported" tag)
|
|
* @return Layout
|
|
* @throws \FontLib\Exception\FontNotFoundException
|
|
* @throws \Xibo\Support\Exception\GeneralException
|
|
*/
|
|
public function createFromZip(
|
|
$zipFile,
|
|
$layoutName,
|
|
$userId,
|
|
$template,
|
|
$replaceExisting,
|
|
$importTags,
|
|
$useExistingDataSets,
|
|
$importDataSetData,
|
|
$dataSetFactory,
|
|
$tags,
|
|
MediaServiceInterface $mediaService,
|
|
int $folderId,
|
|
bool $isSystemTags = true,
|
|
) {
|
|
$this->getLog()->debug(sprintf(
|
|
'Create Layout from ZIP File: %s, imported name will be %s.',
|
|
$zipFile,
|
|
$layoutName
|
|
));
|
|
|
|
$libraryLocation = $this->config->getSetting('LIBRARY_LOCATION');
|
|
$libraryLocationTemp = $libraryLocation . 'temp/';
|
|
|
|
// Do some pre-checks on the arguments we have been provided
|
|
if (!file_exists($zipFile)) {
|
|
throw new InvalidArgumentException(__('File does not exist'));
|
|
}
|
|
|
|
// Open the Zip file
|
|
$zip = new \ZipArchive();
|
|
if (!$zip->open($zipFile)) {
|
|
throw new InvalidArgumentException(__('Unable to open ZIP'));
|
|
}
|
|
|
|
// Get the layout details
|
|
$layoutJson = $zip->getFromName('layout.json');
|
|
if (!$layoutJson) {
|
|
throw new InvalidArgumentException(__('Unable to read layout details from ZIP'));
|
|
}
|
|
|
|
$layoutDetails = json_decode($layoutJson, true);
|
|
|
|
// Get the Playlist details
|
|
$playlistDetails = $zip->getFromName('playlist.json');
|
|
$nestedPlaylistDetails = $zip->getFromName('nestedPlaylist.json');
|
|
$folder = $this->folderFactory->getById($folderId);
|
|
|
|
// it is no longer possible to re-create a Layout just from xlf
|
|
// as such if layoutDefinitions are missing, we need to throw an error here.
|
|
if (array_key_exists('layoutDefinitions', $layoutDetails)) {
|
|
// Construct the Layout
|
|
if ($playlistDetails !== false) {
|
|
$playlistDetails = json_decode(($playlistDetails), true);
|
|
} else {
|
|
$playlistDetails = [];
|
|
}
|
|
|
|
if ($nestedPlaylistDetails !== false) {
|
|
$nestedPlaylistDetails = json_decode($nestedPlaylistDetails, true);
|
|
} else {
|
|
$nestedPlaylistDetails = [];
|
|
}
|
|
|
|
$jsonResults = $this->loadByJson(
|
|
$layoutDetails,
|
|
$playlistDetails,
|
|
$nestedPlaylistDetails,
|
|
$folder,
|
|
null,
|
|
$importTags
|
|
);
|
|
/** @var Layout $layout */
|
|
$layout = $jsonResults[0];
|
|
/** @var Playlist[] $playlists */
|
|
$playlists = $jsonResults[1];
|
|
|
|
if (array_key_exists('code', $layoutDetails['layoutDefinitions'])) {
|
|
// Layout code, remove it if Layout with the same code already exists in the CMS,
|
|
// otherwise import would fail.
|
|
// if the code does not exist, then persist it on import.
|
|
try {
|
|
$this->getByCode($layoutDetails['layoutDefinitions']['code']);
|
|
$layout->code = null;
|
|
} catch (NotFoundException $exception) {
|
|
$layout->code = $layoutDetails['layoutDefinitions']['code'];
|
|
}
|
|
}
|
|
} else {
|
|
throw new InvalidArgumentException(
|
|
__('Unsupported format. Missing Layout definitions from layout.json file in the archive.')
|
|
);
|
|
}
|
|
|
|
$this->getLog()->debug('Layout Loaded: ' . $layout);
|
|
|
|
// Ensure width and height are integer type for resolution validation purpose xibosignage/xibo#1648
|
|
$layout->width = (int)$layout->width;
|
|
$layout->height = (int)$layout->height;
|
|
|
|
// Override the name/description
|
|
$layout->layout = (($layoutName != '') ? $layoutName : $layoutDetails['layout']);
|
|
$layout->description = $layoutDetails['description'] ?? '';
|
|
|
|
// Get global stat setting of layout to on/off proof of play statistics
|
|
$layout->enableStat = $this->config->getSetting('LAYOUT_STATS_ENABLED_DEFAULT');
|
|
|
|
$this->getLog()->debug('Layout Loaded: ' . $layout);
|
|
|
|
// Check that the resolution we have in this layout exists, and if not create it.
|
|
try {
|
|
if ($layout->schemaVersion < 2) {
|
|
$this->resolutionFactory->getByDesignerDimensions($layout->width, $layout->height);
|
|
} else {
|
|
$this->resolutionFactory->getByDimensions($layout->width, $layout->height);
|
|
}
|
|
} catch (NotFoundException $notFoundException) {
|
|
$this->getLog()->info('Import is for an unknown resolution, we will create it with name: '
|
|
. $layout->width . ' x ' . $layout->height);
|
|
|
|
$resolution = $this->resolutionFactory->create(
|
|
$layout->width . ' x ' . $layout->height,
|
|
(int)$layout->width,
|
|
(int)$layout->height
|
|
);
|
|
$resolution->userId = $userId;
|
|
$resolution->save();
|
|
}
|
|
|
|
// Update region names
|
|
if (isset($layoutDetails['regions']) && count($layoutDetails['regions']) > 0) {
|
|
$this->getLog()->debug('Updating region names according to layout.json');
|
|
foreach ($layout->regions as $region) {
|
|
if (array_key_exists($region->tempId, $layoutDetails['regions'])
|
|
&& !empty($layoutDetails['regions'][$region->tempId])
|
|
) {
|
|
$region->name = $layoutDetails['regions'][$region->tempId];
|
|
$region->regionPlaylist->name = $layoutDetails['regions'][$region->tempId];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update drawer region names
|
|
if (isset($layoutDetails['drawers']) && count($layoutDetails['drawers']) > 0) {
|
|
$this->getLog()->debug('Updating drawer region names according to layout.json');
|
|
foreach ($layout->drawers as $drawer) {
|
|
if (array_key_exists($drawer->tempId, $layoutDetails['drawers'])
|
|
&& !empty($layoutDetails['drawers'][$drawer->tempId])
|
|
) {
|
|
$drawer->name = $layoutDetails['drawers'][$drawer->tempId];
|
|
$drawer->regionPlaylist->name = $layoutDetails['drawers'][$drawer->tempId];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove the tags if necessary
|
|
if (!$importTags) {
|
|
$this->getLog()->debug('Removing tags from imported layout');
|
|
$layout->tags = [];
|
|
}
|
|
|
|
// Add the template tag if we are importing a template
|
|
if ($template) {
|
|
$layout->assignTag($this->tagFactory->tagFromString('template'));
|
|
}
|
|
|
|
// Add system tags?
|
|
if ($isSystemTags) {
|
|
// Tag as imported
|
|
$layout->assignTag($this->tagFactory->tagFromString('imported'));
|
|
}
|
|
|
|
// Tag from the upload form
|
|
$tagsFromForm = (($tags != '') ? $this->tagFactory->tagsFromString($tags) : []);
|
|
foreach ($tagsFromForm as $tagFromForm) {
|
|
$layout->assignTag($tagFromForm);
|
|
}
|
|
|
|
// Set the owner
|
|
$layout->setOwner($userId, true);
|
|
|
|
// Track if we've added any fonts
|
|
$fontsAdded = false;
|
|
|
|
$widgets = $layout->getAllWidgets();
|
|
$this->getLog()->debug('Layout has ' . count($widgets) . ' widgets');
|
|
$this->getLog()->debug('Process mapping.json file.');
|
|
|
|
// Go through each region and add the media (updating the media ids)
|
|
$mappings = json_decode($zip->getFromName('mapping.json'), true);
|
|
$oldMediaIds = [];
|
|
$newMediaIds = [];
|
|
foreach ($mappings as $file) {
|
|
// Import the Media File
|
|
$intendedMediaName = $file['name'];
|
|
|
|
// Validate the file name
|
|
$fileName = basename($file['file']);
|
|
if (empty($fileName) || $fileName == '.' || $fileName == '..') {
|
|
$this->getLog()->error('Skipping file on import due to invalid filename. ' . $fileName);
|
|
continue;
|
|
}
|
|
|
|
$temporaryFileName = $libraryLocationTemp . $fileName;
|
|
|
|
// Get the file from the ZIP
|
|
$fileStream = $zip->getStream('library/' . $fileName);
|
|
|
|
if ($fileStream === false) {
|
|
// Log out the entire ZIP file and all entries.
|
|
$log = 'Problem getting library/' . $fileName . '. Files: ';
|
|
for ($i = 0; $i < $zip->numFiles; $i++) {
|
|
$log .= $zip->getNameIndex($i) . ', ';
|
|
}
|
|
|
|
$this->getLog()->error($log);
|
|
|
|
throw new InvalidArgumentException(__('Empty file in ZIP'));
|
|
}
|
|
|
|
// Open a file pointer to stream into
|
|
$temporaryFileStream = fopen($temporaryFileName, 'w');
|
|
if (!$temporaryFileStream) {
|
|
throw new InvalidArgumentException(__('Cannot save media file from ZIP file'), 'temp');
|
|
}
|
|
|
|
// Loop over the file and write into the stream
|
|
while (!feof($fileStream)) {
|
|
fwrite($temporaryFileStream, fread($fileStream, 8192));
|
|
}
|
|
|
|
fclose($fileStream);
|
|
fclose($temporaryFileStream);
|
|
|
|
// Check if it's a font file
|
|
$isFont = (isset($file['font']) && $file['font'] == 1);
|
|
|
|
if ($isFont) {
|
|
try {
|
|
$font = $this->fontFactory->getByName($intendedMediaName);
|
|
if (count($font) <= 0) {
|
|
throw new NotFoundException();
|
|
}
|
|
$this->getLog()->debug('Font already exists with name: ' . $intendedMediaName);
|
|
} catch (NotFoundException) {
|
|
$this->getLog()->debug('Font does not exist in Library, add it ' . $fileName);
|
|
// Add the Font
|
|
$font = $this->fontFactory->createFontFromUpload(
|
|
$temporaryFileName,
|
|
$file['name'],
|
|
$fileName,
|
|
$this->getUser()->userName,
|
|
);
|
|
$font->save();
|
|
$fontsAdded = true;
|
|
|
|
// everything is fine, move the file from temp folder.
|
|
rename($temporaryFileName, $libraryLocation . 'fonts/' . $font->fileName);
|
|
}
|
|
|
|
// Fonts do not create media records, so we have nothing left to do in the rest of this loop
|
|
continue;
|
|
} else {
|
|
try {
|
|
$media = $this->mediaFactory->getByName($intendedMediaName);
|
|
|
|
$this->getLog()->debug('Media already exists with name: ' . $intendedMediaName);
|
|
|
|
if ($replaceExisting) {
|
|
// Media with this name already exists, but we don't want to use it.
|
|
$intendedMediaName = 'import_' . $layout->layout . '_' . uniqid();
|
|
throw new NotFoundException();
|
|
}
|
|
} catch (NotFoundException $e) {
|
|
// Create it instead
|
|
$this->getLog()->debug('Media does not exist in Library, add it ' . $fileName);
|
|
|
|
$media = $this->mediaFactory->create(
|
|
$intendedMediaName,
|
|
$fileName,
|
|
$file['type'],
|
|
$userId,
|
|
$file['duration']
|
|
);
|
|
|
|
if ($importTags && isset($file['tags'])) {
|
|
foreach ($file['tags'] as $tagNode) {
|
|
if ($tagNode == []) {
|
|
continue;
|
|
}
|
|
|
|
$media->assignTag($this->tagFactory->tagFromString(
|
|
$tagNode['tag'] . (!empty($tagNode['value']) ? '|' . $tagNode['value'] : '')
|
|
));
|
|
}
|
|
}
|
|
|
|
$media->assignTag($this->tagFactory->tagFromString('imported'));
|
|
$media->folderId = $folder->id;
|
|
$media->permissionsFolderId =
|
|
($folder->permissionsFolderId == null) ? $folder->id : $folder->permissionsFolderId;
|
|
// Get global stat setting of media to set to on/off/inherit
|
|
$media->enableStat = $this->config->getSetting('MEDIA_STATS_ENABLED_DEFAULT');
|
|
$media->save();
|
|
}
|
|
}
|
|
|
|
// Find where this is used and swap for the real mediaId
|
|
$oldMediaId = $file['mediaid'];
|
|
$newMediaId = $media->mediaId;
|
|
$oldMediaIds[] = $oldMediaId;
|
|
$newMediaIds[] = $newMediaId;
|
|
|
|
if ($file['background'] == 1) {
|
|
// Set the background image on the new layout
|
|
$layout->backgroundImageId = $newMediaId;
|
|
} else {
|
|
// Go through all widgets and replace if necessary
|
|
// Keep the keys the same? Doesn't matter
|
|
foreach ($widgets as $widget) {
|
|
$audioIds = $widget->getAudioIds();
|
|
|
|
$this->getLog()->debug(sprintf(
|
|
'Checking Widget for the old mediaID [%d] so we can replace it with the new mediaId '
|
|
. '[%d] and storedAs [%s]. Media assigned to widget %s.',
|
|
$oldMediaId,
|
|
$newMediaId,
|
|
$media->storedAs,
|
|
json_encode($widget->mediaIds)
|
|
));
|
|
|
|
if (in_array($oldMediaId, $widget->mediaIds)) {
|
|
$this->getLog()->debug(sprintf('Removing %d and replacing with %d', $oldMediaId, $newMediaId));
|
|
|
|
// Are we an audio record?
|
|
if (in_array($oldMediaId, $audioIds)) {
|
|
// Swap the mediaId on the audio record
|
|
foreach ($widget->audio as $widgetAudio) {
|
|
if ($widgetAudio->mediaId == $oldMediaId) {
|
|
$widgetAudio->mediaId = $newMediaId;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
// Non audio
|
|
$widget->setOptionValue('uri', 'attrib', $media->storedAs);
|
|
}
|
|
|
|
// Always manage the assignments
|
|
// Unassign the old ID
|
|
$widget->unassignMedia($oldMediaId);
|
|
|
|
// Assign the new ID
|
|
$widget->assignMedia($newMediaId);
|
|
}
|
|
|
|
// change mediaId references in applicable widgets, outside the if condition,
|
|
// because if the Layout is loadByXLF we will not have mediaIds set on Widget at this point
|
|
// the mediaIds array for Widgets with Library references will be correctly populated on
|
|
// getResource call from Player/CMS.
|
|
// if the Layout was loadByJson then it will already have correct mediaIds array at this point.
|
|
$this->handleWidgetMediaIdReferences($widget, $newMediaId, $oldMediaId);
|
|
}
|
|
}
|
|
}
|
|
$uploadedMediaIds = array_combine($oldMediaIds, $newMediaIds);
|
|
|
|
foreach ($widgets as $widget) {
|
|
// handle importing elements with image.
|
|
// if we have multiple images in global widget
|
|
// we need to go through them here and replace all old media with new ones
|
|
// this cannot be done one by one in the loop when uploading from mapping
|
|
// as one widget can have multiple elements with mediaId in it.
|
|
if ($widget->type === 'global' && !empty($widget->getOptionValue('elements', []))) {
|
|
$widgetElements = $widget->getOptionValue('elements', null);
|
|
$widgetElements = json_decode($widgetElements, true);
|
|
$updatedWidgetElements = [];
|
|
$updatedElements = [];
|
|
foreach (($widgetElements ?? []) as $widgetElement) {
|
|
foreach (($widgetElement['elements'] ?? []) as $element) {
|
|
if (isset($element['mediaId'])) {
|
|
foreach ($uploadedMediaIds as $old => $new) {
|
|
if ($element['mediaId'] === $old) {
|
|
$element['mediaId'] = $new;
|
|
}
|
|
}
|
|
}
|
|
// if we have combo of say text element and image
|
|
// make sure we have the element updated here (outside the if condition),
|
|
// otherwise we would end up only with image elements in the options.
|
|
$updatedElements[] = $element;
|
|
}
|
|
}
|
|
|
|
if (!empty($updatedElements)) {
|
|
$updatedWidgetElements[]['elements'] = $updatedElements;
|
|
$widget->setOptionValue(
|
|
'elements',
|
|
'raw',
|
|
json_encode($updatedWidgetElements)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Playlists with media widgets
|
|
// We will iterate through all Playlists we've created during layout import here and
|
|
// replace any mediaIds if needed
|
|
if (isset($playlists) && $playlistDetails !== false) {
|
|
foreach ($playlists as $playlist) {
|
|
foreach ($playlist->widgets as $widget) {
|
|
$audioIds = $widget->getAudioIds();
|
|
|
|
foreach ($widget->mediaIds as $mediaId) {
|
|
foreach ($uploadedMediaIds as $old => $new) {
|
|
if ($mediaId == $old) {
|
|
$this->getLog()->debug(sprintf(
|
|
'Playlist import Removing %d and replacing with %d',
|
|
$old,
|
|
$new
|
|
));
|
|
|
|
// Are we an audio record?
|
|
if (in_array($old, $audioIds)) {
|
|
// Swap the mediaId on the audio record
|
|
foreach ($widget->audio as $widgetAudio) {
|
|
if ($widgetAudio->mediaId == $old) {
|
|
$widgetAudio->mediaId = $new;
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
$addedMedia = $this->mediaFactory->getById($new);
|
|
// Non audio
|
|
$widget->setOptionValue('uri', 'attrib', $addedMedia->storedAs);
|
|
}
|
|
|
|
// Always manage the assignments
|
|
// Unassign the old ID
|
|
$widget->unassignMedia($old);
|
|
|
|
// Assign the new ID
|
|
$widget->assignMedia($new);
|
|
|
|
// change mediaId references in applicable widgets in all Playlists we have created
|
|
// on this import.
|
|
$this->handleWidgetMediaIdReferences($widget, $new, $old);
|
|
}
|
|
}
|
|
}
|
|
$widget->save();
|
|
|
|
if (!in_array($widget, $playlist->widgets)) {
|
|
$playlist->assignWidget($widget);
|
|
$playlist->requiresDurationUpdate = 1;
|
|
$playlist->save();
|
|
}
|
|
|
|
// add Playlist widgets to the $widgets (which already has all widgets from layout regionPlaylists)
|
|
// this will be needed if any Playlist has widgets with dataSets
|
|
if ($widget->type == 'datasetview'
|
|
|| $widget->type == 'datasetticker'
|
|
|| $widget->type == 'chart'
|
|
) {
|
|
$widgets[] = $widget;
|
|
$playlistWidgets[] = $widget;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle any datasets provided with the layout
|
|
$dataSets = $zip->getFromName('dataSet.json');
|
|
|
|
if ($dataSets !== false) {
|
|
$dataSets = json_decode($dataSets, true);
|
|
|
|
$this->getLog()->debug('There are ' . count($dataSets) . ' DataSets to import.');
|
|
|
|
foreach ($dataSets as $item) {
|
|
// Hydrate a new dataset object with this json object
|
|
$dataSet = $dataSetFactory->createEmpty()->hydrate($item);
|
|
$dataSet->columns = [];
|
|
$dataSetId = $dataSet->dataSetId;
|
|
$columnWithImages = [];
|
|
// We must null the ID so that we don't try to load the dataset when we assign columns
|
|
$dataSet->dataSetId = null;
|
|
|
|
// Hydrate the columns
|
|
foreach ($item['columns'] as $columnItem) {
|
|
$this->getLog()->debug(sprintf('Assigning column: %s', json_encode($columnItem)));
|
|
if ($columnItem['dataTypeId'] === 5) {
|
|
$columnWithImages[] = $columnItem['heading'];
|
|
}
|
|
$dataSet->assignColumn($dataSetFactory
|
|
->getDataSetColumnFactory()
|
|
->createEmpty()
|
|
->hydrate($columnItem));
|
|
}
|
|
|
|
/** @var DataSet $existingDataSet */
|
|
$existingDataSet = null;
|
|
|
|
// Do we want to try and use a dataset that already exists?
|
|
if ($useExistingDataSets) {
|
|
// Check to see if we already have a dataset with the same code/name, prefer code.
|
|
if ($dataSet->code != '') {
|
|
try {
|
|
// try and get by code
|
|
$existingDataSet = $dataSetFactory->getByCode($dataSet->code);
|
|
} catch (NotFoundException $e) {
|
|
$this->getLog()->debug(sprintf('Existing dataset not found with code %s', $dataSet->code));
|
|
}
|
|
}
|
|
|
|
if ($existingDataSet === null) {
|
|
// try by name
|
|
try {
|
|
$existingDataSet = $dataSetFactory->getByName($dataSet->dataSet);
|
|
} catch (NotFoundException $e) {
|
|
$this->getLog()->debug(sprintf('Existing dataset not found with name %s', $dataSet->code));
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($existingDataSet === null) {
|
|
$this->getLog()->debug(sprintf(
|
|
'Matching DataSet not found, will need to add one. useExistingDataSets = %s',
|
|
$useExistingDataSets
|
|
));
|
|
|
|
// We want to add the dataset we have as a new dataset.
|
|
// we will need to make sure we clear the ID's and save it
|
|
$existingDataSet = clone $dataSet;
|
|
$existingDataSet->userId = $this->getUser()->userId;
|
|
$existingDataSet->folderId = $folder->id;
|
|
$existingDataSet->permissionsFolderId =
|
|
($folder->permissionsFolderId == null) ? $folder->id : $folder->permissionsFolderId;
|
|
|
|
// Save to get the IDs created
|
|
$existingDataSet->save([
|
|
'activate' => false,
|
|
'notify' => false,
|
|
'testFormulas' => false,
|
|
'allowSpacesInHeading' => true,
|
|
]);
|
|
|
|
// Do we need to add data
|
|
if ($importDataSetData) {
|
|
// Import the data here
|
|
$this->getLog()->debug(sprintf(
|
|
'Importing data into new DataSet %d',
|
|
$existingDataSet->dataSetId
|
|
));
|
|
|
|
foreach (($item['data'] ?? []) as $itemData) {
|
|
if (isset($itemData['id'])) {
|
|
unset($itemData['id']);
|
|
}
|
|
|
|
foreach ($columnWithImages as $columnHeading) {
|
|
foreach ($uploadedMediaIds as $old => $new) {
|
|
if ($itemData[$columnHeading] == $old) {
|
|
$itemData[$columnHeading] = $new;
|
|
}
|
|
}
|
|
}
|
|
|
|
$existingDataSet->addRow($itemData);
|
|
}
|
|
}
|
|
} else {
|
|
$this->getLog()->debug('Matching DataSet found, validating the columns');
|
|
|
|
// Load the existing dataset
|
|
$existingDataSet->load();
|
|
|
|
// Validate that the columns are the same
|
|
if (count($dataSet->columns) != count($existingDataSet->columns)) {
|
|
$this->getLog()->debug(sprintf(
|
|
'Columns for Imported DataSet = %s',
|
|
json_encode($dataSet->columns)
|
|
));
|
|
throw new InvalidArgumentException(sprintf(
|
|
__('DataSets have different number of columns imported = %d, existing = %d'),
|
|
count($dataSet->columns),
|
|
count($existingDataSet->columns)
|
|
));
|
|
}
|
|
|
|
// Loop over the desired column headings and the ones in the existing dataset and error out
|
|
// as soon as we have one that isn't found.
|
|
foreach ($dataSet->columns as $column) {
|
|
// Loop through until we find it
|
|
foreach ($existingDataSet->columns as $existingDataSetColumn) {
|
|
if ($column->heading === $existingDataSetColumn->heading) {
|
|
// Drop out to the next column we want to find.
|
|
continue 2;
|
|
}
|
|
}
|
|
// We have not found that column in our existing data set
|
|
throw new InvalidArgumentException(__('DataSets have different column names'));
|
|
}
|
|
}
|
|
|
|
// Set the prior dataSetColumnId on each column.
|
|
foreach ($existingDataSet->columns as $column) {
|
|
// Lookup the matching column in the external dataSet definition.
|
|
foreach ($dataSet->columns as $externalColumn) {
|
|
if ($externalColumn->heading == $column->heading) {
|
|
$column->priorDatasetColumnId = $externalColumn->dataSetColumnId;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Replace instances of this dataSetId with the existing dataSetId, which will either be the existing
|
|
// dataSet or one we've added above.
|
|
// Also make sure we replace the columnId's with the columnId's in the new "existing" DataSet.
|
|
foreach ($widgets as $widget) {
|
|
if ($widget->type == 'dataset') {
|
|
$widgetDataSetId = $widget->getOptionValue('dataSetId', 0);
|
|
|
|
if ($widgetDataSetId != 0 && $widgetDataSetId == $dataSetId) {
|
|
// Widget has a dataSet, and it matches the one we've just actioned.
|
|
$widget->setOptionValue('dataSetId', 'attrib', $existingDataSet->dataSetId);
|
|
|
|
// Check for and replace column references.
|
|
// We are looking in the "columns" option for datasetview
|
|
// and the "template" option for datasetticker
|
|
// DataSetView (now just dataset)
|
|
$existingColumns = $widget->getOptionValue('columns', '');
|
|
if (!empty($existingColumns)) {
|
|
// Get the columns option
|
|
$columns = json_decode($existingColumns, true);
|
|
|
|
$this->getLog()->debug(sprintf(
|
|
'Looking to replace columns from %s',
|
|
$existingColumns
|
|
));
|
|
|
|
foreach ($existingDataSet->columns as $column) {
|
|
foreach ($columns as $index => $col) {
|
|
if ($col == $column->priorDatasetColumnId) {
|
|
// v4 uses integers as its column ids.
|
|
$columns[$index] = $column->dataSetColumnId;
|
|
}
|
|
}
|
|
}
|
|
|
|
$columns = json_encode($columns);
|
|
$widget->setOptionValue('columns', 'attrib', $columns);
|
|
|
|
$this->getLog()->debug(sprintf('Replaced columns with %s', $columns));
|
|
}
|
|
|
|
// DataSetTicker (now just dataset)
|
|
$template = $widget->getOptionValue('template', '');
|
|
if (!empty($template)) {
|
|
$this->getLog()->debug(sprintf('Looking to replace columns from %s', $template));
|
|
|
|
foreach ($existingDataSet->columns as $column) {
|
|
// We replace with the |%d] so that we don't experience double replacements
|
|
$template = str_replace(
|
|
'|' . $column->priorDatasetColumnId . ']',
|
|
'|' . $column->dataSetColumnId . ']',
|
|
$template
|
|
);
|
|
}
|
|
|
|
$widget->setOptionValue('template', 'cdata', $template);
|
|
|
|
$this->getLog()->debug(sprintf('Replaced columns with %s', $template));
|
|
}
|
|
}
|
|
|
|
// save widgets with dataSets on Playlists, widgets directly on the layout are saved later on.
|
|
if (isset($playlistWidgets) && in_array($widget, $playlistWidgets)) {
|
|
$widget->save();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Load widget data into an array for processing outside (once the layout has been saved)
|
|
$fallback = $zip->getFromName('fallback.json');
|
|
if ($fallback !== false) {
|
|
$layout->setUnmatchedProperty('fallback', json_decode($fallback, true));
|
|
}
|
|
|
|
// Save the thumbnail to a temporary location.
|
|
$image_path = $zip->getFromName('library/thumbs/campaign_thumb.png');
|
|
if ($image_path !== false) {
|
|
$temporaryLayoutThumb = $libraryLocationTemp . $layout->layout . '-campaign_thumb.png';
|
|
$layout->setUnmatchedProperty('thumbnail', $temporaryLayoutThumb);
|
|
$image = imagecreatefromstring($image_path);
|
|
imagepng($image, $temporaryLayoutThumb);
|
|
}
|
|
|
|
$this->getLog()->debug('Finished creating from Zip');
|
|
|
|
// Finished
|
|
$zip->close();
|
|
|
|
// We need one final pass through all widgets on the layout so that we can set the durations properly.
|
|
foreach ($layout->getAllWidgets() as $widget) {
|
|
// By now we should not have any modules which don't exist.
|
|
$module = $this->moduleFactory->getByType($widget->type);
|
|
$widget->calculateDuration($module);
|
|
|
|
// Get global stat setting of widget to set to on/off/inherit
|
|
$widget->setOptionValue('enableStat', 'attrib', $this->config->getSetting('WIDGET_STATS_ENABLED_DEFAULT'));
|
|
}
|
|
|
|
if ($fontsAdded) {
|
|
$this->getLog()->debug('Fonts have been added');
|
|
$mediaService->setUser($this->getUser())->updateFontsCss();
|
|
}
|
|
|
|
return $layout;
|
|
}
|
|
|
|
/**
|
|
* Create widgets in nested Playlists and handle their closure table
|
|
*
|
|
* @param $widgets array An array of playlist widgets with old playlistId as key
|
|
* @param $combined array An array of key and value pairs with oldPlaylistId => newPlaylistId
|
|
* @param $playlists array An array of Playlist objects
|
|
* @return array An array of Playlist objects with widgets
|
|
* @throws NotFoundException
|
|
*/
|
|
public function createNestedPlaylistWidgets($widgets, $combined, &$playlists)
|
|
{
|
|
foreach ($widgets as $playlistId => $widgetsDetails) {
|
|
foreach ($combined as $old => $new) {
|
|
if ($old == $playlistId) {
|
|
$playlistId = $new;
|
|
}
|
|
}
|
|
|
|
$playlist = $this->playlistFactory->getById($playlistId);
|
|
|
|
foreach ($widgetsDetails as $widgetsDetail) {
|
|
$modules = $this->moduleFactory->getKeyedArrayOfModules();
|
|
$playlistWidget = $this->widgetFactory->createEmpty();
|
|
$playlistWidget->playlistId = $playlistId;
|
|
$playlistWidget->widgetId = null;
|
|
$playlistWidget->type = $widgetsDetail['type'];
|
|
$playlistWidget->ownerId = $playlist->ownerId;
|
|
$playlistWidget->displayOrder = $widgetsDetail['displayOrder'];
|
|
$playlistWidget->duration = $widgetsDetail['duration'];
|
|
$playlistWidget->useDuration = $widgetsDetail['useDuration'];
|
|
$playlistWidget->calculatedDuration = $widgetsDetail['calculatedDuration'];
|
|
$playlistWidget->fromDt = $widgetsDetail['fromDt'];
|
|
$playlistWidget->toDt = $widgetsDetail['toDt'];
|
|
$playlistWidget->tempId = $widgetsDetail['tempId'];
|
|
$playlistWidget->mediaIds = $widgetsDetail['mediaIds'];
|
|
$playlistWidget->widgetOptions = [];
|
|
$playlistWidget->schemaVersion = isset($widgetsDetail['schemaVersion'])
|
|
? (int)$widgetsDetail['schemaVersion']
|
|
: 1;
|
|
|
|
// Prepare widget options
|
|
foreach ($widgetsDetail['widgetOptions'] as $optionsNode) {
|
|
$widgetOption = $this->widgetOptionFactory->createEmpty();
|
|
$widgetOption->type = $optionsNode['type'];
|
|
$widgetOption->option = $optionsNode['option'];
|
|
$widgetOption->value = $optionsNode['value'];
|
|
$playlistWidget->widgetOptions[] = $widgetOption;
|
|
}
|
|
|
|
try {
|
|
$module = $this->prepareWidgetAndGetModule($playlistWidget);
|
|
} catch (NotFoundException) {
|
|
// Skip this widget
|
|
$this->getLog()->info('createNestedPlaylistWidgets: ' . $playlistWidget->type
|
|
. ' could not be found or resolved');
|
|
continue;
|
|
}
|
|
|
|
if ($playlistWidget->type == 'subplaylist') {
|
|
// Get the subplaylists from widget option
|
|
$nestedSubPlaylists = json_decode($playlistWidget->getOptionValue('subPlaylists', '[]'), true);
|
|
|
|
$updatedSubPlaylists = [];
|
|
foreach ($combined as $old => $new) {
|
|
foreach ($nestedSubPlaylists as $subPlaylistItem) {
|
|
if (intval($subPlaylistItem['playlistId']) === $old) {
|
|
$subPlaylistItem['playlistId'] = $new;
|
|
$updatedSubPlaylists[] = $subPlaylistItem;
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach ($updatedSubPlaylists as $updatedSubPlaylistItem) {
|
|
$this->getStore()->insert('
|
|
INSERT INTO `lkplaylistplaylist` (parentId, childId, depth)
|
|
SELECT p.parentId, c.childId, p.depth + c.depth + 1
|
|
FROM lkplaylistplaylist p, lkplaylistplaylist c
|
|
WHERE p.childId = :parentId AND c.parentId = :childId
|
|
', [
|
|
'parentId' => $playlist->playlistId,
|
|
'childId' => $updatedSubPlaylistItem['playlistId']
|
|
]);
|
|
}
|
|
|
|
$playlistWidget->setOptionValue('subPlaylists', 'attrib', json_encode($updatedSubPlaylists));
|
|
}
|
|
|
|
$playlist->assignWidget($playlistWidget);
|
|
$playlist->requiresDurationUpdate = 1;
|
|
|
|
// save non-media based widget, we can't save media based widgets here as we don't have updated mediaId yet.
|
|
if ($module->regionSpecific == 1 && $playlistWidget->mediaIds == []) {
|
|
$playlistWidget->save();
|
|
}
|
|
}
|
|
|
|
$playlists[] = $playlist;
|
|
$this->getLog()->debug('Finished creating Playlist added the following Playlist ' . json_encode($playlist));
|
|
}
|
|
|
|
return $playlists;
|
|
}
|
|
|
|
public function hasSubPlaylist(array $widgets)
|
|
{
|
|
$hasSubPlaylist = false;
|
|
|
|
foreach ($widgets as $widget) {
|
|
if ($widget['type'] === 'subplaylist') {
|
|
$hasSubPlaylist = true;
|
|
}
|
|
}
|
|
|
|
return $hasSubPlaylist;
|
|
}
|
|
|
|
/**
|
|
* Get all Codes assigned to Layouts
|
|
* @param array $filterBy
|
|
* @return array
|
|
*/
|
|
public function getLayoutCodes($filterBy = []): array
|
|
{
|
|
$parsedFilter = $this->getSanitizer($filterBy);
|
|
$params = [];
|
|
$select = 'SELECT DISTINCT code, `layout`.layout, `campaign`.CampaignID, `campaign`.permissionsFolderId ';
|
|
$body = ' FROM layout INNER JOIN `lkcampaignlayout` ON lkcampaignlayout.LayoutID = layout.LayoutID INNER JOIN `campaign` ON lkcampaignlayout.CampaignID = campaign.CampaignID AND campaign.IsLayoutSpecific = 1 WHERE `layout`.code IS NOT NULL AND `layout`.code <> \'\' ';
|
|
|
|
// get by Code
|
|
if ($parsedFilter->getString('code') != '') {
|
|
$body.= ' AND layout.code LIKE :code ';
|
|
$params['code'] = '%' . $parsedFilter->getString('code') . '%';
|
|
}
|
|
|
|
// Logged in user view permissions
|
|
$this->viewPermissionSql('Xibo\Entity\Campaign', $body, $params, 'campaign.campaignId', 'layout.userId', $filterBy, 'campaign.permissionsFolderId');
|
|
|
|
$order = ' ORDER BY code';
|
|
|
|
// Paging
|
|
$limit = '';
|
|
if ($filterBy !== null && $parsedFilter->getInt('start') !== null && $parsedFilter->getInt('length') !== null) {
|
|
$limit = ' LIMIT ' . $parsedFilter->getInt('start', ['default' => 0]) . ', ' . $parsedFilter->getInt('length', ['default' => 10]);
|
|
}
|
|
|
|
$sql = $select . $body . $order . $limit;
|
|
$entries = $this->getStore()->select($sql, $params);
|
|
|
|
// Paging
|
|
if ($limit != '' && count($entries) > 0) {
|
|
$results = $this->getStore()->select('SELECT COUNT(*) AS total ' . $body, $params);
|
|
$this->_countLast = intval($results[0]['total']);
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* Query for all Layouts
|
|
* @param array $sortOrder
|
|
* @param array $filterBy
|
|
* @return Layout[]
|
|
* @throws NotFoundException
|
|
*/
|
|
public function query($sortOrder = null, $filterBy = [])
|
|
{
|
|
$parsedFilter = $this->getSanitizer($filterBy);
|
|
$entries = [];
|
|
$params = [];
|
|
|
|
if ($sortOrder === null) {
|
|
$sortOrder = ['layout'];
|
|
}
|
|
|
|
$select = 'SELECT `layout`.layoutID,
|
|
`layout`.parentId,
|
|
`layout`.layout,
|
|
`layout`.description,
|
|
`layout`.duration,
|
|
`layout`.userID,
|
|
`user`.userName as owner,
|
|
`campaign`.CampaignID,
|
|
`campaign`.type,
|
|
`layout`.status,
|
|
`layout`.statusMessage,
|
|
`layout`.enableStat,
|
|
`layout`.width,
|
|
`layout`.height,
|
|
`layout`.retired,
|
|
`layout`.createdDt,
|
|
`layout`.modifiedDt,
|
|
`layout`.backgroundImageId,
|
|
`layout`.backgroundColor,
|
|
`layout`.backgroundzIndex,
|
|
`layout`.schemaVersion,
|
|
`layout`.publishedStatusId,
|
|
`status`.status AS publishedStatus,
|
|
`layout`.publishedDate,
|
|
`layout`.autoApplyTransitions,
|
|
`layout`.code,
|
|
`campaign`.folderId,
|
|
`campaign`.permissionsFolderId,
|
|
';
|
|
|
|
if ($parsedFilter->getInt('campaignId') !== null) {
|
|
$select .= ' lkcl.displayOrder, ';
|
|
} else {
|
|
$select .= ' NULL as displayOrder, ';
|
|
}
|
|
|
|
$select .= " (SELECT GROUP_CONCAT(DISTINCT `group`.group)
|
|
FROM `permission`
|
|
INNER JOIN `permissionentity`
|
|
ON `permissionentity`.entityId = permission.entityId
|
|
INNER JOIN `group`
|
|
ON `group`.groupId = `permission`.groupId
|
|
WHERE entity = :permissionEntityForGroup
|
|
AND objectId = campaign.CampaignID
|
|
AND view = 1
|
|
) AS groupsWithPermissions ";
|
|
$params['permissionEntityForGroup'] = 'Xibo\\Entity\\Campaign';
|
|
|
|
$body = " FROM layout ";
|
|
$body .= ' INNER JOIN status ON status.id = layout.publishedStatusId ';
|
|
$body .= " INNER JOIN `lkcampaignlayout` ";
|
|
$body .= " ON lkcampaignlayout.LayoutID = layout.LayoutID ";
|
|
$body .= " INNER JOIN `campaign` ";
|
|
$body .= " ON lkcampaignlayout.CampaignID = campaign.CampaignID ";
|
|
$body .= " AND campaign.IsLayoutSpecific = 1";
|
|
$body .= " INNER JOIN `user` ON `user`.userId = `campaign`.userId ";
|
|
|
|
if ($parsedFilter->getInt('campaignId') !== null) {
|
|
// Join Campaign back onto it again
|
|
$body .= "
|
|
INNER JOIN `lkcampaignlayout` lkcl
|
|
ON lkcl.layoutid = layout.layoutid
|
|
AND lkcl.CampaignID = :campaignId
|
|
";
|
|
$params['campaignId'] = $parsedFilter->getInt('campaignId');
|
|
}
|
|
|
|
if ($parsedFilter->getInt('displayGroupId') !== null) {
|
|
$body .= '
|
|
INNER JOIN `lklayoutdisplaygroup`
|
|
ON lklayoutdisplaygroup.layoutId = `layout`.layoutId
|
|
AND lklayoutdisplaygroup.displayGroupId = :displayGroupId
|
|
';
|
|
|
|
$params['displayGroupId'] = $parsedFilter->getInt('displayGroupId');
|
|
}
|
|
|
|
if ($parsedFilter->getInt('activeDisplayGroupId') !== null) {
|
|
$displayGroupIds = [];
|
|
$displayId = null;
|
|
|
|
// get the displayId if we were provided with display specific displayGroup in the filter
|
|
$sql = 'SELECT display.displayId FROM display INNER JOIN lkdisplaydg ON lkdisplaydg.displayId = display.displayId INNER JOIN displaygroup ON displaygroup.displayGroupId = lkdisplaydg.displayGroupId WHERE displaygroup.displayGroupId = :displayGroupId AND displaygroup.isDisplaySpecific = 1';
|
|
|
|
foreach ($this->getStore()->select($sql, ['displayGroupId' => $parsedFilter->getInt('activeDisplayGroupId')]) as $row) {
|
|
$displayId = $this->getSanitizer($row)->getInt('displayId');
|
|
}
|
|
|
|
// if we have displayId, get all displayGroups to which the display is a member of
|
|
if ($displayId !== null) {
|
|
$sql = 'SELECT displayGroupId FROM lkdisplaydg WHERE displayId = :displayId';
|
|
|
|
foreach ($this->getStore()->select($sql, ['displayId' => $displayId]) as $row) {
|
|
$displayGroupIds[] = $this->getSanitizer($row)->getInt('displayGroupId');
|
|
}
|
|
}
|
|
|
|
// if we are filtering by actual displayGroup, use just the displayGroupId in the param
|
|
if ($displayGroupIds == []) {
|
|
$displayGroupIds[] = $parsedFilter->getInt('activeDisplayGroupId');
|
|
}
|
|
|
|
// get events for the selected displayGroup / Display and all displayGroups the display is member of
|
|
$body .= '
|
|
INNER JOIN `lkscheduledisplaygroup`
|
|
ON lkscheduledisplaygroup.displayGroupId IN ( ' . implode(',', $displayGroupIds) . ' )
|
|
INNER JOIN schedule
|
|
ON schedule.eventId = lkscheduledisplaygroup.eventId
|
|
';
|
|
}
|
|
|
|
// MediaID
|
|
if ($parsedFilter->getInt('mediaId', ['default' => 0]) != 0) {
|
|
$body .= ' INNER JOIN (
|
|
SELECT DISTINCT `region`.layoutId
|
|
FROM `lkwidgetmedia`
|
|
INNER JOIN `widget`
|
|
ON `widget`.widgetId = `lkwidgetmedia`.widgetId
|
|
INNER JOIN `lkplaylistplaylist`
|
|
ON `widget`.playlistId = `lkplaylistplaylist`.childId
|
|
INNER JOIN `playlist`
|
|
ON `lkplaylistplaylist`.parentId = `playlist`.playlistId
|
|
INNER JOIN `region`
|
|
ON `region`.regionId = `playlist`.regionId
|
|
WHERE `lkwidgetmedia`.mediaId = :mediaId
|
|
) layoutsWithMedia
|
|
ON layoutsWithMedia.layoutId = `layout`.layoutId
|
|
';
|
|
|
|
$params['mediaId'] = $parsedFilter->getInt('mediaId', ['default' => 0]);
|
|
}
|
|
|
|
// Media Like
|
|
if (!empty($parsedFilter->getString('mediaLike'))) {
|
|
$body .= ' INNER JOIN (
|
|
SELECT DISTINCT `region`.layoutId
|
|
FROM `lkwidgetmedia`
|
|
INNER JOIN `widget`
|
|
ON `widget`.widgetId = `lkwidgetmedia`.widgetId
|
|
INNER JOIN `lkplaylistplaylist`
|
|
ON `widget`.playlistId = `lkplaylistplaylist`.childId
|
|
INNER JOIN `playlist`
|
|
ON `lkplaylistplaylist`.parentId = `playlist`.playlistId
|
|
INNER JOIN `region`
|
|
ON `region`.regionId = `playlist`.regionId
|
|
INNER JOIN `media`
|
|
ON `lkwidgetmedia`.mediaId = `media`.mediaId
|
|
WHERE `media`.name LIKE :mediaLike
|
|
) layoutsWithMediaLike
|
|
ON layoutsWithMediaLike.layoutId = `layout`.layoutId
|
|
';
|
|
|
|
$params['mediaLike'] = '%' . $parsedFilter->getString('mediaLike') . '%';
|
|
}
|
|
|
|
$body .= " WHERE 1 = 1 ";
|
|
|
|
// Layout Like
|
|
if ($parsedFilter->getString('layout') != '') {
|
|
$terms = explode(',', $parsedFilter->getString('layout'));
|
|
$logicalOperator = $parsedFilter->getString('logicalOperatorName', ['default' => 'OR']);
|
|
$this->nameFilter(
|
|
'layout',
|
|
'layout',
|
|
$terms,
|
|
$body,
|
|
$params,
|
|
($parsedFilter->getCheckbox('useRegexForName') == 1),
|
|
$logicalOperator
|
|
);
|
|
}
|
|
|
|
if ($parsedFilter->getString('layoutExact') != '') {
|
|
$body.= " AND layout.layout = :exact ";
|
|
$params['exact'] = $parsedFilter->getString('layoutExact');
|
|
}
|
|
|
|
// Layout
|
|
if ($parsedFilter->getInt('layoutId', ['default' => 0]) != 0) {
|
|
$body .= " AND layout.layoutId = :layoutId ";
|
|
$params['layoutId'] = $parsedFilter->getInt('layoutId', ['default' => 0]);
|
|
} else if ($parsedFilter->getInt('excludeTemplates', ['default' => 1]) != -1) {
|
|
// Exclude templates by default
|
|
if ($parsedFilter->getInt('excludeTemplates', ['default' => 1]) == 1) {
|
|
$body .= " AND layout.layoutID NOT IN (SELECT layoutId FROM lktaglayout INNER JOIN tag ON lktaglayout.tagId = tag.tagId WHERE tag = 'template') ";
|
|
} else {
|
|
$body .= " AND layout.layoutID IN (SELECT layoutId FROM lktaglayout INNER JOIN tag ON lktaglayout.tagId = tag.tagId WHERE tag = 'template') ";
|
|
}
|
|
}
|
|
|
|
// Layout Draft
|
|
if ($parsedFilter->getInt('parentId', ['default' => 0]) != 0) {
|
|
$body .= " AND layout.parentId = :parentId ";
|
|
$params['parentId'] = $parsedFilter->getInt('parentId', ['default' => 0]);
|
|
} else if ($parsedFilter->getInt('layoutId', ['default' => 0]) == 0
|
|
&& $parsedFilter->getInt('showDrafts', ['default' => 0]) == 0) {
|
|
// If we're not searching for a parentId and we're not searching for a layoutId, then don't show any
|
|
// drafts (parentId will be empty on drafts)
|
|
$body .= ' AND layout.parentId IS NULL ';
|
|
}
|
|
|
|
// Layout Published Status
|
|
if ($parsedFilter->getInt('publishedStatusId') !== null) {
|
|
$body .= " AND layout.publishedStatusId = :publishedStatusId ";
|
|
$params['publishedStatusId'] = $parsedFilter->getInt('publishedStatusId');
|
|
}
|
|
|
|
// Layout Status
|
|
if ($parsedFilter->getInt('status') !== null) {
|
|
$body .= " AND layout.status = :status ";
|
|
$params['status'] = $parsedFilter->getInt('status');
|
|
}
|
|
|
|
// Layout Duration
|
|
if ($parsedFilter->getInt('duration') !== null) {
|
|
$body .= " AND layout.duration = :duration ";
|
|
$params['duration'] = $parsedFilter->getInt('duration');
|
|
}
|
|
|
|
// Layout Background Color
|
|
if ($parsedFilter->getString('backgroundColor') !== null) {
|
|
$bgConvertedHex = $parsedFilter->getString('backgroundColor');
|
|
|
|
// Handle both shorthand and normal hex values
|
|
if (preg_match('/^#?([0-9a-fA-F]{3})$/', $bgConvertedHex, $matches)) {
|
|
// Convert shorthand hex to normal hex (i.e. #000 -> #000000)
|
|
$bgConvertedHex = '#' . preg_replace('/(.)/', '$1$1', $matches[1]);
|
|
} else {
|
|
// Convert normal hex to shorthand hex (i.e. #000000 -> #000)
|
|
$bgConvertedHex = preg_replace(
|
|
'/^#?([0-9a-fA-F])\1([0-9a-fA-F])\2([0-9a-fA-F])\3$/',
|
|
'#$1$2$3',
|
|
$bgConvertedHex
|
|
);
|
|
}
|
|
|
|
$body .= " AND (layout.backgroundColor = :backgroundColor OR layout.backgroundColor = :bgConvertedHex) ";
|
|
$params['backgroundColor'] = $parsedFilter->getString('backgroundColor');
|
|
$params['bgConvertedHex'] = $bgConvertedHex;
|
|
}
|
|
|
|
// Layout Height
|
|
if ($parsedFilter->getInt('height') !== null) {
|
|
$body .= " AND layout.height = :height ";
|
|
$params['height'] = $parsedFilter->getInt('height');
|
|
}
|
|
|
|
// Layout Width
|
|
if ($parsedFilter->getInt('width') !== null) {
|
|
$body .= " AND layout.width = :width ";
|
|
$params['width'] = $parsedFilter->getInt('width');
|
|
}
|
|
|
|
// Background Image
|
|
if ($parsedFilter->getInt('backgroundImageId') !== null) {
|
|
$body .= " AND layout.backgroundImageId = :backgroundImageId ";
|
|
$params['backgroundImageId'] = $parsedFilter->getInt('backgroundImageId', ['default' => 0]);
|
|
}
|
|
// Not Layout
|
|
if ($parsedFilter->getInt('notLayoutId', ['default' => 0]) != 0) {
|
|
$body .= " AND layout.layoutId <> :notLayoutId ";
|
|
$params['notLayoutId'] = $parsedFilter->getInt('notLayoutId', ['default' => 0]);
|
|
}
|
|
|
|
// Owner filter
|
|
if ($parsedFilter->getInt('userId', ['default' => 0]) != 0) {
|
|
$body .= " AND layout.userid = :userId ";
|
|
$params['userId'] = $parsedFilter->getInt('userId', ['default' => 0]);
|
|
}
|
|
|
|
if ($parsedFilter->getCheckbox('onlyMyLayouts') === 1) {
|
|
$body .= ' AND layout.userid = :userId ';
|
|
$params['userId'] = $this->getUser()->userId;
|
|
}
|
|
|
|
// User Group filter
|
|
if ($parsedFilter->getInt('ownerUserGroupId', ['default' => 0]) != 0) {
|
|
$body .= ' AND layout.userid IN (SELECT DISTINCT userId FROM `lkusergroup` WHERE groupId = :ownerUserGroupId) ';
|
|
$params['ownerUserGroupId'] = $parsedFilter->getInt('ownerUserGroupId', ['default' => 0]);
|
|
}
|
|
|
|
// Retired options (provide -1 to return all)
|
|
if ($parsedFilter->getInt('retired', ['default' => -1]) != -1) {
|
|
$body .= " AND layout.retired = :retired ";
|
|
$params['retired'] = $parsedFilter->getInt('retired',['default' => 0]);
|
|
}
|
|
|
|
// Modified Since?
|
|
if ($parsedFilter->getDate('modifiedSinceDt') != null) {
|
|
$body .= ' AND layout.modifiedDt > :modifiedSinceDt ';
|
|
$params['modifiedSinceDt'] = $parsedFilter->getDate('modifiedSinceDt')
|
|
->format(DateFormatHelper::getSystemFormat());
|
|
}
|
|
|
|
if ($parsedFilter->getInt('ownerCampaignId') !== null) {
|
|
// Join Campaign back onto it again
|
|
$body .= " AND `campaign`.campaignId = :ownerCampaignId ";
|
|
$params['ownerCampaignId'] = $parsedFilter->getInt('ownerCampaignId', ['default' => 0]);
|
|
}
|
|
|
|
if ($parsedFilter->getInt('layoutHistoryId') !== null) {
|
|
$body .= ' AND `campaign`.campaignId IN (
|
|
SELECT MAX(campaignId)
|
|
FROM `layouthistory`
|
|
WHERE `layouthistory`.layoutId = :layoutHistoryId
|
|
) ';
|
|
$params['layoutHistoryId'] = $parsedFilter->getInt('layoutHistoryId');
|
|
}
|
|
|
|
// Get by regionId
|
|
if ($parsedFilter->getInt('regionId') !== null) {
|
|
// Join Campaign back onto it again
|
|
$body .= " AND `layout`.layoutId IN (SELECT layoutId FROM `region` WHERE regionId = :regionId) ";
|
|
$params['regionId'] = $parsedFilter->getInt('regionId', ['default' => 0]);
|
|
}
|
|
|
|
// get by Code
|
|
if ($parsedFilter->getString('code') != '') {
|
|
$body.= " AND layout.code = :code ";
|
|
$params['code'] = $parsedFilter->getString('code');
|
|
}
|
|
|
|
if ($parsedFilter->getString('codeLike') != '') {
|
|
$body.= ' AND layout.code LIKE :codeLike ';
|
|
$params['codeLike'] = '%' . $parsedFilter->getString('codeLike') . '%';
|
|
}
|
|
|
|
// Tags
|
|
if ($parsedFilter->getString('tags') != '') {
|
|
$tagFilter = $parsedFilter->getString('tags');
|
|
|
|
if (trim($tagFilter) === '--no-tag') {
|
|
$body .= ' AND `layout`.layoutID NOT IN (
|
|
SELECT `lktaglayout`.layoutId
|
|
FROM `tag`
|
|
INNER JOIN `lktaglayout`
|
|
ON `lktaglayout`.tagId = `tag`.tagId
|
|
)
|
|
';
|
|
} else {
|
|
$operator = $parsedFilter->getCheckbox('exactTags') == 1 ? '=' : 'LIKE';
|
|
$logicalOperator = $parsedFilter->getString('logicalOperator', ['default' => 'OR']);
|
|
$allTags = explode(',', $tagFilter);
|
|
$notTags = [];
|
|
$tags = [];
|
|
|
|
foreach ($allTags as $tag) {
|
|
if (str_starts_with($tag, '-')) {
|
|
$notTags[] = ltrim(($tag), '-');
|
|
} else {
|
|
$tags[] = $tag;
|
|
}
|
|
}
|
|
|
|
if (!empty($notTags)) {
|
|
$body .= ' AND layout.layoutID NOT IN (
|
|
SELECT lktaglayout.layoutId
|
|
FROM tag
|
|
INNER JOIN lktaglayout
|
|
ON lktaglayout.tagId = tag.tagId
|
|
';
|
|
|
|
$this->tagFilter(
|
|
$notTags,
|
|
'lktaglayout',
|
|
'lkTagLayoutId',
|
|
'layoutId',
|
|
$logicalOperator,
|
|
$operator,
|
|
true,
|
|
$body,
|
|
$params
|
|
);
|
|
}
|
|
|
|
if (!empty($tags)) {
|
|
$body .= ' AND layout.layoutID IN (
|
|
SELECT lktaglayout.layoutId
|
|
FROM tag
|
|
INNER JOIN lktaglayout
|
|
ON lktaglayout.tagId = tag.tagId
|
|
';
|
|
|
|
$this->tagFilter(
|
|
$tags,
|
|
'lktaglayout',
|
|
'lkTagLayoutId',
|
|
'layoutId',
|
|
$logicalOperator,
|
|
$operator,
|
|
false,
|
|
$body,
|
|
$params
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Show All, Used or UnUsed
|
|
// Used - In active schedule, scheduled in the future, directly assigned to displayGroup, default Layout.
|
|
// Unused - Every layout NOT matching the Used ie not in active schedule, not scheduled in the future, not directly assigned to any displayGroup, not default layout.
|
|
if ($parsedFilter->getInt('filterLayoutStatusId', ['default' => 1]) != 1) {
|
|
if ($parsedFilter->getInt('filterLayoutStatusId') == 2) {
|
|
|
|
// Only show used layouts
|
|
$now = Carbon::now()->format('U');
|
|
$sql = 'SELECT DISTINCT schedule.CampaignID FROM schedule WHERE ( ( schedule.fromDt < '. $now . ' OR schedule.fromDt = 0 ) ' . ' AND schedule.toDt > ' . $now . ') OR schedule.fromDt > ' . $now;
|
|
$campaignIds = [];
|
|
foreach ($this->getStore()->select($sql, []) as $row) {
|
|
$campaignIds[] = $row['CampaignID'];
|
|
}
|
|
$body .= ' AND ('
|
|
. ' campaign.CampaignID IN ( ' . implode(',', array_filter($campaignIds)) . ' )
|
|
OR layout.layoutID IN (SELECT DISTINCT defaultlayoutid FROM display)
|
|
OR layout.layoutID IN (SELECT DISTINCT layoutId FROM lklayoutdisplaygroup)'
|
|
. ' ) ';
|
|
}
|
|
else {
|
|
// Only show unused layouts
|
|
$now = Carbon::now()->format('U');
|
|
$sql = 'SELECT DISTINCT schedule.CampaignID FROM schedule WHERE ( ( schedule.fromDt < '. $now . ' OR schedule.fromDt = 0 ) ' . ' AND schedule.toDt > ' . $now . ') OR schedule.fromDt > ' . $now;
|
|
$campaignIds = [];
|
|
foreach ($this->getStore()->select($sql, []) as $row) {
|
|
$campaignIds[] = $row['CampaignID'];
|
|
}
|
|
|
|
$body .= ' AND campaign.CampaignID NOT IN ( ' . implode(',', array_filter($campaignIds)) . ' )
|
|
AND layout.layoutID NOT IN (SELECT DISTINCT defaultlayoutid FROM display)
|
|
AND layout.layoutID NOT IN (SELECT DISTINCT layoutId FROM lklayoutdisplaygroup)
|
|
';
|
|
}
|
|
}
|
|
|
|
// PlaylistID
|
|
if ($parsedFilter->getInt('playlistId', ['default' => 0]) != 0) {
|
|
$body .= ' AND layout.layoutId IN (SELECT DISTINCT `region`.layoutId
|
|
FROM `lkplaylistplaylist`
|
|
INNER JOIN `playlist`
|
|
ON `playlist`.playlistId = `lkplaylistplaylist`.parentId
|
|
INNER JOIN `region`
|
|
ON `region`.regionId = `playlist`.regionId
|
|
WHERE `lkplaylistplaylist`.childId = :playlistId )
|
|
';
|
|
|
|
$params['playlistId'] = $parsedFilter->getInt('playlistId', ['default' => 0]);
|
|
}
|
|
|
|
// publishedDate
|
|
if ($parsedFilter->getInt('havePublishDate', ['default' => -1]) != -1) {
|
|
$body .= " AND `layout`.publishedDate IS NOT NULL ";
|
|
}
|
|
|
|
if ($parsedFilter->getInt('activeDisplayGroupId') !== null) {
|
|
|
|
$date = Carbon::now()->format('U');
|
|
|
|
// for filter by displayGroup, we need to add some additional filters in WHERE clause to show only relevant Layouts at the time the Layout grid is viewed
|
|
$body .= ' AND campaign.campaignId = schedule.campaignId
|
|
AND ( schedule.fromDt < '. $date . ' OR schedule.fromDt = 0 ) ' . ' AND schedule.toDt > ' . $date;
|
|
}
|
|
|
|
if ($parsedFilter->getInt('folderId') !== null) {
|
|
$body .= " AND campaign.folderId = :folderId ";
|
|
$params['folderId'] = $parsedFilter->getInt('folderId');
|
|
}
|
|
|
|
if ($parsedFilter->getString('orientation') !== null) {
|
|
if ($parsedFilter->getString('orientation') === 'portrait') {
|
|
$body .= ' AND layout.width < layout.height ';
|
|
} else {
|
|
$body .= ' AND layout.width >= layout.height ';
|
|
}
|
|
}
|
|
|
|
// Get the fullscreen media or playlist layout
|
|
if ($parsedFilter->getInt('isFullScreenCampaign', ['default' => -1]) == 1) {
|
|
$body .= ' AND campaign.type IN ("media", "playlist") ';
|
|
} else if ($parsedFilter->getString('campaignType') != '') {
|
|
$body .= ' AND campaign.type = :type ';
|
|
$params['type'] = $parsedFilter->getString('campaignType');
|
|
}
|
|
|
|
// Logged in user view permissions
|
|
$this->viewPermissionSql('Xibo\Entity\Campaign', $body, $params, 'campaign.campaignId', 'layout.userId', $filterBy, 'campaign.permissionsFolderId');
|
|
|
|
// Sorting?
|
|
$order = '';
|
|
|
|
if (is_array($sortOrder)) {
|
|
$order .= ' ORDER BY ' . implode(',', $sortOrder);
|
|
}
|
|
|
|
$limit = '';
|
|
// Paging
|
|
if ($filterBy !== null && $parsedFilter->getInt('start') !== null && $parsedFilter->getInt('length') !== null) {
|
|
$limit = ' LIMIT ' . $parsedFilter->getInt('start', ['default' => 0]) . ', ' . $parsedFilter->getInt('length', ['default' => 10]);
|
|
}
|
|
|
|
// The final statements
|
|
$sql = $select . $body . $order . $limit;
|
|
$layoutIds = [];
|
|
|
|
foreach ($this->getStore()->select($sql, $params) as $row) {
|
|
$layout = $this->createEmpty();
|
|
|
|
$parsedRow = $this->getSanitizer($row);
|
|
|
|
// Validate each param and add it to the array.
|
|
$layout->layoutId = $parsedRow->getInt('layoutID');
|
|
$layout->parentId = $parsedRow->getInt('parentId');
|
|
$layout->schemaVersion = $parsedRow->getInt('schemaVersion');
|
|
$layout->layout = $parsedRow->getString('layout');
|
|
$layout->description = $parsedRow->getString('description');
|
|
$layout->duration = $parsedRow->getInt('duration');
|
|
$layout->backgroundColor = $parsedRow->getString('backgroundColor');
|
|
$layout->owner = $parsedRow->getString('owner');
|
|
$layout->ownerId = $parsedRow->getInt('userID');
|
|
$layout->campaignId = $parsedRow->getInt('CampaignID');
|
|
$layout->retired = $parsedRow->getInt('retired');
|
|
$layout->status = $parsedRow->getInt('status');
|
|
$layout->backgroundImageId = $parsedRow->getInt('backgroundImageId');
|
|
$layout->backgroundzIndex = $parsedRow->getInt('backgroundzIndex');
|
|
$layout->width = $parsedRow->getDouble('width');
|
|
$layout->height = $parsedRow->getDouble('height');
|
|
$layout->orientation = $layout->width >= $layout->height ? 'landscape' : 'portrait';
|
|
$layout->createdDt = $parsedRow->getString('createdDt');
|
|
$layout->modifiedDt = $parsedRow->getString('modifiedDt');
|
|
$layout->displayOrder = $parsedRow->getInt('displayOrder');
|
|
$layout->statusMessage = $parsedRow->getString('statusMessage');
|
|
$layout->enableStat = $parsedRow->getInt('enableStat');
|
|
$layout->publishedStatusId = $parsedRow->getInt('publishedStatusId');
|
|
$layout->publishedStatus = $parsedRow->getString('publishedStatus');
|
|
$layout->publishedDate = $parsedRow->getString('publishedDate');
|
|
$layout->autoApplyTransitions = $parsedRow->getInt('autoApplyTransitions');
|
|
$layout->code = $parsedRow->getString('code');
|
|
$layout->folderId = $parsedRow->getInt('folderId');
|
|
$layout->permissionsFolderId = $parsedRow->getInt('permissionsFolderId');
|
|
|
|
$layout->groupsWithPermissions = $row['groupsWithPermissions'];
|
|
$layout->setOriginals();
|
|
|
|
$entries[] = $layout;
|
|
$layoutIds[] = $layout->layoutId;
|
|
}
|
|
|
|
// decorate with TagLinks
|
|
if (count($entries) > 0) {
|
|
$this->decorateWithTagLinks('lktaglayout', 'layoutId', $layoutIds, $entries);
|
|
}
|
|
|
|
// Paging
|
|
if ($limit != '' && count($entries) > 0) {
|
|
unset($params['permissionEntityForGroup']);
|
|
$results = $this->getStore()->select('SELECT COUNT(*) AS total ' . $body, $params);
|
|
$this->_countLast = intval($results[0]['total']);
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* @param \Xibo\Entity\Widget $widget
|
|
* @return \Xibo\Entity\Widget
|
|
*/
|
|
private function setWidgetExpiryDatesOrDefault($widget)
|
|
{
|
|
$minSubYear = Carbon::createFromTimestamp(Widget::$DATE_MIN)->subYear()->format('U');
|
|
$minAddYear = Carbon::createFromTimestamp(Widget::$DATE_MIN)->addYear()->format('U');
|
|
$maxSubYear = Carbon::createFromTimestamp(Widget::$DATE_MAX)->subYear()->format('U');
|
|
$maxAddYear = Carbon::createFromTimestamp(Widget::$DATE_MAX)->addYear()->format('U');
|
|
|
|
// if we are importing from layout.json the Widget from/to expiry dates are already timestamps
|
|
// for old Layouts when the Widget from/to dt are missing we set them to timestamps as well.
|
|
$timestampFromDt = is_integer($widget->fromDt) ? $widget->fromDt : Carbon::createFromTimeString($widget->fromDt)->format('U');
|
|
$timestampToDt = is_integer($widget->toDt) ? $widget->toDt : Carbon::createFromTimeString($widget->toDt)->format('U');
|
|
|
|
// convert the date string to a unix timestamp, if the layout xlf does not contain dates, then set it to the $DATE_MIN / $DATE_MAX which are already unix timestamps, don't attempt to convert them
|
|
// we need to check if provided from and to dates are within $DATE_MIN +- year to avoid issues with CMS Instances in different timezones https://github.com/xibosignage/xibo/issues/1934
|
|
if ($widget->fromDt === Widget::$DATE_MIN || ($timestampFromDt > $minSubYear && $timestampFromDt < $minAddYear)) {
|
|
$widget->fromDt = Widget::$DATE_MIN;
|
|
} else {
|
|
$widget->fromDt = $timestampFromDt;
|
|
}
|
|
|
|
if ($widget->toDt === Widget::$DATE_MAX || ($timestampToDt > $maxSubYear && $timestampToDt < $maxAddYear)) {
|
|
$widget->toDt = Widget::$DATE_MAX;
|
|
} else {
|
|
$widget->toDt = $timestampToDt;
|
|
}
|
|
|
|
return $widget;
|
|
}
|
|
|
|
/**
|
|
* @param \Xibo\Entity\Playlist $newPlaylist
|
|
* @param Folder $folder
|
|
* @return \Xibo\Entity\Playlist
|
|
* @throws DuplicateEntityException
|
|
* @throws InvalidArgumentException
|
|
* @throws NotFoundException
|
|
*/
|
|
private function setOwnerAndSavePlaylist($newPlaylist, Folder $folder)
|
|
{
|
|
// try to save with the name from import, if it already exists add "imported - " to the name
|
|
try {
|
|
// The new Playlist should be owned by the importing user
|
|
$newPlaylist->ownerId = $this->getUser()->getId();
|
|
$newPlaylist->playlistId = null;
|
|
$newPlaylist->widgets = [];
|
|
$newPlaylist->folderId = $folder->id;
|
|
$newPlaylist->permissionsFolderId =
|
|
($folder->permissionsFolderId == null) ? $folder->id : $folder->permissionsFolderId;
|
|
$newPlaylist->save();
|
|
} catch (DuplicateEntityException $e) {
|
|
$newPlaylist->name = 'imported - ' . $newPlaylist->name;
|
|
$newPlaylist->save();
|
|
}
|
|
|
|
return $newPlaylist;
|
|
}
|
|
|
|
/**
|
|
* Checkout a Layout
|
|
* @param \Xibo\Entity\Layout $layout
|
|
* @param bool $returnDraft Should we return the Draft or the pre-checkout Layout
|
|
* @return \Xibo\Entity\Layout
|
|
* @throws \Xibo\Support\Exception\GeneralException
|
|
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
|
* @throws \Xibo\Support\Exception\NotFoundException
|
|
*/
|
|
public function checkoutLayout($layout, $returnDraft = true)
|
|
{
|
|
// Load the Layout
|
|
$layout->load();
|
|
|
|
// Make a skeleton copy of the Layout
|
|
$draft = clone $layout;
|
|
$draft->parentId = $layout->layoutId;
|
|
$draft->campaignId = $layout->campaignId;
|
|
$draft->publishedStatusId = 2; // Draft
|
|
$draft->publishedStatus = __('Draft');
|
|
$draft->autoApplyTransitions = $layout->autoApplyTransitions;
|
|
$draft->code = $layout->code;
|
|
$draft->folderId = $layout->folderId;
|
|
|
|
// Save without validation or notification.
|
|
$draft->save([
|
|
'validate' => false,
|
|
'notify' => false
|
|
]);
|
|
|
|
// Update the original
|
|
$layout->publishedStatusId = 2; // Draft
|
|
$layout->publishedStatus = __('Draft');
|
|
$layout->save([
|
|
'saveLayout' => true,
|
|
'saveRegions' => false,
|
|
'saveTags' => false,
|
|
'setBuildRequired' => false,
|
|
'validate' => false,
|
|
'notify' => false
|
|
]);
|
|
|
|
/** @var Region[] $allRegions */
|
|
$allRegions = array_merge($draft->regions, $draft->drawers);
|
|
|
|
// Skip the action validation on checkout
|
|
$draft->copyActions($draft, $layout, false);
|
|
|
|
// Permissions && Sub-Playlists
|
|
// Layout level permissions are managed on the Campaign entity, so we do not need to worry about that
|
|
// Regions/Widgets need to copy down our layout permissions
|
|
foreach ($allRegions as $region) {
|
|
// Match our original region id to the id in the parent layout
|
|
$original = $layout->getRegionOrDrawer($region->getOriginalValue('regionId'));
|
|
|
|
// Make sure Playlist closure table from the published one are copied over
|
|
$original->getPlaylist()->cloneClosureTable($region->getPlaylist()->playlistId);
|
|
|
|
// Copy over original permissions
|
|
foreach ($original->permissions as $permission) {
|
|
$new = clone $permission;
|
|
$new->objectId = $region->regionId;
|
|
$new->save();
|
|
}
|
|
|
|
// Playlist
|
|
foreach ($original->getPlaylist()->permissions as $permission) {
|
|
$new = clone $permission;
|
|
$new->objectId = $region->getPlaylist()->playlistId;
|
|
$new->save();
|
|
}
|
|
|
|
// Widgets
|
|
foreach ($region->getPlaylist()->widgets as $widget) {
|
|
$originalWidget = $original->getPlaylist()->getWidget($widget->getOriginalValue('widgetId'));
|
|
// Copy over original permissions
|
|
foreach ($originalWidget->permissions as $permission) {
|
|
$new = clone $permission;
|
|
$new->objectId = $widget->widgetId;
|
|
$new->save();
|
|
}
|
|
|
|
// Copy widget data
|
|
$this->widgetDataFactory->copyByWidgetId($originalWidget->widgetId, $widget->widgetId);
|
|
}
|
|
}
|
|
|
|
return $returnDraft ? $draft : $layout;
|
|
}
|
|
|
|
/**
|
|
* Function called during Layout Import
|
|
* Check if provided Widget has options to have Library references
|
|
* if it does, then go through them find and replace old media references
|
|
*
|
|
* @param Widget $widget
|
|
* @param int $newMediaId
|
|
* @param int $oldMediaId
|
|
* @throws NotFoundException
|
|
*/
|
|
public function handleWidgetMediaIdReferences(Widget $widget, int $newMediaId, int $oldMediaId)
|
|
{
|
|
$module = $this->moduleFactory->getByType($widget->type);
|
|
|
|
foreach ($module->getPropertiesAllowingLibraryRefs() as $property) {
|
|
$widget->setOptionValue(
|
|
$property->id,
|
|
'cdata',
|
|
str_replace(
|
|
'[' . $oldMediaId . ']',
|
|
'[' . $newMediaId . ']',
|
|
$widget->getOptionValue($property->id, null)
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param int $layoutId
|
|
* @param array $actionLayoutIds
|
|
* @param array $processedLayoutIds
|
|
* @return array
|
|
*/
|
|
public function getActionPublishedLayoutIds(int $layoutId, array &$actionLayoutIds, array &$processedLayoutIds): array
|
|
{
|
|
// if Layout was already processed, do not attempt to do it again
|
|
// we should have all actionLayoutsIds from it at this point, there is no need to process it again
|
|
if (!in_array($layoutId, $processedLayoutIds)) {
|
|
// Get Layout Codes set in Actions on this Layout
|
|
// Actions directly on this Layout
|
|
$sql = '
|
|
SELECT DISTINCT `action`.layoutCode
|
|
FROM `action`
|
|
INNER JOIN `layout`
|
|
ON `layout`.layoutId = `action`.sourceId
|
|
WHERE `action`.actionType = :actionType
|
|
AND `layout`.layoutId = :layoutId
|
|
AND `layout`.parentId IS NULL
|
|
';
|
|
|
|
// Actions on this Layout's Regions
|
|
$sql .= '
|
|
UNION
|
|
SELECT DISTINCT `action`.layoutCode
|
|
FROM `action`
|
|
INNER JOIN `region`
|
|
ON `region`.regionId = `action`.sourceId
|
|
INNER JOIN `layout`
|
|
ON `layout`.layoutId = `region`.layoutId
|
|
WHERE `action`.actionType = :actionType
|
|
AND `layout`.layoutId = :layoutId
|
|
AND `layout`.parentId IS NULL
|
|
';
|
|
|
|
// Actions on this Layout's Widgets
|
|
$sql .= '
|
|
UNION
|
|
SELECT DISTINCT `action`.layoutCode
|
|
FROM `action`
|
|
INNER JOIN `widget`
|
|
ON `widget`.widgetId = `action`.sourceId
|
|
INNER JOIN `playlist`
|
|
ON `playlist`.playlistId = `widget`.playlistId
|
|
INNER JOIN `region`
|
|
ON `region`.regionId = `playlist`.regionId
|
|
INNER JOIN `layout`
|
|
ON `layout`.layoutId = `region`.layoutId
|
|
WHERE `action`.actionType = :actionType
|
|
AND `layout`.layoutId = :layoutId
|
|
AND `layout`.parentId IS NULL
|
|
';
|
|
|
|
// Join them together and get the Layout's referenced by those codes
|
|
$actionLayoutCodes = $this->getStore()->select('
|
|
SELECT `layout`.layoutId
|
|
FROM `layout`
|
|
WHERE `layout`.code IN (
|
|
' . $sql . '
|
|
)
|
|
', [
|
|
'actionType' => 'navLayout',
|
|
'layoutId' => $layoutId,
|
|
]);
|
|
|
|
$processedLayoutIds[] = $layoutId;
|
|
|
|
foreach ($actionLayoutCodes as $row) {
|
|
// if we have not processed this Layout yet, do it now
|
|
if (!in_array($row['layoutId'], $actionLayoutIds)) {
|
|
$actionLayoutIds[] = $row['layoutId'];
|
|
// check if this layout is linked with any further navLayout actions
|
|
$this->getActionPublishedLayoutIds($row['layoutId'], $actionLayoutIds, $processedLayoutIds);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $actionLayoutIds;
|
|
}
|
|
|
|
// <editor-fold desc="Concurrency Locking">
|
|
|
|
/**
|
|
* @param \Stash\Interfaces\PoolInterface|null $pool
|
|
* @return $this
|
|
*/
|
|
public function usePool($pool)
|
|
{
|
|
$this->pool = $pool;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* @return \Stash\Interfaces\PoolInterface|\Stash\Pool
|
|
*/
|
|
private function getPool()
|
|
{
|
|
if ($this->pool === null) {
|
|
$this->pool = new Pool();
|
|
}
|
|
return $this->pool;
|
|
}
|
|
|
|
/**
|
|
* @param \Xibo\Entity\Layout $layout
|
|
* @return \Xibo\Entity\Layout
|
|
*/
|
|
public function decorateLockedProperties(Layout $layout): Layout
|
|
{
|
|
$locked = $this->pool->getItem('locks/layout/' . $layout->layoutId);
|
|
$layout->isLocked = $locked->isMiss() ? [] : $locked->get();
|
|
if (!empty($layout->isLocked)) {
|
|
$layout->isLocked->lockedUser = ($layout->isLocked->userId != $this->getUser()->userId);
|
|
}
|
|
|
|
return $layout;
|
|
}
|
|
|
|
/**
|
|
* Hold a lock on concurrent requests
|
|
* blocks if the request is locked
|
|
* @param int $ttl seconds
|
|
* @param int $wait seconds
|
|
* @param int $tries
|
|
* @throws \Xibo\Support\Exception\GeneralException
|
|
*/
|
|
public function concurrentRequestLock(Layout $layout, $force = false, $pass = 1, $ttl = 300, $wait = 6, $tries = 10): Layout
|
|
{
|
|
// Does this layout require building?
|
|
if (!$force && !$layout->isBuildRequired()) {
|
|
return $layout;
|
|
}
|
|
|
|
$lock = $this->getPool()->getItem('locks/layout_build/' . $layout->campaignId);
|
|
|
|
// Set the invalidation method to simply return the value (not that we use it, but it gets us a miss on expiry)
|
|
// isMiss() returns false if the item is missing or expired, no exceptions.
|
|
$lock->setInvalidationMethod(Invalidation::NONE);
|
|
|
|
// Get the lock
|
|
// other requests will wait here until we're done, or we've timed out
|
|
$locked = $lock->get();
|
|
|
|
// Did we get a lock?
|
|
// if we're a miss, then we're not already locked
|
|
if ($lock->isMiss() || $locked === false) {
|
|
$this->getLog()->debug('Lock miss or false. Locking for ' . $ttl . ' seconds. $locked is '. var_export($locked, true));
|
|
|
|
// so lock now
|
|
$lock->set(true);
|
|
$lock->expiresAfter($ttl);
|
|
$lock->save();
|
|
|
|
// If we have been locked previously, then reload our layout before passing back out.
|
|
if ($pass > 1) {
|
|
$layout = $this->getById($layout->layoutId);
|
|
}
|
|
|
|
return $layout;
|
|
} else {
|
|
// We are a hit - we must be locked
|
|
$this->getLog()->debug('LOCK hit for ' . $layout->campaignId . ' expires '
|
|
. $lock->getExpiration()->format('Y-m-d H:i:s') . ', created '
|
|
. $lock->getCreation()->format('Y-m-d H:i:s'));
|
|
|
|
// Try again?
|
|
$tries--;
|
|
|
|
if ($tries <= 0) {
|
|
// We've waited long enough
|
|
throw new GeneralException('Concurrent record locked, time out.');
|
|
} else {
|
|
$this->getLog()->debug('Unable to get a lock, trying again. Remaining retries: ' . $tries);
|
|
|
|
// Hang about waiting for the lock to be released.
|
|
sleep($wait);
|
|
|
|
// Recursive request (we've decremented the number of tries)
|
|
$pass++;
|
|
return $this->concurrentRequestLock($layout, $force, $pass, $ttl, $wait, $tries);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Release a lock on concurrent requests
|
|
*/
|
|
public function concurrentRequestRelease(Layout $layout, bool $force = false)
|
|
{
|
|
if (!$force && !$layout->hasBuilt()) {
|
|
return;
|
|
}
|
|
|
|
$this->getLog()->debug('Releasing lock ' . $layout->campaignId);
|
|
|
|
$lock = $this->getPool()->getItem('locks/layout_build/' . $layout->campaignId);
|
|
|
|
// Release lock
|
|
$lock->set(false);
|
|
$lock->expiresAfter(10); // Expire straight away (but give it time to save the thing)
|
|
|
|
$this->getPool()->save($lock);
|
|
}
|
|
|
|
public function convertOldPlaylistOptions($playlistIds, $playlistOptions)
|
|
{
|
|
$convertedPlaylistOption = [];
|
|
$i = 0;
|
|
foreach ($playlistIds as $playlistId) {
|
|
$i++;
|
|
$convertedPlaylistOption[] = [
|
|
'rowNo' => $i,
|
|
'playlistId' => $playlistId,
|
|
'spotFill' => $playlistOptions[$playlistId]['subPlaylistIdSpotFill'] ?? null,
|
|
'spotLength' => $playlistOptions[$playlistId]['subPlaylistIdSpotLength'] ?? null,
|
|
'spots' => $playlistOptions[$playlistId]['subPlaylistIdSpots'] ?? null,
|
|
];
|
|
}
|
|
|
|
return $convertedPlaylistOption;
|
|
}
|
|
|
|
/**
|
|
* Prepare widget options, check legacy types from conditions, set widget type and upgrade
|
|
* @throws NotFoundException
|
|
*/
|
|
private function prepareWidgetAndGetModule(Widget $widget): Module
|
|
{
|
|
// Form conditions from the widget's option and value, e.g, templateId==worldclock1
|
|
$widgetConditionMatch = [];
|
|
foreach ($widget->widgetOptions as $option) {
|
|
$widgetConditionMatch[] = $option->option . '==' . $option->value;
|
|
}
|
|
|
|
// Get module
|
|
try {
|
|
$module = $this->moduleFactory->getByType($widget->type, $widgetConditionMatch);
|
|
} catch (NotFoundException $notFoundException) {
|
|
throw new NotFoundException(__('Module not found'));
|
|
}
|
|
|
|
// Set the widget type and then assert the new one
|
|
$widget->setOriginalValue('type', $widget->type);
|
|
$widget->type = $module->type;
|
|
|
|
// Upgrade if necessary
|
|
// We do not upgrade widgets which are already at the right schema version
|
|
if ($widget->schemaVersion < $module->schemaVersion && $module->isWidgetCompatibilityAvailable()) {
|
|
// Grab a widget compatibility interface, if there is one
|
|
$widgetCompatibilityInterface = $module->getWidgetCompatibilityOrNull();
|
|
if ($widgetCompatibilityInterface !== null) {
|
|
try {
|
|
// We will leave the widget save for later
|
|
$upgraded = $widgetCompatibilityInterface->upgradeWidget(
|
|
$widget,
|
|
$widget->schemaVersion,
|
|
$module->schemaVersion
|
|
);
|
|
|
|
if ($upgraded) {
|
|
$widget->schemaVersion = $module->schemaVersion;
|
|
}
|
|
} catch (\Exception $e) {
|
|
$this->getLog()->error('Error upgrading widget '. $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
return $module;
|
|
}
|
|
|
|
/**
|
|
* Creates fullscreen layout from media or playlist
|
|
* @params $id
|
|
* @params $resolutionId
|
|
* @params $backgroundColor
|
|
* @params $duration
|
|
* @params $type
|
|
* @throws InvalidArgumentException
|
|
* @throws NotFoundException
|
|
* @throws GeneralException
|
|
*/
|
|
public function createFullScreenLayout($type, $id, $resolutionId, $backgroundColor, $duration): Layout
|
|
{
|
|
$media = null;
|
|
$playlist = null;
|
|
$playlistItems = [];
|
|
|
|
if ($type === 'media') {
|
|
$media = $this->mediaFactory->getById($id);
|
|
} else if ($type === 'playlist') {
|
|
$playlist = $this->playlistFactory->getById($id);
|
|
$playlist->load();
|
|
}
|
|
|
|
if (empty($resolutionId)) {
|
|
if ($type === 'media') {
|
|
$resolutionId = $this->resolutionFactory->getClosestMatchingResolution(
|
|
$media->width,
|
|
$media->height
|
|
)->resolutionId;
|
|
} else if ($type === 'playlist') {
|
|
$resolutionId = $this->resolutionFactory->getClosestMatchingResolution(
|
|
1920,
|
|
1080
|
|
)->resolutionId;
|
|
}
|
|
}
|
|
|
|
$module = $this->moduleFactory->getByType($type === 'media' ? $media->mediaType : 'subplaylist');
|
|
|
|
// Determine the duration
|
|
// if we have a duration provided, then use it, otherwise use the duration recorded on the
|
|
// library item/playlist already
|
|
$itemDuration = $duration;
|
|
|
|
if (empty($itemDuration)) {
|
|
$itemDuration = ($type === 'media' ? $media->duration : $playlist->duration);
|
|
}
|
|
|
|
// If the library item duration (or provided duration) is 0, then default to the Module Default
|
|
// Duration as configured in settings.
|
|
$itemDuration = ($itemDuration == 0) ? $module->defaultDuration : $itemDuration;
|
|
|
|
// Do we have an existing layout with the same properties as the current one?
|
|
$currentLayoutDimension = $this->resolutionFactory->getById($resolutionId);
|
|
|
|
if (empty($backgroundColor)) {
|
|
$backgroundColor = '#000000';
|
|
}
|
|
|
|
$currentLayoutProperties = [
|
|
'backgroundColor' => $backgroundColor,
|
|
'height' => $currentLayoutDimension->height,
|
|
'width' => $currentLayoutDimension->width
|
|
];
|
|
|
|
if ($type === 'media') {
|
|
// do we already have a full screen layout with this media?
|
|
$existingFullscreenLayout = $this->getLinkedFullScreenLayout(
|
|
'media',
|
|
$media->mediaId,
|
|
array_merge($currentLayoutProperties, ['duration' => $itemDuration])
|
|
);
|
|
} else if ($type === 'playlist') {
|
|
// do we already have a full screen layout with this playlist?
|
|
$existingFullscreenLayout = $this->getLinkedFullScreenLayout(
|
|
'playlist',
|
|
$playlist->playlistId,
|
|
$currentLayoutProperties
|
|
);
|
|
}
|
|
|
|
if (!empty($existingFullscreenLayout)) {
|
|
// Return
|
|
return $existingFullscreenLayout;
|
|
}
|
|
|
|
$layout = $this->createFromResolution(
|
|
$resolutionId,
|
|
$this->getUser()->userId,
|
|
$type . '_' .
|
|
($type === 'media' ? $media->name : $playlist->name) .
|
|
'_' . ($type === 'media' ? $media->mediaId : $playlist->playlistId),
|
|
'Full Screen Layout created from ' . ($type === 'media' ? $media->name : $playlist->name),
|
|
'',
|
|
null,
|
|
false
|
|
);
|
|
|
|
if (!empty($backgroundColor)) {
|
|
$layout->backgroundColor = $backgroundColor;
|
|
}
|
|
|
|
$this->addRegion(
|
|
$layout,
|
|
$type === 'media' ? 'frame' : 'playlist',
|
|
$layout->width,
|
|
$layout->height,
|
|
0,
|
|
0
|
|
);
|
|
|
|
$layout->setUnmatchedProperty('type', $type);
|
|
$layout->autoApplyTransitions = 0;
|
|
$layout->schemaVersion = Environment::$XLF_VERSION;
|
|
$layout->folderId = ($type === 'media') ? $media->folderId : $playlist->folderId;
|
|
|
|
// Media files have their own validation so we can skip
|
|
$layout->save(['validate' => false]);
|
|
|
|
$draft = $this->checkoutLayout($layout);
|
|
|
|
$region = $draft->regions[0];
|
|
|
|
// Create a widget
|
|
$widget = $this->widgetFactory->create(
|
|
$this->getUser()->userId,
|
|
$region->getPlaylist()->playlistId,
|
|
$type === 'media' ? $media->mediaType : 'subplaylist',
|
|
$itemDuration,
|
|
$module->schemaVersion
|
|
);
|
|
|
|
if ($type === 'playlist') {
|
|
// save here, simulate add Widget
|
|
// next save (with playlist) will edit and save the Widget and dispatch event that manages closure table.
|
|
$widget->save();
|
|
$item = new SubPlaylistItem();
|
|
$item->rowNo = 1;
|
|
$item->playlistId = $playlist->playlistId;
|
|
$item->spotFill = 'repeat';
|
|
$item->spotLength = '';
|
|
$item->spots = '';
|
|
|
|
$playlistItems[] = $item;
|
|
$widget->setOptionValue('subPlaylists', 'attrib', json_encode($playlistItems));
|
|
} else {
|
|
$widget->useDuration = 1;
|
|
$widget->assignMedia($media->mediaId);
|
|
}
|
|
|
|
// Calculate the duration
|
|
$widget->calculateDuration($module);
|
|
|
|
// Set loop for media items with custom duration
|
|
if ($type === 'media' && $media->mediaType === 'video' && $itemDuration > $media->duration) {
|
|
$widget->setOptionValue('loop', 'attrib', 1);
|
|
$widget->save();
|
|
}
|
|
|
|
// Assign the widget to the playlist
|
|
$region->getPlaylist()->assignWidget($widget);
|
|
// Save the playlist
|
|
$region->getPlaylist()->save();
|
|
$region->save();
|
|
|
|
// look up the record in the database
|
|
// as we do not set modifiedDt on the object on save.
|
|
$draft = $this->getByParentId($layout->layoutId);
|
|
$draft->publishDraft();
|
|
$draft->load();
|
|
|
|
// We also build the XLF at this point, and if we have a problem we prevent publishing and raise as an
|
|
// error message
|
|
$draft->xlfToDisk(['notify' => true, 'exceptionOnError' => true, 'exceptionOnEmptyRegion' => false]);
|
|
|
|
// Return
|
|
return $draft;
|
|
}
|
|
|
|
/**
|
|
* Get the layout resolutionId
|
|
* @params $layout
|
|
* @throws NotFoundException
|
|
* @throws GeneralException
|
|
*/
|
|
public function getLayoutResolutionId($layout)
|
|
{
|
|
return $this->resolutionFactory->getClosestMatchingResolution($layout->width, $layout->height);
|
|
}
|
|
|
|
// </editor-fold>
|
|
}
|