. */ namespace Xibo\Entity; use Carbon\Carbon; use Xibo\Event\SubPlaylistDurationEvent; use Xibo\Event\WidgetDeleteEvent; use Xibo\Event\WidgetEditEvent; use Xibo\Factory\ActionFactory; use Xibo\Factory\PermissionFactory; use Xibo\Factory\WidgetAudioFactory; use Xibo\Factory\WidgetMediaFactory; use Xibo\Factory\WidgetOptionFactory; use Xibo\Helper\DateFormatHelper; use Xibo\Service\DisplayNotifyServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; use Xibo\Support\Exception\NotFoundException; use Xibo\Widget\Definition\Property; /** * Class Widget * @package Xibo\Entity * * @SWG\Definition() */ class Widget implements \JsonSerializable { public static $DATE_MIN = 0; public static $DATE_MAX = 2147483647; use EntityTrait; /** * @SWG\Property(description="The Widget ID") * @var int */ public $widgetId; /** * @SWG\Property(description="The ID of the Playlist this Widget belongs to") * @var int */ public $playlistId; /** * @SWG\Property(description="The ID of the User that owns this Widget") * @var int */ public $ownerId; /** * @SWG\Property(description="The Module Type Code") * @var string */ public $type; /** * @SWG\Property(description="The duration in seconds this widget should be shown") * @var int */ public $duration; /** * @SWG\Property(description="The display order of this widget") * @var int */ public $displayOrder; /** * @SWG\Property(description="Flag indicating if this widget has a duration that should be used") * @var int */ public $useDuration; /** * @SWG\Property(description="Calculated Duration of this widget after taking into account the useDuration flag") * @var int */ public $calculatedDuration = 0; /** * @var string * @SWG\Property( * description="The datetime the Layout was created" * ) */ public $createdDt; /** * @var string * @SWG\Property( * description="The datetime the Layout was last modified" * ) */ public $modifiedDt; /** * @SWG\Property(description="Widget From Date") * @var int */ public $fromDt; /** * @SWG\Property(description="Widget To Date") * @var int */ public $toDt; /** * @SWG\Property(description="Widget Schema Version") * @var int */ public $schemaVersion; /** * @SWG\Property(description="Transition Type In") * @var int */ public $transitionIn; /** * @SWG\Property(description="Transition Type out") * @var int */ public $transitionOut; /** * @SWG\Property(description="Transition duration in") * @var int */ public $transitionDurationIn; /** * @SWG\Property(description="Transition duration out") * @var int */ public $transitionDurationOut; /** * @SWG\Property(description="An array of Widget Options") * @var WidgetOption[] */ public $widgetOptions = []; /** * @SWG\Property(description="An array of MediaIds this widget is linked to") * @var int[] */ public $mediaIds = []; /** * @SWG\Property(description="An array of Audio MediaIds this widget is linked to") * @var WidgetAudio[] */ public $audio = []; /** * @SWG\Property(description="An array of permissions for this widget") * @var Permission[] */ public $permissions = []; /** * @SWG\Property(description="The name of the Playlist this Widget is on") * @var string $playlist */ public $playlist; /** @var Action[] */ public $actions = []; /** * Hash Key of Media Assignments * @var string */ private $mediaHash = null; /** * Temporary media Id used during import/upgrade/sub-playlist ordering * @var string read only string */ public $tempId = null; /** * Temporary widget Id used during import/upgrade/sub-playlist ordering * @var string read only string */ public $tempWidgetId = null; /** * Flag to indicate whether the widget is valid * @var bool */ public $isValid = false; /** * Flag to indicate whether the widget is newly added * @var bool */ public $isNew = false; public $folderId; public $permissionsFolderId; /** @var int[] Original Media IDs */ private $originalMediaIds = []; /** @var array[WidgetAudio] Original Widget Audio */ private $originalAudio = []; /** @var \Xibo\Entity\WidgetOption[] Original widget options when this widget was laded */ private $originalWidgetOptions = []; /** * Minimum duration for widgets * @var int */ public static $widgetMinDuration = 1; private $datesToFormat = ['toDt', 'fromDt']; // /** * @var WidgetOptionFactory */ private $widgetOptionFactory; /** * @var WidgetMediaFactory */ private $widgetMediaFactory; /** @var WidgetAudioFactory */ private $widgetAudioFactory; /** * @var PermissionFactory */ private $permissionFactory; /** @var DisplayNotifyServiceInterface */ private $displayNotifyService; /** @var ActionFactory */ private $actionFactory; // /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher * @param WidgetOptionFactory $widgetOptionFactory * @param WidgetMediaFactory $widgetMediaFactory * @param WidgetAudioFactory $widgetAudioFactory * @param PermissionFactory $permissionFactory * @param DisplayNotifyServiceInterface $displayNotifyService * @param ActionFactory $actionFactory */ public function __construct( $store, $log, $dispatcher, $widgetOptionFactory, $widgetMediaFactory, $widgetAudioFactory, $permissionFactory, $displayNotifyService, $actionFactory ) { $this->setCommonDependencies($store, $log, $dispatcher); $this->excludeProperty('module'); $this->widgetOptionFactory = $widgetOptionFactory; $this->widgetMediaFactory = $widgetMediaFactory; $this->widgetAudioFactory = $widgetAudioFactory; $this->permissionFactory = $permissionFactory; $this->displayNotifyService = $displayNotifyService; $this->actionFactory = $actionFactory; } public function __clone() { $this->hash = null; $this->widgetId = null; $this->widgetOptions = array_map(function ($object) { return clone $object; }, $this->widgetOptions); $this->permissions = []; // No need to clone the media, but we should empty the original arrays of ids $this->originalMediaIds = []; $this->originalAudio = []; // Clone actions $this->actions = array_map(function ($object) { return clone $object; }, $this->actions); } /** * String * @return string */ public function __toString() { return sprintf('Widget. %s on playlist %d in position %d. WidgetId = %d', $this->type, $this->playlistId, $this->displayOrder, $this->widgetId); } public function getPermissionFolderId() { return $this->permissionsFolderId; } /** * Get the Display Notify Service * @return DisplayNotifyServiceInterface */ public function getDisplayNotifyService(): DisplayNotifyServiceInterface { return $this->displayNotifyService->init(); } /** * Unique Hash * @return string */ private function hash() { return md5($this->widgetId . $this->playlistId . $this->ownerId . $this->type . $this->duration . $this->displayOrder . $this->useDuration . $this->calculatedDuration . $this->fromDt . $this->toDt . json_encode($this->widgetOptions) . json_encode($this->actions) ); } /** * Hash of all media id's * @return string */ private function mediaHash() { sort($this->mediaIds); return md5(implode(',', $this->mediaIds)); } /** * Get the Id * @return int */ public function getId() { return $this->widgetId; } /** * Get the OwnerId * @return int */ public function getOwnerId() { return $this->ownerId; } /** * Set the Owner * @param int $ownerId */ public function setOwner($ownerId) { $this->ownerId = $ownerId; } /** * Get Option * @param string $option * @param bool $originalValue * @return WidgetOption * @throws \Xibo\Support\Exception\NotFoundException */ public function getOption(string $option, bool $originalValue = false): WidgetOption { $widgetOptions = $originalValue ? $this->originalWidgetOptions : $this->widgetOptions; foreach ($widgetOptions as $widgetOption) { if (strtolower($widgetOption->option) == strtolower($option)) { return $widgetOption; } } throw new NotFoundException(__('Widget Option not found')); } /** * Remove an option * @param string $option * @return $this */ public function removeOption(string $option): Widget { try { $widgetOption = $this->getOption($option); $this->getLog()->debug('removeOption: ' . $option); // Unassign foreach ($this->widgetOptions as $key => $value) { if ($value->option === $option) { unset($this->widgetOptions[$key]); } } // Delete now $widgetOption->delete(); } catch (NotFoundException $exception) { // This is good, notihng to do. } return $this; } /** * Change an option * @param string $option * @param string $newOption * @return $this */ public function changeOption(string $option, string $newOption): Widget { try { $widgetOption = $this->getOption($option); $this->getLog()->debug('changeOption: ' . $option); // Unassign foreach ($this->widgetOptions as $key => $value) { if ($value->option === $option) { unset($this->widgetOptions[$key]); } } // Change now $widgetOption->delete(); $this->widgetOptions[] = $this->widgetOptionFactory->create($this->widgetId, $widgetOption->type, $newOption, $widgetOption->value); } catch (NotFoundException $exception) { // This is good, nothing to do. } return $this; } /** * Get Widget Option Value * @param string $option * @param mixed $default * @param bool $originalValue * @return mixed */ public function getOptionValue(string $option, $default, bool $originalValue = false) { try { $widgetOption = $this->getOption($option, $originalValue); $widgetOption = (($widgetOption->value) === null) ? $default : $widgetOption->value; if (is_integer($default)) { $widgetOption = intval($widgetOption); } return $widgetOption; } catch (NotFoundException $e) { return $default; } } /** * Set Widget Option Value * @param string $option * @param string $type * @param mixed $value */ public function setOptionValue(string $option, string $type, $value) { $this->getLog()->debug('setOptionValue: ' . $option . ', ' . $type . '. Value = ' . $value); try { $widgetOption = $this->getOption($option); $widgetOption->type = $type; $widgetOption->value = $value; } catch (NotFoundException $e) { $this->widgetOptions[] = $this->widgetOptionFactory->create($this->widgetId, $type, $option, $value); } } /** * Assign File Media * @param int $mediaId */ public function assignMedia($mediaId) { $this->load(); if (!in_array($mediaId, $this->mediaIds)) $this->mediaIds[] = $mediaId; } /** * Unassign File Media * @param int $mediaId */ public function unassignMedia($mediaId) { $this->load(); $this->mediaIds = array_diff($this->mediaIds, [$mediaId]); } /** * Count media * @return int count of media */ public function countMedia() { $this->load(); return count($this->mediaIds); } /** * @return int * @throws NotFoundException */ public function getPrimaryMediaId() { $primary = $this->getPrimaryMedia(); if (count($primary) <= 0) throw new NotFoundException(__('No file to return')); return $primary[0]; } /** * Get Primary Media * @return int[] */ public function getPrimaryMedia() { $this->load(); $this->getLog()->debug('Getting first primary media for Widget: ' . $this->widgetId . ' Media: ' . json_encode($this->mediaIds) . ' audio ' . json_encode($this->getAudioIds())); if (count($this->mediaIds) <= 0) return []; // Remove the audio media from this array return array_values(array_diff($this->mediaIds, $this->getAudioIds())); } /** * Get the temporary path * @return string */ public function getLibraryTempPath(): string { return $this->widgetMediaFactory->getLibraryTempPath(); } /** * Get the path of the primary media * @return string * @throws NotFoundException */ public function getPrimaryMediaPath(): string { return $this->widgetMediaFactory->getPathForMediaId($this->getPrimaryMediaId()); } /** * Assign Audio Media * @param WidgetAudio $audio */ public function assignAudio($audio) { $this->load(); $found = false; foreach ($this->audio as $existingAudio) { if ($existingAudio->mediaId == $audio->mediaId) { $existingAudio->loop = $audio->loop; $existingAudio->volume = $audio->volume; $found = true; break; } } if (!$found) $this->audio[] = $audio; // Assign the media $this->assignMedia($audio->mediaId); } /** * Unassign Audio Media * @param int $mediaId */ public function assignAudioById($mediaId) { $this->load(); $widgetAudio = $this->widgetAudioFactory->createEmpty(); $widgetAudio->mediaId = $mediaId; $widgetAudio->volume = 100; $widgetAudio->loop = 0; $this->assignAudio($widgetAudio); } /** * Unassign Audio Media * @param WidgetAudio $audio */ public function unassignAudio($audio) { $this->load(); $this->audio = array_udiff($this->audio, [$audio], function($a, $b) { /** * @var WidgetAudio $a * @var WidgetAudio $b */ return $a->getId() - $b->getId(); }); // Unassign the media $this->unassignMedia($audio->mediaId); } /** * Unassign Audio Media * @param int $mediaId */ public function unassignAudioById($mediaId) { $this->load(); foreach ($this->audio as $audio) { if ($audio->mediaId == $mediaId) $this->unassignAudio($audio); } } /** * Count Audio * @return int */ public function countAudio() { $this->load(); return count($this->audio); } /** * Get AudioIds * @return int[] */ public function getAudioIds() { $this->load(); return array_map(function($element) { /** @var WidgetAudio $element */ return $element->mediaId; }, $this->audio); } /** * Have the media assignments changed. */ public function hasMediaChanged() { return ($this->mediaHash != $this->mediaHash()); } /** * @return bool true if this widget has an expiry date */ public function hasExpiry() { return $this->toDt !== self::$DATE_MAX; } /** * @return bool true if this widget has expired */ public function isExpired() { return ($this->toDt !== self::$DATE_MAX && Carbon::createFromTimestamp($this->toDt)->format('U') < Carbon::now()->format('U')); } /** * Calculates the duration of this widget according to some rules * @param \Xibo\Entity\Module $module * @param bool $import * @return $this */ public function calculateDuration( Module $module, bool $import = false ): Widget { $this->getLog()->debug('calculateDuration: Calculating for ' . $this->type . ' - existing value is ' . $this->calculatedDuration . ' import is ' . ($import ? 'true' : 'false')); // Import // ------ // If we are importing a layout we need to adjust the `duration` **before** we pass to any duration // provider, as providers will use the duration set on the widget in their calculations. // $this->duration from xml is `duration * (numItems/itemsPerPage)` if ($import) { $numItems = $this->getOptionValue('numItems', 1); if ($this->getOptionValue('durationIsPerItem', 0) == 1 && $numItems > 1) { // If we have paging involved then work out the page count. $itemsPerPage = $this->getOptionValue('itemsPerPage', 0); if ($itemsPerPage > 0) { $numItems = ceil($numItems / $itemsPerPage); } // This is a change to v3 // in v3 we only divide by numItems if useDuration = 0, which I think was wrong. $this->duration = ($this->useDuration == 1 ? $this->duration : $module->defaultDuration) / $numItems; } } // Start with either the default module duration, or the duration provided if ($this->useDuration == 1) { // Widget duration is as specified $this->calculatedDuration = $this->duration; } else { // Use the default duration $this->calculatedDuration = $module->defaultDuration; } // Modify the duration if necessary if ($module->type === 'subplaylist') { // Sub Playlists are a special case and provide their own duration. $this->getLog()->debug('calculateDuration: subplaylist using SubPlaylistDurationEvent'); $event = new SubPlaylistDurationEvent($this); $this->getDispatcher()->dispatch($event, SubPlaylistDurationEvent::$NAME); $this->calculatedDuration = $event->getDuration(); } else { // Our module will calculate the duration for us. $duration = $module->calculateDuration($this); if ($duration !== null) { $this->calculatedDuration = $duration; } else { $this->getLog()->debug('calculateDuration: Duration not set by module'); } } $this->getLog()->debug('calculateDuration: set to ' . $this->calculatedDuration); return $this; } /** * @return int * @throws NotFoundException */ public function getDurationForMedia(): int { return $this->widgetMediaFactory->getDurationForMediaId($this->getPrimaryMediaId()); } /** * Load the Widget * @param bool $loadActions * @return Widget */ public function load(bool $loadActions = true): Widget { if ($this->loaded || $this->widgetId == null || $this->widgetId == 0) { return $this; } // Load permissions $this->permissions = $this->permissionFactory->getByObjectId(get_class($this), $this->widgetId); // Load the widget options $this->widgetOptions = $this->widgetOptionFactory->getByWidgetId($this->widgetId); foreach ($this->widgetOptions as $widgetOption) { $this->originalWidgetOptions[] = clone $widgetOption; } // Load any media assignments for this widget $this->mediaIds = $this->widgetMediaFactory->getByWidgetId($this->widgetId); $this->originalMediaIds = $this->mediaIds; // Load any widget audio assignments $this->audio = $this->widgetAudioFactory->getByWidgetId($this->widgetId); $this->originalAudio = $this->audio; if ($loadActions) { $this->actions = $this->actionFactory->getBySourceAndSourceId('widget', $this->widgetId); } $this->hash = $this->hash(); $this->mediaHash = $this->mediaHash(); $this->loaded = true; return $this; } /** * Load the Widget with minimal data i.e., options */ public function loadMinimum(): void { if ($this->loaded || $this->widgetId == null || $this->widgetId == 0) { return; } // Load the widget options $this->widgetOptions = $this->widgetOptionFactory->getByWidgetId($this->widgetId); foreach ($this->widgetOptions as $widgetOption) { $this->originalWidgetOptions[] = clone $widgetOption; } $this->loaded = true; } /** * @param Property[] $properties * @return \Xibo\Entity\Widget */ public function applyProperties(array $properties): Widget { foreach ($properties as $property) { // Do not save null properties. if ($property->value === null) { $this->removeOption($property->id); } else { // Apply filters $property->applyFilters(); // Set the property for saving into the widget options $this->setOptionValue($property->id, $property->isCData() ? 'cdata' : 'attrib', $property->value); // If this property allows library references to be added, we parse them out here and assign // the matching media to the widget. if ($property->allowLibraryRefs) { // Parse them out and replace for our special syntax. $matches = []; preg_match_all('/\[(.*?)\]/', $property->value, $matches); foreach ($matches[1] as $match) { if (is_numeric($match)) { $this->assignMedia(intval($match)); } } } // Is this a media selector? and if so should we assign the library media if ($property->type === 'mediaSelector') { if (!empty($value) && is_numeric($value)) { $this->assignMedia(intval($value)); } } } } return $this; } /** * Save the widget * @param array $options * @throws \Xibo\Support\Exception\NotFoundException */ public function save($options = []) { // Default options $options = array_merge([ 'saveWidgetOptions' => true, 'saveWidgetAudio' => true, 'saveWidgetMedia' => true, 'notify' => true, 'notifyPlaylists' => true, 'notifyDisplays' => false, 'audit' => true, 'auditWidgetOptions' => true, 'auditMessage' => 'Saved', 'alwaysUpdate' => false, 'import' => false, 'upgrade' => false, ], $options); $this->getLog()->debug('Saving widgetId ' . $this->getId() . ' with options. ' . json_encode($options, JSON_PRETTY_PRINT)); // if we are auditing get layout specific campaignId $campaignId = 0; $layoutId = 0; if ($options['audit']) { $results = $this->store->select(' SELECT `campaign`.campaignId, `layout`.layoutId FROM `playlist` INNER JOIN `region` ON `playlist`.regionId = `region`.regionId INNER JOIN `layout` ON `region`.layoutId = `layout`.layoutId INNER JOIN `lkcampaignlayout` ON `layout`.layoutId = `lkcampaignlayout`.layoutId INNER JOIN `campaign` ON `campaign`.campaignId = `lkcampaignlayout`.campaignId WHERE `campaign`.isLayoutSpecific = 1 AND `playlist`.playlistId = :playlistId ', [ 'playlistId' => $this->playlistId ]); foreach ($results as $row) { $campaignId = intval($row['campaignId']); $layoutId = intval($row['layoutId']); } } // Add/Edit $isNew = $this->widgetId == null || $this->widgetId == 0; if ($isNew) { $this->add(); } else { // When saving after Widget compatibility upgrade // do not trigger this event, as it will throw an error // this is due to mismatch between playlist closure table (already populated) // and subPlaylists option original values (empty array) - attempt to add the same child will error out. if (!$options['upgrade']) { $this->getDispatcher()->dispatch(new WidgetEditEvent($this), WidgetEditEvent::$NAME); } if ($this->hash != $this->hash() || $options['alwaysUpdate']) { $this->update(); } } // Save the widget options if ($options['saveWidgetOptions']) { foreach ($this->widgetOptions as $widgetOption) { // Assert the widgetId $widgetOption->widgetId = $this->widgetId; $widgetOption->save(); } } // Save the widget audio if ($options['saveWidgetAudio']) { foreach ($this->audio as $audio) { // Assert the widgetId $audio->widgetId = $this->widgetId; $audio->save(); } $removedAudio = array_udiff($this->originalAudio, $this->audio, function ($a, $b) { /** * @var WidgetAudio $a * @var WidgetAudio $b */ return $a->getId() - $b->getId(); }); foreach ($removedAudio as $audio) { /* @var \Xibo\Entity\WidgetAudio $audio */ // Assert the widgetId $audio->widgetId = $this->widgetId; $audio->delete(); } } // Manage the assigned media if ($options['saveWidgetMedia'] || $options['saveWidgetAudio']) { $this->linkMedia(); $this->unlinkMedia(); } // Call notify with the notify options passed in $this->notify($options); if ($options['audit']) { if ($isNew) { if ($campaignId != 0 && $layoutId != 0) { $this->audit($this->widgetId, 'Added', [ 'widgetId' => $this->widgetId, 'type' => $this->type, 'layoutId' => $layoutId, 'campaignId' => $campaignId ]); } } else { // For elements, do not try to look up changed properties. $changedProperties = $options['auditWidgetOptions'] ? $this->getChangedProperties() : []; $changedItems = []; if ($options['auditWidgetOptions']) { foreach ($this->widgetOptions as $widgetOption) { $itemsProperties = $widgetOption->getChangedProperties(); // for widget options what we get from getChangedProperities is an array with value as key and // changed value as value we want to override the key in the returned array, so that we get a // clear option name that was changed if (array_key_exists('value', $itemsProperties)) { $itemsProperties[$widgetOption->option] = $itemsProperties['value']; unset($itemsProperties['value']); } if (count($itemsProperties) > 0) { $changedItems[] = $itemsProperties; } } } if (count($changedItems) > 0) { $changedProperties['widgetOptions'] = json_encode($changedItems, JSON_PRETTY_PRINT); } // if we are editing a widget assigned to a regionPlaylist add the layout specific campaignId to // the audit log if ($campaignId != 0 && $layoutId != 0) { $changedProperties['campaignId'][] = $campaignId; $changedProperties['layoutId'][] = $layoutId; } $this->audit($this->widgetId, $options['auditMessage'], $changedProperties); } } } /** * @param array $options */ public function delete($options = []) { $options = array_merge([ 'notify' => true, 'notifyPlaylists' => true, 'forceNotifyPlaylists' => true, 'notifyDisplays' => false ], $options); // We must ensure everything is loaded before we delete $this->load(); // Widget Delete Event $this->getDispatcher()->dispatch(new WidgetDeleteEvent($this), WidgetDeleteEvent::$NAME); // Delete Permissions foreach ($this->permissions as $permission) { $permission->deleteAll(); } // Delete all Options foreach ($this->widgetOptions as $widgetOption) { // Assert the widgetId $widgetOption->widgetId = $this->widgetId; $widgetOption->delete(); } // Delete the widget audio foreach ($this->audio as $audio) { // Assert the widgetId $audio->widgetId = $this->widgetId; $audio->delete(); } foreach ($this->actions as $action) { $action->delete(); } // Set widgetId to null on any navWidget action that was using this drawer Widget. $this->getStore()->update( 'UPDATE `action` SET `action`.widgetId = NULL WHERE widgetId = :widgetId AND `action`.actionType = \'navWidget\' ', ['widgetId' => $this->widgetId] ); $this->mediaIds = []; // initialize media Ids to unlink $mediaIdsToUnlink = $this->getMediaIdsToUnlink(); // Unlink Media $this->unlinkMedia(); // Delete any fallback data $this->getStore()->update('DELETE FROM `widgetdata` WHERE `widgetId` = :widgetId', [ 'widgetId' => $this->widgetId, ]); // Delete this $this->getStore()->update('DELETE FROM `widget` WHERE `widgetId` = :widgetId', [ 'widgetId' => $this->widgetId, ]); // Call notify with the notify options passed in $this->notify($options); $this->getLog()->debug('Delete Widget Complete'); // Audit $this->audit($this->widgetId, 'Deleted', array_merge( ['widgetId' => $this->widgetId, 'playlistId' => $this->playlistId], $mediaIdsToUnlink ) ); } /** * Notify * @param $options */ private function notify($options) { // By default we do nothing in here, options have to be explicitly enabled. $options = array_merge([ 'notify' => false, 'notifyPlaylists' => false, 'forceNotifyPlaylists' => false, 'notifyDisplays' => false ], $options); $this->getLog()->debug('Notifying upstream playlist. Notify Layout: ' . $options['notify'] . ' Notify Displays: ' . $options['notifyDisplays']); // Should we notify the Playlist // we do this if the duration has changed on this widget. if ($options['forceNotifyPlaylists']|| ($options['notifyPlaylists'] && ( $this->hasPropertyChanged('calculatedDuration') || $this->hasPropertyChanged('fromDt') || $this->hasPropertyChanged('toDt') ))) { // Notify the Playlist $this->getStore()->update('UPDATE `playlist` SET requiresDurationUpdate = 1, `modifiedDT` = :modifiedDt WHERE playlistId = :playlistId', [ 'playlistId' => $this->playlistId, 'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()) ]); } // Notify Layout // We do this for draft and published versions of the Layout to keep the Layout Status fresh and the modified // date updated. if ($options['notify']) { // Notify the Layout $this->getStore()->update(' UPDATE `layout` SET `status` = 3, `modifiedDT` = :modifiedDt WHERE layoutId IN ( SELECT `region`.layoutId FROM `lkplaylistplaylist` INNER JOIN `playlist` ON `playlist`.playlistId = `lkplaylistplaylist`.parentId INNER JOIN `region` ON `region`.regionId = `playlist`.regionId WHERE `lkplaylistplaylist`.childId = :playlistId ) ', [ 'playlistId' => $this->playlistId, 'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()) ]); } // Notify any displays (clearing their cache) // this is typically done when there has been a dynamic change to the Widget - i.e. the Layout doesn't need // to be rebuilt, but the Widget has some change that will be pushed out through getResource if ($options['notifyDisplays']) { $this->getDisplayNotifyService()->collectNow()->notifyByPlaylistId($this->playlistId); } } private function add() { $this->getLog()->debug('Adding Widget ' . $this->type . ' to PlaylistId ' . $this->playlistId); $this->isNew = true; $sql = ' INSERT INTO `widget` (`playlistId`, `ownerId`, `type`, `duration`, `displayOrder`, `useDuration`, `calculatedDuration`, `fromDt`, `toDt`, `createdDt`, `modifiedDt`, `schemaVersion`) VALUES (:playlistId, :ownerId, :type, :duration, :displayOrder, :useDuration, :calculatedDuration, :fromDt, :toDt, :createdDt, :modifiedDt, :schemaVersion) '; $this->widgetId = $this->getStore()->insert($sql, array( 'playlistId' => $this->playlistId, 'ownerId' => $this->ownerId, 'type' => $this->type, 'duration' => $this->duration, 'displayOrder' => $this->displayOrder, 'useDuration' => $this->useDuration, 'calculatedDuration' => $this->calculatedDuration, 'fromDt' => ($this->fromDt == null) ? self::$DATE_MIN : $this->fromDt, 'toDt' => ($this->toDt == null) ? self::$DATE_MAX : $this->toDt, 'createdDt' => ($this->createdDt === null) ? Carbon::now()->format('U') : $this->createdDt, 'modifiedDt' => Carbon::now()->format('U'), 'schemaVersion' => $this->schemaVersion )); } private function update() { $this->getLog()->debug('Saving Widget ' . $this->type . ' on PlaylistId ' . $this->playlistId . ' WidgetId: ' . $this->widgetId); $sql = ' UPDATE `widget` SET `playlistId` = :playlistId, `ownerId` = :ownerId, `type` = :type, `duration` = :duration, `displayOrder` = :displayOrder, `useDuration` = :useDuration, `calculatedDuration` = :calculatedDuration, `fromDt` = :fromDt, `toDt` = :toDt, `modifiedDt` = :modifiedDt, `schemaVersion` = :schemaVersion WHERE `widgetId` = :widgetId '; $params = [ 'playlistId' => $this->playlistId, 'ownerId' => $this->ownerId, 'type' => $this->type, 'duration' => $this->duration, 'widgetId' => $this->widgetId, 'displayOrder' => $this->displayOrder, 'useDuration' => $this->useDuration, 'calculatedDuration' => $this->calculatedDuration, 'fromDt' => ($this->fromDt == null) ? self::$DATE_MIN : $this->fromDt, 'toDt' => ($this->toDt == null) ? self::$DATE_MAX : $this->toDt, 'modifiedDt' => Carbon::now()->format('U'), 'schemaVersion' => $this->schemaVersion ]; $this->getStore()->update($sql, $params); } /** * Link Media */ private function linkMedia() { // Calculate the difference between the current assignments and the original. $mediaToLink = array_diff($this->mediaIds, $this->originalMediaIds); $this->getLog()->debug('Linking %d new media to Widget %d', count($mediaToLink), $this->widgetId); // TODO: Make this more efficient by storing the prepared SQL statement $sql = 'INSERT INTO `lkwidgetmedia` (widgetId, mediaId) VALUES (:widgetId, :mediaId) ON DUPLICATE KEY UPDATE mediaId = :mediaId2'; foreach ($mediaToLink as $mediaId) { $this->getStore()->insert($sql, array( 'widgetId' => $this->widgetId, 'mediaId' => $mediaId, 'mediaId2' => $mediaId )); // audit the media being added $this->getLog()->audit('Media', $mediaId, 'Media added to widget', ['mediaId' => $mediaId, 'widgetId' => $this->widgetId]); } } /** * Unlink Media */ private function unlinkMedia() { // Calculate the difference between the current assignments and the original. $mediaToUnlink = array_diff($this->originalMediaIds, $this->mediaIds); $this->getLog()->debug('Unlinking %d old media from Widget %d', count($mediaToUnlink), $this->widgetId); if (count($mediaToUnlink) <= 0) { return; } // Unlink any media in the collection $params = ['widgetId' => $this->widgetId]; $sql = 'DELETE FROM `lkwidgetmedia` WHERE widgetId = :widgetId AND mediaId IN (0'; $i = 0; foreach ($mediaToUnlink as $mediaId) { $i++; $sql .= ',:mediaId' . $i; $params['mediaId' . $i] = $mediaId; // audit the media being deleted $this->getLog()->audit('Media', $mediaId, 'Media removed from widget', ['mediaId' => $mediaId, 'widgetId' => $this->widgetId]); } $sql .= ')'; $this->getStore()->update($sql, $params); } /** * Returns an array of MediaIds to unlink * * @return array */ private function getMediaIdsToUnlink(): array { // Calculate the difference between the current assignments and the original. $mediaToUnlink = array_diff($this->originalMediaIds, $this->mediaIds); if (count($mediaToUnlink) <= 0) { return []; } // If there is only one mediaId, add it without a suffix if (count($mediaToUnlink) === 1) { $mediaId = reset($mediaToUnlink); return ['mediaId' => $mediaId]; } // More than one mediaId, append a suffix to the key $mediaIdsToUnlink = []; $i = 1; foreach ($mediaToUnlink as $mediaId) { $mediaIdsToUnlink['mediaId_' . $i] = $mediaId; $i++; } return $mediaIdsToUnlink; } }