File: //home/posscale/subdomains/xibo/lib/Entity/Layout.php
<?php
/*
* Xibo - Digital Signage - http://www.xibo.org.uk
* Copyright (C) 2015 Spring Signage Ltd
*
* 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\Entity;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Event\LayoutBuildEvent;
use Xibo\Event\LayoutBuildRegionEvent;
use Xibo\Exception\DuplicateEntityException;
use Xibo\Exception\InvalidArgumentException;
use Xibo\Exception\NotFoundException;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\DataSetFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Factory\RegionFactory;
use Xibo\Factory\TagFactory;
use Xibo\Service\ConfigServiceInterface;
use Xibo\Service\DateServiceInterface;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
/**
* Class Layout
* @package Xibo\Entity
*
* @SWG\Definition()
*/
class Layout implements \JsonSerializable
{
use EntityTrait;
/**
* @SWG\Property(
* description="The layoutId"
* )
* @var int
*/
public $layoutId;
/**
* @var int
* @SWG\Property(
* description="The userId of the Layout Owner"
* )
*/
public $ownerId;
/**
* @var int
* @SWG\Property(
* description="The id of the Layout's dedicated Campaign"
* )
*/
public $campaignId;
/**
* @var int
* @SWG\Property(
* description="The id of the image media set as the background"
* )
*/
public $backgroundImageId;
/**
* @var int
* @SWG\Property(
* description="The XLF schema version"
* )
*/
public $schemaVersion;
/**
* @var string
* @SWG\Property(
* description="The name of the Layout"
* )
*/
public $layout;
/**
* @var string
* @SWG\Property(
* description="The description of the Layout"
* )
*/
public $description;
/**
* @var string
* @SWG\Property(
* description="A HEX string representing the Layout background color"
* )
*/
public $backgroundColor;
/**
* @var string
* @SWG\Property(
* description="The datetime the Layout was created"
* )
*/
public $createdDt;
/**
* @var string
* @SWG\Property(
* description="The datetime the Layout was last modified"
* )
*/
public $modifiedDt;
/**
* @var int
* @SWG\Property(
* description="Flag indicating the Layout status"
* )
*/
public $status;
/**
* @var int
* @SWG\Property(
* description="Flag indicating whether the Layout is retired"
* )
*/
public $retired;
/**
* @var int
* @SWG\Property(
* description="The Layer that the background should occupy"
* )
*/
public $backgroundzIndex;
/**
* @var double
* @SWG\Property(
* description="The Layout Width"
* )
*/
public $width;
/**
* @var double
* @SWG\Property(
* description="The Layout Height"
* )
*/
public $height;
/**
* @var int
* @SWG\Property(
* description="If this Layout has been requested by Campaign, then this is the display order of the Layout within the Campaign"
* )
*/
public $displayOrder;
/**
* @var int
* @SWG\Property(
* description="A read-only estimate of this Layout's total duration in seconds. This is equal to the longest region duration and is valid when the layout status is 1 or 2."
* )
*/
public $duration;
/**
* @var string
* @SWG\Property(description="A status message detailing any errors with the layout")
*/
public $statusMessage;
// Child items
/** @var Region[] */
public $regions = [];
public $tags = [];
public $permissions = [];
public $campaigns = [];
// Read only properties
public $owner;
public $groupsWithPermissions;
// Private
private $unassignTags = [];
public static $loadOptionsMinimum = [
'loadPlaylists' => false,
'loadTags' => false,
'loadPermissions' => false,
'loadCampaigns' => false
];
public static $saveOptionsMinimum = [
'saveLayout' => true,
'saveRegions' => false,
'saveTags' => false,
'setBuildRequired' => true,
'validate' => false,
'audit' => false,
'notify' => false
];
/**
* @var ConfigServiceInterface
*/
private $config;
/**
* @var DateServiceInterface
*/
private $date;
/** @var EventDispatcherInterface */
private $dispatcher;
/**
* @var PermissionFactory
*/
private $permissionFactory;
/**
* @var RegionFactory
*/
private $regionFactory;
/**
* @var TagFactory
*/
private $tagFactory;
/**
* @var CampaignFactory
*/
private $campaignFactory;
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* @var ModuleFactory
*/
private $moduleFactory;
/**
* Entity constructor.
* @param StorageServiceInterface $store
* @param LogServiceInterface $log
* @param ConfigServiceInterface $config
* @param DateServiceInterface $date
* @param EventDispatcherInterface $eventDispatcher
* @param PermissionFactory $permissionFactory
* @param RegionFactory $regionFactory
* @param TagFactory $tagFactory
* @param CampaignFactory $campaignFactory
* @param LayoutFactory $layoutFactory
* @param MediaFactory $mediaFactory
* @param ModuleFactory $moduleFactory
*/
public function __construct($store, $log, $config, $date, $eventDispatcher, $permissionFactory, $regionFactory, $tagFactory, $campaignFactory, $layoutFactory, $mediaFactory, $moduleFactory)
{
$this->setCommonDependencies($store, $log);
$this->setPermissionsClass('Xibo\Entity\Campaign');
$this->config = $config;
$this->date = $date;
$this->dispatcher = $eventDispatcher;
$this->permissionFactory = $permissionFactory;
$this->regionFactory = $regionFactory;
$this->tagFactory = $tagFactory;
$this->campaignFactory = $campaignFactory;
$this->layoutFactory = $layoutFactory;
$this->mediaFactory = $mediaFactory;
$this->moduleFactory = $moduleFactory;
}
public function __clone()
{
// Clear the layout id
$this->layoutId = null;
$this->campaignId = null;
$this->hash = null;
$this->permissions = [];
// Clone the regions
$this->regions = array_map(function ($object) { return clone $object; }, $this->regions);
}
/**
* @return string
*/
public function __toString()
{
return sprintf('Layout %s - %d x %d. Regions = %d, Tags = %d. layoutId = %d. Status = %d, messages %d', $this->layout, $this->width, $this->height, count($this->regions), count($this->tags), $this->layoutId, $this->status, count($this->getStatusMessage()));
}
/**
* @return string
*/
private function hash()
{
return md5($this->layoutId . $this->ownerId . $this->campaignId . $this->backgroundImageId . $this->backgroundColor . $this->width . $this->height . $this->status . $this->description . json_encode($this->statusMessage));
}
/**
* Get the Id
* @return int
*/
public function getId()
{
return $this->campaignId;
}
/**
* Get the OwnerId
* @return int
*/
public function getOwnerId()
{
return $this->ownerId;
}
/**
* Sets the Owner of the Layout (including children)
* @param int $ownerId
* @param bool $cascade Cascade ownership change down to Playlist records
*/
public function setOwner($ownerId, $cascade = false)
{
$this->ownerId = $ownerId;
$this->load();
foreach ($this->regions as $region) {
/* @var Region $region */
$region->setOwner($ownerId, $cascade);
}
}
/**
* Set the status of this layout to indicate a build is required
*/
private function setBuildRequired()
{
$this->status = 3;
}
/**
* Load Regions from a Layout
* @param int $regionId
* @return Region
* @throws NotFoundException
*/
public function getRegion($regionId)
{
foreach ($this->regions as $region) {
/* @var Region $region */
if ($region->regionId == $regionId)
return $region;
}
throw new NotFoundException(__('Cannot find region'));
}
/**
* Get Widgets assigned to this Layout
* @return Widget[]
*/
public function getWidgets()
{
$widgets = [];
foreach ($this->regions as $region) {
/* @var Region $region */
foreach ($region->playlists as $playlist) {
/* @var Playlist $playlist */
$widgets = array_merge($playlist->widgets, $widgets);
}
}
return $widgets;
}
/**
* @return array
*/
public function getStatusMessage()
{
if (empty($this->statusMessage))
return [];
if (is_array($this->statusMessage))
return $this->statusMessage;
$this->statusMessage = json_decode($this->statusMessage, true);
return $this->statusMessage;
}
/**
* Push a new message
* @param $message
*/
public function pushStatusMessage($message)
{
$this->getStatusMessage();
$this->statusMessage[] = $message;
}
/**
* Clear status message
*/
private function clearStatusMessage()
{
$this->statusMessage = null;
}
/**
* Load this Layout
* @param array $options
*/
public function load($options = [])
{
$options = array_merge([
'loadPlaylists' => true,
'loadTags' => true,
'loadPermissions' => true,
'loadCampaigns' => true
], $options);
if ($this->loaded || $this->layoutId == 0)
return;
$this->getLog()->debug('Loading Layout %d with options %s', $this->layoutId, json_encode($options));
// Load permissions
if ($options['loadPermissions'])
$this->permissions = $this->permissionFactory->getByObjectId('Xibo\\Entity\\Campaign', $this->campaignId);
// Load all regions
$this->regions = $this->regionFactory->getByLayoutId($this->layoutId);
if ($options['loadPlaylists'])
$this->loadPlaylists($options);
// Load all tags
if ($options['loadTags'])
$this->tags = $this->tagFactory->loadByLayoutId($this->layoutId);
// Load Campaigns
if ($options['loadCampaigns'])
$this->campaigns = $this->campaignFactory->getByLayoutId($this->layoutId);
// Set the hash
$this->hash = $this->hash();
$this->loaded = true;
$this->getLog()->debug('Loaded %s', $this->layoutId);
}
/**
* Load Playlists
* @param array $options
*/
public function loadPlaylists($options = [])
{
foreach ($this->regions as $region) {
/* @var Region $region */
$region->load($options);
}
}
/**
* Save this Layout
* @param array $options
*/
public function save($options = [])
{
// Default options
$options = array_merge([
'saveLayout' => true,
'saveRegions' => true,
'saveTags' => true,
'setBuildRequired' => true,
'validate' => true,
'notify' => true,
'audit' => true
], $options);
if ($options['validate'])
$this->validate();
if ($options['setBuildRequired'])
$this->setBuildRequired();
$this->getLog()->debug('Saving ' . $this . ' with options ' . json_encode($options, JSON_PRETTY_PRINT));
// New or existing layout
if ($this->layoutId == null || $this->layoutId == 0) {
$this->add();
if ($options['audit'])
$this->audit($this->layoutId, 'Added', ['layoutId' => $this->layoutId, 'layout' => $this->layout]);
} else if (($this->hash() != $this->hash && $options['saveLayout']) || $options['setBuildRequired']) {
$this->update($options);
if ($options['audit'])
$this->audit($this->layoutId, 'Updated');
} else {
$this->getLog()->info('Save layout properties unchanged for layoutId ' . $this->layoutId);
}
if ($options['saveRegions']) {
$this->getLog()->debug('Saving Regions on ' . $this);
// Update the regions
foreach ($this->regions as $region) {
/* @var Region $region */
// Assert the Layout Id
$region->layoutId = $this->layoutId;
$region->save($options);
}
}
if ($options['saveTags']) {
$this->getLog()->debug('Saving tags on ' . $this);
// Save the tags
if (is_array($this->tags)) {
foreach ($this->tags as $tag) {
/* @var Tag $tag */
$this->getLog()->debug('Assigning tag ' . $tag->tag);
$tag->assignLayout($this->layoutId);
$tag->save();
}
}
// Remove unwanted ones
if (is_array($this->unassignTags)) {
foreach ($this->unassignTags as $tag) {
/* @var Tag $tag */
$this->getLog()->debug('Unassigning tag ' . $tag->tag);
$tag->unassignLayout($this->layoutId);
$tag->save();
}
}
}
$this->getLog()->debug('Save finished for ' . $this);
}
/**
* Delete Layout
* @param array $options
* @throws \Exception
*/
public function delete($options = [])
{
$options = array_merge([
'deleteOrphanedPlaylists' => true
], $options);
// We must ensure everything is loaded before we delete
if (!$this->loaded)
$this->load();
$this->getLog()->debug('Deleting %s', $this);
// We cannot delete the default default
if ($this->layoutId == $this->config->GetSetting('DEFAULT_LAYOUT'))
throw new InvalidArgumentException(__('This layout is used as the global default and cannot be deleted'), 'layoutId');
// Delete Permissions
foreach ($this->permissions as $permission) {
/* @var Permission $permission */
$permission->deleteAll();
}
// Unassign all Tags
foreach ($this->tags as $tag) {
/* @var Tag $tag */
$tag->unassignLayout($this->layoutId);
$tag->save();
}
// Delete Regions
foreach ($this->regions as $region) {
/* @var Region $region */
$region->delete($options);
}
// Unassign from all Campaigns
foreach ($this->campaigns as $campaign) {
/* @var Campaign $campaign */
$campaign->setChildObjectDependencies($this->layoutFactory);
$campaign->unassignLayout($this);
$campaign->save(['validate' => false]);
}
// Delete our own Campaign
$campaign = $this->campaignFactory->getById($this->campaignId);
$campaign->setChildObjectDependencies($this->layoutFactory);
$campaign->delete();
// Remove the Layout from any display defaults
$this->getStore()->update('UPDATE `display` SET defaultlayoutid = :defaultLayoutId WHERE defaultlayoutid = :layoutId', [
'layoutId' => $this->layoutId,
'defaultLayoutId' => $this->config->GetSetting('DEFAULT_LAYOUT')
]);
// Remove the Layout (now it is orphaned it can be deleted safely)
$this->getStore()->update('DELETE FROM `layout` WHERE layoutid = :layoutId', array('layoutId' => $this->layoutId));
// Delete the cached file (if there is one)
if (file_exists($this->getCachePath()))
@unlink($this->getCachePath());
}
/**
* Validate this layout
* @throws NotFoundException
*/
public function validate()
{
// We must provide either a template or a resolution
if ($this->width == 0 || $this->height == 0)
throw new InvalidArgumentException(__('The layout dimensions cannot be empty'), 'width/height');
// Validation
if (strlen($this->layout) > 50 || strlen($this->layout) < 1)
throw new InvalidArgumentException(__("Layout Name must be between 1 and 50 characters"), 'name');
if (strlen($this->description) > 254)
throw new InvalidArgumentException(__("Description can not be longer than 254 characters"), 'description');
// Check for duplicates
$duplicates = $this->layoutFactory->query(null, array('userId' => $this->ownerId, 'layoutExact' => $this->layout, 'notLayoutId' => $this->layoutId, 'disableUserCheck' => 1));
if (count($duplicates) > 0)
throw new DuplicateEntityException(sprintf(__("You already own a layout called '%s'. Please choose another name."), $this->layout));
// Check zindex is positive
if ($this->backgroundzIndex < 0)
throw new InvalidArgumentException(__('Layer must be 0 or a positive number'), 'backgroundzIndex');
}
/**
* Does the layout have the provided tag?
* @param $searchTag
* @return bool
*/
public function hasTag($searchTag)
{
$this->load();
foreach ($this->tags as $tag) {
/* @var Tag $tag */
if ($tag->tag == $searchTag)
return true;
}
return false;
}
/**
* Assign Tag
* @param Tag $tag
* @return $this
*/
public function assignTag($tag)
{
$this->load();
if (!in_array($tag, $this->tags))
$this->tags[] = $tag;
return $this;
}
/**
* Unassign tag
* @param Tag $tag
* @return $this
*/
public function unassignTag($tag)
{
$this->tags = array_udiff($this->tags, [$tag], function($a, $b) {
/* @var Tag $a */
/* @var Tag $b */
return $a->tagId - $b->tagId;
});
return $this;
}
/**
* @param array[Tag] $tags
*/
public function replaceTags($tags = [])
{
if (!is_array($this->tags) || count($this->tags) <= 0)
$this->tags = $this->tagFactory->loadByLayoutId($this->layoutId);
$this->unassignTags = array_udiff($this->tags, $tags, function($a, $b) {
/* @var Tag $a */
/* @var Tag $b */
return $a->tagId - $b->tagId;
});
$this->getLog()->debug('Tags to be removed: %s', json_encode($this->unassignTags));
// Replace the arrays
$this->tags = $tags;
$this->getLog()->debug('Tags remaining: %s', json_encode($this->tags));
}
/**
* Export the Layout as its XLF
* @return string
*/
public function toXlf()
{
$this->getLog()->debug('Layout toXLF for Layout %s, %d', $this->layout, $this->layoutId);
$this->load(['loadPlaylists' => true]);
$document = new \DOMDocument();
$layoutNode = $document->createElement('layout');
$layoutNode->setAttribute('width', $this->width);
$layoutNode->setAttribute('height', $this->height);
$layoutNode->setAttribute('bgcolor', $this->backgroundColor);
$layoutNode->setAttribute('schemaVersion', $this->schemaVersion);
// Only set the z-index if present
if ($this->backgroundzIndex != 0)
$layoutNode->setAttribute('zindex', $this->backgroundzIndex);
if ($this->backgroundImageId != 0) {
// Get stored as
$media = $this->mediaFactory->getById($this->backgroundImageId);
$layoutNode->setAttribute('background', $media->storedAs);
}
$document->appendChild($layoutNode);
// Track module status within the layout
$status = 0;
$this->clearStatusMessage();
// We need to make some assessment based on the duration
// 1. Find out whether any of the regions have more than 1 widget
// If they do, then we will always have a region that controls duration.
// 2. If we don't, then find out if any of the single item regions have a duration specified
// If they do, then we will always have a region that controls duration.
// 3. Go through each region and assess whether they are single widget regions or not.
// If they are, then check to see if we have a region that governs duration and if we do set them
// to expire after 1 second
// 4. If they are not single widget regions, then set their duration to be either the duration specified, or
// the default duration if none has been specified.
// 5. In either case, add the duration from #4 to the region duration
$layoutHasRegionControllingDuration = false;
$layoutHasEmptyRegion = false;
foreach ($this->regions as $region) {
/* @var Region $region */
// Get a count of widgets in this region
$countWidgets = 0;
$hasDuration = false;
foreach ($region->playlists as $playlist) {
$countWidgets = $countWidgets + count($playlist->widgets);
foreach ($playlist->widgets as $widget) {
/* @var Widget $widget */
if ($widget->useDuration == 1) {
$hasDuration = true;
break;
}
}
}
// Record whether there is an empty region
if ($countWidgets <= 0)
$layoutHasEmptyRegion = true;
// Any with more than one widget
// Any with duration specified?
if ($countWidgets > 1 || $hasDuration) {
$layoutHasRegionControllingDuration = true;
}
}
if ($layoutHasEmptyRegion)
$this->getLog()->alert('Layout has empty region');
foreach ($this->regions as $region) {
/* @var Region $region */
$regionNode = $document->createElement('region');
$regionNode->setAttribute('id', $region->regionId);
$regionNode->setAttribute('width', $region->width);
$regionNode->setAttribute('height', $region->height);
$regionNode->setAttribute('top', $region->top);
$regionNode->setAttribute('left', $region->left);
// Only set the zIndex if present
if ($region->zIndex != 0)
$regionNode->setAttribute('zindex', $region->zIndex);
$layoutNode->appendChild($regionNode);
// Region Duration
$region->duration = 0;
// Region Options
$regionOptionsNode = $document->createElement('options');
foreach ($region->regionOptions as $regionOption) {
$regionOptionNode = $document->createElement($regionOption->option, $regionOption->value);
$regionOptionsNode->appendChild($regionOptionNode);
}
$regionNode->appendChild($regionOptionsNode);
// Store region look to work out duration calc
$regionLoop = $region->getOptionValue('loop', 0);
// Get a count of widgets in this region
$countWidgets = 0;
foreach ($region->playlists as $playlist) {
$countWidgets = $countWidgets + count($playlist->widgets);
}
foreach ($region->playlists as $playlist) {
/* @var Playlist $playlist */
foreach ($playlist->widgets as $widget) {
/* @var Widget $widget */
$module = $this->moduleFactory->createWithWidget($widget, $region);
// Set the Layout Status
$moduleStatus = $module->isValid();
$status = ($moduleStatus > $status) ? $moduleStatus : $status;
if ($moduleStatus > 1 && $module->getStatusMessage() != '')
$this->pushStatusMessage($module->getStatusMessage());
// Determine the duration of this widget
if ($widget->useDuration == 1) {
// Widget duration is as specified
$widgetDuration = $widget->duration;
// The calculated duration is the provided one
$widget->calculatedDuration = $widgetDuration;
} else if (!$layoutHasRegionControllingDuration || $countWidgets > 1 || $regionLoop == 1) {
// No specified duration, but we've detected that we need to use the default duration
// Edge case being video - we must ensure that the default duration for video is always 0.
$widgetDuration = $module->getModule()->defaultDuration;
// The calculated duration is the "real" duration (caters for 0 videos)
$widget->calculatedDuration = (($widgetDuration == 0) ? $module->getDuration(['real' => true]) : $widgetDuration);
} else {
// No specified duration, add nothing to region duration and expire the widget in 1 second
$widgetDuration = Widget::$widgetMinDuration;
// The calculated duration is 0
$widget->calculatedDuration = 0;
}
// Does our widget have a durationIsPerItem and a Number of Items?
$numItems = $widget->getOptionValue('numItems', 0);
if ($widget->getOptionValue('durationIsPerItem', 0) == 1 && $numItems > 1) {
$widget->calculatedDuration = (($widget->useDuration == 1) ? $widget->duration : $module->getModule()->defaultDuration) * $numItems;
}
// Region duration
$region->duration = $region->duration + $widget->calculatedDuration;
// We also want to add any transition OUT duration
// only the OUT duration because IN durations do not get added to the widget duration by the player
// https://github.com/xibosignage/xibo/issues/705
if ($widget->getOptionValue('transOut', '') != '') {
// Transition durations are in milliseconds
$region->duration = $region->duration + ($widget->getOptionValue('transOutDuration', 0) / 1000);
}
// Create media xml node for XLF.
$renderAs = $module->getModule()->renderAs;
$mediaNode = $document->createElement('media');
$mediaNode->setAttribute('id', $widget->widgetId);
$mediaNode->setAttribute('type', $widget->type);
$mediaNode->setAttribute('render', ($renderAs == '') ? 'native' : $renderAs);
// Set the duration according to whether we are using widget duration or not
$mediaNode->setAttribute('duration', $widgetDuration);
$mediaNode->setAttribute('useDuration', $widget->useDuration);
// Create options nodes
$optionsNode = $document->createElement('options');
$rawNode = $document->createElement('raw');
$mediaNode->appendChild($optionsNode);
$mediaNode->appendChild($rawNode);
// Inject the URI
$uriInjected = false;
if ($module->getModule()->regionSpecific == 0) {
$media = $this->mediaFactory->getById($widget->getPrimaryMediaId());
$optionNode = $document->createElement('uri', $media->storedAs);
$optionsNode->appendChild($optionNode);
$uriInjected = true;
// Add the fileId attribute to the media element
$mediaNode->setAttribute('fileId', $media->mediaId);
}
foreach ($widget->widgetOptions as $option) {
/* @var WidgetOption $option */
if (trim($option->value) === '')
continue;
if ($option->type == 'cdata') {
$optionNode = $document->createElement($option->option);
$cdata = $document->createCDATASection($option->value);
$optionNode->appendChild($cdata);
$rawNode->appendChild($optionNode);
}
else if ($option->type == 'attrib' || $option->type == 'attribute') {
if ($uriInjected && $option->option == 'uri')
continue;
$optionNode = $document->createElement($option->option, $option->value);
$optionsNode->appendChild($optionNode);
}
}
// Handle associated audio
$audioNodes = null;
foreach ($widget->audio as $audio) {
/** @var WidgetAudio $audio */
if ($audioNodes == null)
$audioNodes = $document->createElement('audio');
// Get the full media node for this audio element
$audioMedia = $this->mediaFactory->getById($audio->mediaId);
$audioNode = $document->createElement('uri', $audioMedia->storedAs);
$audioNode->setAttribute('volume', $audio->volume);
$audioNode->setAttribute('loop', $audio->loop);
$audioNode->setAttribute('mediaId', $audio->mediaId);
$audioNodes->appendChild($audioNode);
}
if ($audioNodes != null)
$mediaNode->appendChild($audioNodes);
// Save our widget
$widget->save([
'notify' => false,
'saveWidgetOptions' => false
]);
$regionNode->appendChild($mediaNode);
}
}
$this->getLog()->debug('Region duration on layout %d is %d. Comparing to %d.', $this->layoutId, $region->duration, $this->duration);
// Track the max duration within the layout
// Test this duration against the layout duration
if ($this->duration < $region->duration)
$this->duration = $region->duration;
$event = new LayoutBuildRegionEvent($region->regionId, $regionNode);
$this->dispatcher->dispatch($event::NAME, $event);
// End of region loop.
}
$this->getLog()->debug('Setting Layout Duration to %d', $this->duration);
$tagsNode = $document->createElement('tags');
foreach ($this->tags as $tag) {
/* @var Tag $tag */
$tagNode = $document->createElement('tag', $tag->tag);
$tagsNode->appendChild($tagNode);
}
$layoutNode->appendChild($tagsNode);
// Update the layout status / duration accordingly
if ($layoutHasEmptyRegion)
$status = 4;
$this->status = ($status < $this->status) ? $status : $this->status;
// Fire a layout.build event, passing the layout and the generated document.
$event = new LayoutBuildEvent($this, $document);
$this->dispatcher->dispatch($event::NAME, $event);
return $document->saveXML();
}
/**
* Export the Layout as a ZipArchive
* @param DataSetFactory $dataSetFactory
* @param string $fileName
* @param array $options
*/
public function toZip($dataSetFactory, $fileName, $options = [])
{
$options = array_merge([
'includeData' => false
], $options);
// Load the complete layout
$this->load();
// We export to a ZIP file
$zip = new \ZipArchive();
$result = $zip->open($fileName, \ZipArchive::CREATE | \ZipArchive::OVERWRITE);
if ($result !== true)
throw new \InvalidArgumentException(__('Can\'t create ZIP. Error Code: ' . $result));
// Add a mapping file for the region names
$regionMapping = [];
foreach ($this->regions as $region) {
/** @var Region $region */
$regionMapping[$region->regionId] = $region->name;
}
// Add layout information to the ZIP
$zip->addFromString('layout.json', json_encode([
'layout' => $this->layout,
'description' => $this->description,
'regions' => $regionMapping
]));
// Add the layout XLF
$zip->addFile($this->xlfToDisk(), 'layout.xml');
// Add all media
$libraryLocation = $this->config->GetSetting('LIBRARY_LOCATION');
$mappings = [];
foreach ($this->mediaFactory->getByLayoutId($this->layoutId) as $media) {
/* @var Media $media */
$zip->addFile($libraryLocation . $media->storedAs, 'library/' . $media->fileName);
$mappings[] = [
'file' => $media->fileName,
'mediaid' => $media->mediaId,
'name' => $media->name,
'type' => $media->mediaType,
'duration' => $media->duration,
'background' => 0,
'font' => 0
];
}
// Add the background image
if ($this->backgroundImageId != 0) {
$media = $this->mediaFactory->getById($this->backgroundImageId);
$zip->addFile($libraryLocation . $media->storedAs, 'library/' . $media->fileName);
$mappings[] = [
'file' => $media->fileName,
'mediaid' => $media->mediaId,
'name' => $media->name,
'type' => $media->mediaType,
'duration' => $media->duration,
'background' => 1,
'font' => 0
];
}
// Add any fonts
// parse the XLF file for any font declarations contains therein
// get those font media files by name and add them to the zip
$fonts = null;
preg_match_all('/font-family:(.*?);/', $this->toXlf(), $fonts);
if ($fonts != null) {
$this->getLog()->debug('Matched fonts: %s', json_encode($fonts));
foreach ($fonts[1] as $font) {
$matches = $this->mediaFactory->query(null, array('disableUserCheck' => 1, 'nameExact' => $font, 'allModules' => 1, 'type' => 'font'));
if (count($matches) <= 0) {
$this->getLog()->info('Unmatched font during export: %s', $font);
continue;
}
$media = $matches[0];
$zip->addFile($libraryLocation . $media->storedAs, 'library/' . $media->fileName);
$mappings[] = [
'file' => $media->fileName,
'mediaid' => $media->mediaId,
'name' => $media->name,
'type' => $media->mediaType,
'duration' => $media->duration,
'background' => 0,
'font' => 1
];
}
}
// Add the mappings file to the ZIP
$zip->addFromString('mapping.json', json_encode($mappings));
// Handle any DataSet structures
$dataSetIds = [];
$dataSets = [];
foreach ($this->getWidgets() as $widget) {
/** @var Widget $widget */
if ($widget->type == 'datasetview' || $widget->type == 'ticker') {
$dataSetId = $widget->getOptionValue('dataSetId', 0);
if ($dataSetId != 0) {
if (in_array($dataSetId, $dataSetIds))
continue;
// Export the structure for this dataSet
$dataSet = $dataSetFactory->getById($dataSetId);
$dataSet->load();
// Are we also looking to export the data?
if ($options['includeData']) {
$dataSet->data = $dataSet->getData([], ['includeFormulaColumns' => false]);
}
$dataSetIds[] = $dataSet->dataSetId;
$dataSets[] = $dataSet;
}
}
}
// Add the mappings file to the ZIP
$zip->addFromString('dataSet.json', json_encode($dataSets, JSON_PRETTY_PRINT));
$zip->close();
}
/**
* Save the XLF to disk if necessary
* @param array $options
* @return string the path
*/
public function xlfToDisk($options = [])
{
$options = array_merge([
'notify' => true,
'collectNow' => true
], $options);
$path = $this->getCachePath();
if ($this->status == 3 || !file_exists($path)) {
$this->getLog()->debug('XLF needs building for Layout %d', $this->layoutId);
// Assume error
$this->status = 4;
// Reset duration
$this->duration = 0;
// Save the resulting XLF
try {
file_put_contents($path, $this->toXlf());
} catch (\Exception $e) {
$this->getLog()->error('Cannot build Layout ' . $this->layoutId . '. Unexpected error: ' . $e->getMessage());
// Will continue and save the status as 4
$this->status = 4;
$this->statusMessage = 'Unexpected Error';
}
$this->save([
'saveRegions' => true,
'saveRegionOptions' => false,
'manageRegionAssignments' => false,
'saveTags' => false,
'setBuildRequired' => false,
'audit' => false,
'validate' => false,
'notify' => $options['notify'],
'collectNow' => $options['collectNow']
]);
}
return $path;
}
/**
* @return string
*/
private function getCachePath()
{
$libraryLocation = $this->config->GetSetting('LIBRARY_LOCATION');
return $libraryLocation . $this->layoutId . '.xlf';
}
//
// Add / Update
//
/**
* Add
*/
private function add()
{
$this->getLog()->debug('Adding Layout ' . $this->layout);
$sql = 'INSERT INTO layout (layout, description, userID, createdDT, modifiedDT, status, width, height, schemaVersion, backgroundImageId, backgroundColor, backgroundzIndex)
VALUES (:layout, :description, :userid, :createddt, :modifieddt, :status, :width, :height, 3, :backgroundImageId, :backgroundColor, :backgroundzIndex)';
$time = $this->date->getLocalDate();
$this->layoutId = $this->getStore()->insert($sql, array(
'layout' => $this->layout,
'description' => $this->description,
'userid' => $this->ownerId,
'createddt' => $time,
'modifieddt' => $time,
'status' => 3,
'width' => $this->width,
'height' => $this->height,
'backgroundImageId' => $this->backgroundImageId,
'backgroundColor' => $this->backgroundColor,
'backgroundzIndex' => $this->backgroundzIndex,
));
// Add a Campaign
$campaign = $this->campaignFactory->createEmpty();
$campaign->campaign = $this->layout;
$campaign->isLayoutSpecific = 1;
$campaign->ownerId = $this->getOwnerId();
$campaign->assignLayout($this);
// Ready to save the Campaign
$campaign->save();
// Assign the new campaignId to this layout
$this->campaignId = $campaign->campaignId;
}
/**
* Update
* @param array $options
* NOTE: We set the XML to NULL during this operation as we will always convert old layouts to the new structure
*/
private function update($options = [])
{
$options = array_merge([
'notify' => true,
'collectNow' => true
], $options);
$this->getLog()->debug('Editing Layout ' . $this->layout . '. Id = ' . $this->layoutId);
$sql = '
UPDATE layout
SET layout = :layout,
description = :description,
duration = :duration,
modifiedDT = :modifieddt,
retired = :retired,
width = :width,
height = :height,
backgroundImageId = :backgroundImageId,
backgroundColor = :backgroundColor,
backgroundzIndex = :backgroundzIndex,
`status` = :status,
`userId` = :userId,
`schemaVersion` = :schemaVersion,
`statusMessage` = :statusMessage
WHERE layoutID = :layoutid
';
$time = $this->date->getLocalDate();
$this->getStore()->update($sql, array(
'layoutid' => $this->layoutId,
'layout' => $this->layout,
'description' => $this->description,
'duration' => $this->duration,
'modifieddt' => $time,
'retired' => $this->retired,
'width' => $this->width,
'height' => $this->height,
'backgroundImageId' => $this->backgroundImageId,
'backgroundColor' => $this->backgroundColor,
'backgroundzIndex' => $this->backgroundzIndex,
'status' => $this->status,
'userId' => $this->ownerId,
'schemaVersion' => $this->schemaVersion,
'statusMessage' => (empty($this->statusMessage)) ? null : json_encode($this->statusMessage)
));
// Update the Campaign
$campaign = $this->campaignFactory->getById($this->campaignId);
$campaign->campaign = $this->layout;
$campaign->ownerId = $this->ownerId;
$campaign->save(['validate' => false, 'notify' => $options['notify'], 'collectNow' => $options['collectNow']]);
}
}