. */ 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; } // /** * @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); } // }