芝麻web文件管理V1.00
编辑当前文件:/home/mgatv524/public_html/avenida/views/Entity.zip
PK qYwV' Layout.phpnu [ . */ namespace Xibo\Entity; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Xibo\Event\LayoutBuildEvent; use Xibo\Event\LayoutBuildRegionEvent; use Xibo\Exception\DuplicateEntityException; use Xibo\Exception\InvalidArgumentException; use Xibo\Exception\NotFoundException; use Xibo\Exception\XiboException; use Xibo\Factory\CampaignFactory; use Xibo\Factory\DataSetFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\MediaFactory; use Xibo\Factory\ModuleFactory; use Xibo\Factory\PermissionFactory; use Xibo\Factory\PlaylistFactory; use Xibo\Factory\RegionFactory; use Xibo\Factory\TagFactory; use Xibo\Helper\Environment; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; use Xibo\Widget\ModuleWidget; /** * Class Layout * @package Xibo\Entity * * @SWG\Definition() */ class Layout implements \JsonSerializable { use EntityTrait; /** * @SWG\Property( * description="The layoutId" * ) * @var int */ public $layoutId; /** * @var int * @SWG\Property( * description="The userId of the Layout Owner" * ) */ public $ownerId; /** * @var int * @SWG\Property( * description="The id of the Layout's dedicated Campaign" * ) */ public $campaignId; /** * @var int * @SWG\Property( * description="The parentId, if this Layout has a draft" * ) */ public $parentId; /** * @var int * @SWG\Property( * description="The Status Id" * ) */ public $publishedStatusId = 1; /** * @var string * @SWG\Property( * description="The Published Status (Published, Draft or Pending Approval" * ) */ public $publishedStatus; /** * @var string * @SWG\Property( * description="The Published Date" * ) */ public $publishedDate; /** * @var int * @SWG\Property( * description="The id of the image media set as the background" * ) */ public $backgroundImageId; /** * @var int * @SWG\Property( * description="The XLF schema version" * ) */ public $schemaVersion; /** * @var string * @SWG\Property( * description="The name of the Layout" * ) */ public $layout; /** * @var string * @SWG\Property( * description="The description of the Layout" * ) */ public $description; /** * @var string * @SWG\Property( * description="A HEX string representing the Layout background color" * ) */ public $backgroundColor; /** * @var string * @SWG\Property( * description="The datetime the Layout was created" * ) */ public $createdDt; /** * @var string * @SWG\Property( * description="The datetime the Layout was last modified" * ) */ public $modifiedDt; /** * @var int * @SWG\Property( * description="Flag indicating the Layout status" * ) */ public $status; /** * @var int * @SWG\Property( * description="Flag indicating whether the Layout is retired" * ) */ public $retired; /** * @var int * @SWG\Property( * description="The Layer that the background should occupy" * ) */ public $backgroundzIndex; /** * @var double * @SWG\Property( * description="The Layout Width" * ) */ public $width; /** * @var double * @SWG\Property( * description="The Layout Height" * ) */ public $height; /** * @var int * @SWG\Property( * description="If this Layout has been requested by Campaign, then this is the display order of the Layout within the Campaign" * ) */ public $displayOrder; /** * @var int * @SWG\Property( * description="A read-only estimate of this Layout's total duration in seconds. This is equal to the longest region duration and is valid when the layout status is 1 or 2." * ) */ public $duration; /** * @var string * @SWG\Property(description="A status message detailing any errors with the layout") */ public $statusMessage; /** * @var int * @SWG\Property( * description="Flag indicating whether the Layout stat is enabled" * ) */ public $enableStat; /** * @var int * @SWG\Property( * description="Flag indicating whether the default transitions should be applied to this Layout" * ) */ public $autoApplyTransitions; // Child items /** * @SWG\Property(description="An array of Regions belonging to this Layout") * @var Region[] */ public $regions = []; /** * @SWG\Property(description="An array of Tags belonging to this Layout") * @var \Xibo\Entity\Tag[] */ public $tags = []; public $permissions = []; public $campaigns = []; // Read only properties public $owner; public $groupsWithPermissions; public $tagValues; // Private private $unassignTags = []; // Handle empty regions private $hasEmptyRegion = false; public static $loadOptionsMinimum = [ 'loadPlaylists' => false, 'loadTags' => false, 'loadPermissions' => false, 'loadCampaigns' => false ]; public static $saveOptionsMinimum = [ 'saveLayout' => true, 'saveRegions' => false, 'saveTags' => false, 'setBuildRequired' => true, 'validate' => false, 'audit' => false, 'notify' => false ]; /** * @var ConfigServiceInterface */ private $config; /** * @var DateServiceInterface */ private $date; /** @var EventDispatcherInterface */ private $dispatcher; /** * @var PermissionFactory */ private $permissionFactory; /** * @var RegionFactory */ private $regionFactory; /** * @var TagFactory */ private $tagFactory; /** * @var CampaignFactory */ private $campaignFactory; /** * @var LayoutFactory */ private $layoutFactory; /** * @var MediaFactory */ private $mediaFactory; /** * @var ModuleFactory */ private $moduleFactory; /** @var PlaylistFactory */ private $playlistFactory; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param ConfigServiceInterface $config * @param DateServiceInterface $date * @param EventDispatcherInterface $eventDispatcher * @param PermissionFactory $permissionFactory * @param RegionFactory $regionFactory * @param TagFactory $tagFactory * @param CampaignFactory $campaignFactory * @param LayoutFactory $layoutFactory * @param MediaFactory $mediaFactory * @param ModuleFactory $moduleFactory */ public function __construct($store, $log, $config, $date, $eventDispatcher, $permissionFactory, $regionFactory, $tagFactory, $campaignFactory, $layoutFactory, $mediaFactory, $moduleFactory, $playlistFactory) { $this->setCommonDependencies($store, $log); $this->setPermissionsClass('Xibo\Entity\Campaign'); $this->config = $config; $this->date = $date; $this->dispatcher = $eventDispatcher; $this->permissionFactory = $permissionFactory; $this->regionFactory = $regionFactory; $this->tagFactory = $tagFactory; $this->campaignFactory = $campaignFactory; $this->layoutFactory = $layoutFactory; $this->mediaFactory = $mediaFactory; $this->moduleFactory = $moduleFactory; $this->playlistFactory = $playlistFactory; } public function __clone() { // Clear the layout id $this->layoutId = null; $this->campaignId = null; $this->hash = null; $this->permissions = []; // A normal clone (for copy) will set this to Published, so that the copy is published. $this->publishedStatusId = 1; // Clone the regions $this->regions = array_map(function ($object) { return clone $object; }, $this->regions); } /** * @return string */ public function __toString() { $countRegions = is_array($this->regions) ? count($this->regions) : 0; $countTags = is_array($this->tags) ? count($this->tags) : 0; $statusMessages = $this->getStatusMessage(); $countMessages = is_array($statusMessages) ? count($statusMessages) : 0; return sprintf('Layout %s - %d x %d. Regions = %d, Tags = %d. layoutId = %d. Status = %d, messages %d', $this->layout, $this->width, $this->height, $countRegions, $countTags, $this->layoutId, $this->status, $countMessages); } /** * @return string */ private function hash() { return md5($this->layoutId . $this->ownerId . $this->campaignId . $this->backgroundImageId . $this->backgroundColor . $this->width . $this->height . $this->status . $this->description . json_encode($this->statusMessage) . $this->publishedStatusId); } /** * Get the Id * @return int */ public function getId() { return $this->campaignId; } /** * Get the OwnerId * @return int */ public function getOwnerId() { return $this->ownerId; } /** * Sets the Owner of the Layout (including children) * @param int $ownerId * @param bool $cascade Cascade ownership change down to Playlist records */ public function setOwner($ownerId, $cascade = false) { $this->ownerId = $ownerId; $this->load(); foreach ($this->regions as $region) { /* @var Region $region */ $region->setOwner($ownerId, $cascade); } } /** * Set the status of this layout to indicate a build is required */ private function setBuildRequired() { $this->status = 3; } /** * Load Regions from a Layout * @param int $regionId * @return Region * @throws NotFoundException */ public function getRegion($regionId) { foreach ($this->regions as $region) { /* @var Region $region */ if ($region->regionId == $regionId) return $region; } throw new NotFoundException(__('Cannot find region')); } /** * @return bool if this Layout has an empty Region. */ public function hasEmptyRegion() { return $this->hasEmptyRegion; } /** * Get Widgets assigned to this Layout * @return Widget[] * @throws NotFoundException */ public function getWidgets() { $widgets = []; foreach ($this->regions as $region) { $widgets = array_merge($region->getPlaylist()->widgets, $widgets); } return $widgets; } /** * Is this Layout Editable - i.e. are we in a draft state or not. * @return bool true if this layout is editable */ public function isEditable() { return ($this->publishedStatusId === 2); // Draft } /** * Is this Layout a Child? * @return bool */ public function isChild() { return ($this->parentId !== null); } /** * @return array */ public function getStatusMessage() { if ($this->statusMessage === null || empty($this->statusMessage)) return []; if (is_array($this->statusMessage)) return $this->statusMessage; $this->statusMessage = json_decode($this->statusMessage, true); return $this->statusMessage; } /** * Push a new message * @param $message */ public function pushStatusMessage($message) { $this->getStatusMessage(); $this->statusMessage[] = $message; } /** * Clear status message */ private function clearStatusMessage() { $this->statusMessage = null; } /** * Load this Layout * @param array $options * @throws XiboException */ public function load($options = []) { $options = array_merge([ 'loadPlaylists' => true, 'loadTags' => true, 'loadPermissions' => true, 'loadCampaigns' => true ], $options); if ($this->loaded || $this->layoutId == 0) return; $this->getLog()->debug('Loading Layout %d with options %s', $this->layoutId, json_encode($options)); // Load permissions if ($options['loadPermissions']) $this->permissions = $this->permissionFactory->getByObjectId('Xibo\\Entity\\Campaign', $this->campaignId); // Load all regions $this->regions = $this->regionFactory->getByLayoutId($this->layoutId); if ($options['loadPlaylists']) $this->loadPlaylists($options); // Load all tags if ($options['loadTags']) $this->tags = $this->tagFactory->loadByLayoutId($this->layoutId); // Load Campaigns if ($options['loadCampaigns']) $this->campaigns = $this->campaignFactory->getByLayoutId($this->layoutId); // Set the hash $this->hash = $this->hash(); $this->loaded = true; $this->getLog()->debug('Loaded ' . $this->layoutId . ' with hash ' . $this->hash . ', status ' . $this->status); } /** * Load Playlists * @param array $options * @throws XiboException */ public function loadPlaylists($options = []) { foreach ($this->regions as $region) { /* @var Region $region */ $region->load($options); } } /** * Save this Layout * @param array $options * @throws XiboException */ public function save($options = []) { // Default options $options = array_merge([ 'saveLayout' => true, 'saveRegions' => true, 'saveTags' => true, 'setBuildRequired' => true, 'validate' => true, 'notify' => true, 'audit' => true ], $options); if ($options['validate']) $this->validate(); if ($options['setBuildRequired']) $this->setBuildRequired(); $this->getLog()->debug('Saving ' . $this . ' with options ' . json_encode($options, JSON_PRETTY_PRINT)); // New or existing layout if ($this->layoutId == null || $this->layoutId == 0) { $this->add(); if ($options['audit']) { if ($this->parentId === null) { $this->audit($this->layoutId, 'Added', ['layoutId' => $this->layoutId, 'layout' => $this->layout, 'campaignId' => $this->campaignId]); } else { $this->audit($this->layoutId, 'Checked out', ['layoutId' => $this->parentId, 'layout' => $this->layout, 'campaignId' => $this->campaignId]); } } } else if (($this->hash() != $this->hash && $options['saveLayout']) || $options['setBuildRequired']) { $this->update($options); if ($options['audit']) { $change = $this->getChangedProperties(); $change['campaignId'][] = $this->campaignId; if ($this->parentId === null) { $this->audit($this->layoutId, 'Updated', $change); } else { $this->audit($this->layoutId, 'Updated Draft', $change); } } } else { $this->getLog()->info('Save layout properties unchanged for layoutId ' . $this->layoutId . ', status = ' . $this->status); } if ($options['saveRegions']) { $this->getLog()->debug('Saving Regions on ' . $this); // Update the regions foreach ($this->regions as $region) { /* @var Region $region */ // Assert the Layout Id $region->layoutId = $this->layoutId; $region->save($options); } } if ($options['saveTags']) { $this->getLog()->debug('Saving tags on ' . $this); // Save the tags if (is_array($this->tags)) { foreach ($this->tags as $tag) { /* @var Tag $tag */ $this->getLog()->debug('Assigning tag ' . $tag->tag); $tag->assignLayout($this->layoutId); $tag->save(); } } // Remove unwanted ones if (is_array($this->unassignTags)) { foreach ($this->unassignTags as $tag) { /* @var Tag $tag */ $this->getLog()->debug('Unassigning tag ' . $tag->tag); $tag->unassignLayout($this->layoutId); $tag->save(); } } } $this->getLog()->debug('Save finished for ' . $this); } /** * Delete Layout * @param array $options * @throws XiboException */ public function delete($options = []) { // We must ensure everything is loaded before we delete if (!$this->loaded) $this->load(); $this->getLog()->debug('Deleting ' . $this); // We cannot delete the default default if ($this->layoutId == $this->config->getSetting('DEFAULT_LAYOUT')) throw new InvalidArgumentException(__('This layout is used as the global default and cannot be deleted'), 'layoutId'); // Delete our draft if we have one // this is recursive, so be careful! if ($this->parentId === null && $this->publishedStatusId === 2) { try { $draft = $this->layoutFactory->getByParentId($this->layoutId); $draft->delete(['notify' => false]); } catch (NotFoundException $notFoundException) { $this->getLog()->info('No draft to delete for a Layout in the Draft state, odd!'); } } // Unassign all Tags foreach ($this->tags as $tag) { /* @var Tag $tag */ $tag->unassignLayout($this->layoutId); $tag->save(); } // Delete Regions foreach ($this->regions as $region) { /* @var Region $region */ $region->delete($options); } // If we are the top level parent we also delete objects that sit on the top-level if ($this->parentId === null) { // Delete Permissions foreach ($this->permissions as $permission) { /* @var Permission $permission */ $permission->deleteAll(); } // Delete widget history $this->getStore()->update('DELETE FROM `widgethistory` WHERE layoutHistoryId IN (SELECT layoutHistoryId FROM `layouthistory` WHERE campaignId = :campaignId)', ['campaignId' => $this->campaignId]); // Delete layout history $this->getStore()->update('DELETE FROM `layouthistory` WHERE campaignId = :campaignId', ['campaignId' => $this->campaignId]); // Unassign from all Campaigns foreach ($this->campaigns as $campaign) { /* @var Campaign $campaign */ $campaign->setChildObjectDependencies($this->layoutFactory); $campaign->unassignLayout($this, true); $campaign->save(['validate' => false]); } // Delete our own Campaign $campaign = $this->campaignFactory->getById($this->campaignId); $campaign->setChildObjectDependencies($this->layoutFactory); $campaign->delete(); // Remove the Layout from any display defaults $this->getStore()->update('UPDATE `display` SET defaultlayoutid = :defaultLayoutId WHERE defaultlayoutid = :layoutId', [ 'layoutId' => $this->layoutId, 'defaultLayoutId' => $this->config->getSetting('DEFAULT_LAYOUT') ]); // Remove any display group links $this->getStore()->update('DELETE FROM `lklayoutdisplaygroup` WHERE layoutId = :layoutId', ['layoutId' => $this->layoutId]); } else { // Remove the draft from any Campaign assignments $this->getStore()->update('DELETE FROM `lkcampaignlayout` WHERE layoutId = :layoutId', ['layoutId' => $this->layoutId]); } // Remove the Layout (now it is orphaned it can be deleted safely) $this->getStore()->update('DELETE FROM `layout` WHERE layoutid = :layoutId', array('layoutId' => $this->layoutId)); $this->getLog()->audit('Layout', $this->layoutId, 'Layout Deleted', ['layoutId' => $this->layoutId]); // Delete the cached file (if there is one) if (file_exists($this->getCachePath())) @unlink($this->getCachePath()); // Audit the Delete $this->audit($this->layoutId, 'Deleted' . (($this->parentId !== null) ? ' draft for ' . $this->parentId : '')); } /** * Validate this layout * @throws XiboException */ public function validate() { // We must provide either a template or a resolution if ($this->width == 0 || $this->height == 0) throw new InvalidArgumentException(__('The layout dimensions cannot be empty'), 'width/height'); // Validation if (strlen($this->layout) > 50 || strlen($this->layout) < 1) throw new InvalidArgumentException(__("Layout Name must be between 1 and 50 characters"), 'name'); if (strlen($this->description) > 254) throw new InvalidArgumentException(__("Description can not be longer than 254 characters"), 'description'); // Check for duplicates // exclude our own duplicate (if we're a draft) $duplicates = $this->layoutFactory->query(null, [ 'userId' => $this->ownerId, 'layoutExact' => $this->layout, 'notLayoutId' => ($this->parentId !== null) ? $this->parentId : $this->layoutId, 'disableUserCheck' => 1, 'excludeTemplates' => -1 ]); if (count($duplicates) > 0) { throw new DuplicateEntityException(sprintf(__("You already own a Layout called '%s'. Please choose another name."), $this->layout)); } // Check zindex is positive if ($this->backgroundzIndex < 0) { throw new InvalidArgumentException(__('Layer must be 0 or a positive number'), 'backgroundzIndex'); } } /** * Does the layout have the provided tag? * @param $searchTag * @return bool */ public function hasTag($searchTag) { $this->load(); foreach ($this->tags as $tag) { /* @var Tag $tag */ if ($tag->tag == $searchTag) return true; } return false; } /** * Assign Tag * @param Tag $tag * @return $this */ public function assignTag($tag) { $this->load(); if ($this->tags != [$tag]) { if (!in_array($tag, $this->tags)) { $this->tags[] = $tag; } } else { $this->getLog()->debug('No Tags to assign'); } return $this; } /** * Add layout history * this is called when a new Layout is added, and when a Draft Layout is published * we can therefore expect to always have a Layout History record for a Layout */ private function addLayoutHistory() { $this->getLog()->debug('Adding Layout History record for ' . $this->layoutId); // Add a record in layout history when a layout is added or published $this->getStore()->insert(' INSERT INTO `layouthistory` (campaignId, layoutId, publishedDate) VALUES (:campaignId, :layoutId, :publishedDate) ', [ 'campaignId' => $this->campaignId, 'layoutId' => $this->layoutId, 'publishedDate' => $this->date->parse()->format('Y-m-d H:i:s') ]); } /** * Add Widget History * this should be called when the contents of a Draft Layout are destroyed during the publish process * it preserves the current state of widgets before they are removed from the database * that can then be used for proof of play stats, to get back to the original widget name/type and mediaId * @param \Xibo\Entity\Layout $parent * @throws \Xibo\Exception\NotFoundException */ private function addWidgetHistory($parent) { // Get the most recent layout history record $layoutHistoryId = $this->getStore()->select(' SELECT layoutHistoryId FROM `layouthistory` WHERE layoutId = :layoutId ', [ 'layoutId' => $parent->layoutId ]); if (count($layoutHistoryId) <= 0) { // We are missing the parent layout history record, which isn't good. // I think all we can do at this stage is log it $this->getLog()->alert('Missing Layout History for layoutId ' . $parent->layoutId . ' which is on campaignId ' . $parent->campaignId); return; } $layoutHistoryId = intval($layoutHistoryId[0]['layoutHistoryId']); // Add records in the widget history table representing all widgets on this Layout foreach ($parent->getWidgets() as $widget) { // Does this widget have a mediaId $mediaId = null; try { $mediaId = $widget->getPrimaryMediaId(); } catch (NotFoundException $notFoundException) { // this is fine } $this->getStore()->insert(' INSERT INTO `widgethistory` (layoutHistoryId, widgetId, mediaId, type, name) VALUES (:layoutHistoryId, :widgetId, :mediaId, :type, :name); ', [ 'layoutHistoryId' => $layoutHistoryId, 'widgetId' => $widget->widgetId, 'mediaId' => $mediaId, 'type' => $widget->type, 'name' => $widget->getOptionValue('name', null), ]); } } /** * Unassign tag * @param Tag $tag * @return $this * @throws NotFoundException * @throws XiboException */ public function unassignTag($tag) { $this->load(); $this->tags = array_udiff($this->tags, [$tag], function($a, $b) { /* @var Tag $a */ /* @var Tag $b */ return $a->tagId - $b->tagId; }); $this->unassignTags[] = $tag; $this->getLog()->debug('Tags after removal %s', json_encode($this->tags)); return $this; } /** * @param array[Tag] $tags */ public function replaceTags($tags = []) { if (!is_array($this->tags) || count($this->tags) <= 0) $this->tags = $this->tagFactory->loadByLayoutId($this->layoutId); if ($this->tags != $tags) { $this->unassignTags = array_udiff($this->tags, $tags, function ($a, $b) { /* @var Tag $a */ /* @var Tag $b */ return $a->tagId - $b->tagId; }); $this->getLog()->debug('Tags to be removed: %s', json_encode($this->unassignTags)); // Replace the arrays $this->tags = $tags; $this->getLog()->debug('Tags remaining: %s', json_encode($this->tags)); } else { $this->getLog()->debug('Tags were not changed'); } } /** * Export the Layout as its XLF * @return string * @throws InvalidArgumentException * @throws NotFoundException * @throws XiboException */ public function toXlf() { $this->getLog()->debug('Layout toXLF for Layout ' . $this->layout . ' - ' . $this->layoutId); $this->load(['loadPlaylists' => true]); // Keep track of whether this layout has an empty region $this->hasEmptyRegion = false; $layoutCountRegionsWithDuration = 0; $document = new \DOMDocument(); $layoutNode = $document->createElement('layout'); $layoutNode->setAttribute('width', $this->width); $layoutNode->setAttribute('height', $this->height); $layoutNode->setAttribute('bgcolor', $this->backgroundColor); $layoutNode->setAttribute('schemaVersion', $this->schemaVersion); // Layout stat collection flag if (is_null($this->enableStat)) { $layoutEnableStat = $this->config->getSetting('LAYOUT_STATS_ENABLED_DEFAULT'); $this->getLog()->debug('Layout enableStat is empty. Get the default setting.'); } else { $layoutEnableStat = $this->enableStat; } $layoutNode->setAttribute('enableStat', $layoutEnableStat); // Only set the z-index if present if ($this->backgroundzIndex != 0) $layoutNode->setAttribute('zindex', $this->backgroundzIndex); if ($this->backgroundImageId != 0) { // Get stored as $media = $this->mediaFactory->getById($this->backgroundImageId); $layoutNode->setAttribute('background', $media->storedAs); } $document->appendChild($layoutNode); // Track module status within the layout $status = 0; $this->clearStatusMessage(); foreach ($this->regions as $region) { /* @var Region $region */ $regionNode = $document->createElement('region'); $regionNode->setAttribute('id', $region->regionId); $regionNode->setAttribute('width', $region->width); $regionNode->setAttribute('height', $region->height); $regionNode->setAttribute('top', $region->top); $regionNode->setAttribute('left', $region->left); // Only set the zIndex if present if ($region->zIndex != 0) $regionNode->setAttribute('zindex', $region->zIndex); $layoutNode->appendChild($regionNode); // Region Duration $region->duration = 0; // Region Options $regionOptionsNode = $document->createElement('options'); foreach ($region->regionOptions as $regionOption) { $regionOptionNode = $document->createElement($regionOption->option, $regionOption->value); $regionOptionsNode->appendChild($regionOptionNode); } $regionNode->appendChild($regionOptionsNode); // Store region look to work out duration calc $regionLoop = $region->getOptionValue('loop', 0); // Get a count of widgets in this region $widgets = $region->getPlaylist()->setModuleFactory($this->moduleFactory)->expandWidgets(); $countWidgets = count($widgets); if ($countWidgets <= 0) { $this->getLog()->info('Layout has empty region - ' . $countWidgets . ' widgets. playlistId = ' . $region->getPlaylist()->getId()); $this->hasEmptyRegion = true; } // Work out if we have any "lead regions", those are Widgets with a duration foreach ($widgets as $widget) { if ($widget->useDuration == 1 || $countWidgets > 1 || $regionLoop == 1 || $widget->type == 'video') { $layoutCountRegionsWithDuration++; } } foreach ($widgets as $widget) { /* @var Widget $widget */ $module = $this->moduleFactory->createWithWidget($widget, $region); // Set the Layout Status try { $moduleStatus = $module->isValid(); if ($module->hasStatusMessage()) { $this->pushStatusMessage($module->getStatusMessage()); } } catch (XiboException $xiboException) { $moduleStatus = ModuleWidget::$STATUS_INVALID; // Include the exception on $this->pushStatusMessage($xiboException->getMessage()); } $status = ($moduleStatus > $status) ? $moduleStatus : $status; // Determine the duration of this widget // the calculated duration contains the best guess at this duration from the playlist's perspective // the only time we want to override this, is if we want it set to the Minimum Duration for the XLF $widgetDuration = $widget->calculatedDuration; // Is this Widget one that does not have a duration of its own? // Assuming we have at least 1 region with a set duration, then we ought to // Reset to the minimum duration if ($widget->useDuration == 0 && $countWidgets <= 1 && $regionLoop == 0 && $widget->type != 'video' && $layoutCountRegionsWithDuration >= 1 ) { // Make sure this Widget expires immediately so that the other Regions can be the leaders when // it comes to expiring the Layout $widgetDuration = Widget::$widgetMinDuration; } // Region duration $region->duration = $region->duration + $widget->calculatedDuration; // We also want to add any transition OUT duration // only the OUT duration because IN durations do not get added to the widget duration by the player // https://github.com/xibosignage/xibo/issues/705 if ($widget->getOptionValue('transOut', '') != '') { // Transition durations are in milliseconds $region->duration = $region->duration + ($widget->getOptionValue('transOutDuration', 0) / 1000); } // Create media xml node for XLF. $renderAs = $module->getModule()->renderAs; $mediaNode = $document->createElement('media'); $mediaNode->setAttribute('id', $widget->widgetId); $mediaNode->setAttribute('type', $widget->type); $mediaNode->setAttribute('render', ($renderAs == '') ? 'native' : $renderAs); // Set the duration according to whether we are using widget duration or not $mediaNode->setAttribute('duration', $widgetDuration); $mediaNode->setAttribute('useDuration', $widget->useDuration); // Set a from/to date if ($widget->fromDt != null || $widget->fromDt === Widget::$DATE_MIN) { $mediaNode->setAttribute('fromDt', $this->date->getLocalDate($this->date->parse($widget->fromDt, 'U'))); } if ($widget->toDt != null || $widget->toDt === Widget::$DATE_MAX) { $mediaNode->setAttribute('toDt', $this->date->getLocalDate($this->date->parse($widget->toDt, 'U'))); } // Logic Table // // Widget With Media // LAYOUT MEDIA WIDGET Media stats collected? // ON ON ON YES Widget takes precedence // Match - 1 // ON OFF ON YES Widget takes precedence // Match - 1 // ON INHERIT ON YES Widget takes precedence // Match - 1 // // OFF ON ON YES Widget takes precedence // Match - 1 // OFF OFF ON YES Widget takes precedence // Match - 1 // OFF INHERIT ON YES Widget takes precedence // Match - 1 // // ON ON OFF NO Widget takes precedence // Match - 2 // ON OFF OFF NO Widget takes precedence // Match - 2 // ON INHERIT OFF NO Widget takes precedence // Match - 2 // // OFF ON OFF NO Widget takes precedence // Match - 2 // OFF OFF OFF NO Widget takes precedence // Match - 2 // OFF INHERIT OFF NO Widget takes precedence // Match - 2 // // ON ON INHERIT YES Media takes precedence // Match - 3 // ON OFF INHERIT NO Media takes precedence // Match - 4 // ON INHERIT INHERIT YES Media takes precedence and Inherited from Layout // Match - 5 // // OFF ON INHERIT YES Media takes precedence // Match - 3 // OFF OFF INHERIT NO Media takes precedence // Match - 4 // OFF INHERIT INHERIT NO Media takes precedence and Inherited from Layout // Match - 6 // // Widget Without Media // LAYOUT WIDGET Widget stats collected? // ON ON YES Widget takes precedence // Match - 1 // ON OFF NO Widget takes precedence // Match - 2 // ON INHERIT YES Inherited from Layout // Match - 7 // OFF ON YES Widget takes precedence // Match - 1 // OFF OFF NO Widget takes precedence // Match - 2 // OFF INHERIT NO Inherited from Layout // Match - 8 // Widget stat collection flag $widgetEnableStat = $widget->getOptionValue('enableStat', $this->config->getSetting('WIDGET_STATS_ENABLED_DEFAULT')); if(($widgetEnableStat === null) || ($widgetEnableStat === "")) { $widgetEnableStat = $this->config->getSetting('WIDGET_STATS_ENABLED_DEFAULT'); } $enableStat = 0; // Match - 0 if ($widgetEnableStat == 'On') { $enableStat = 1; // Match - 1 $this->getLog()->debug('For '.$widget->widgetId.': Layout '. (($layoutEnableStat == 1) ? 'On': 'Off') . ' Widget '.$widgetEnableStat . '. Media node output '. $enableStat); } else if ($widgetEnableStat == 'Off') { $enableStat = 0; // Match - 2 $this->getLog()->debug('For '.$widget->widgetId.': Layout '. (($layoutEnableStat == 1) ? 'On': 'Off') . ' Widget '.$widgetEnableStat . '. Media node output '. $enableStat); } else if ($widgetEnableStat == 'Inherit') { try { // Media enable stat flag - WIDGET WITH MEDIA $media = $this->mediaFactory->getById($widget->getPrimaryMediaId()); if (($media->enableStat === null) || ($media->enableStat === "")) { $mediaEnableStat = $this->config->getSetting('MEDIA_STATS_ENABLED_DEFAULT'); $this->getLog()->debug('Media enableStat is empty. Get the default setting.'); } else { $mediaEnableStat = $media->enableStat; } if ($mediaEnableStat == 'On') { $enableStat = 1; // Match - 3 } else if ($mediaEnableStat == 'Off') { $enableStat = 0; // Match - 4 } else if ($mediaEnableStat == 'Inherit') { $enableStat = $layoutEnableStat; // Match - 5 and 6 } $this->getLog()->debug('For '.$widget->widgetId.': Layout '. (($layoutEnableStat == 1) ? 'On': 'Off') . ((isset($mediaEnableStat)) ? (' Media '.$mediaEnableStat) : '') . ' Widget '.$widgetEnableStat . '. Media node output '. $enableStat); } catch (\Exception $e) { // - WIDGET WITHOUT MEDIA $this->getLog()->debug($widget->widgetId. ' is not a library media and does not have a media id.'); $enableStat = $layoutEnableStat; // Match - 7 and 8 $this->getLog()->debug('For '.$widget->widgetId.': Layout '. (($layoutEnableStat == 1) ? 'On': 'Off') . ' Widget '.$widgetEnableStat . '. Media node output '. $enableStat); } } // automatically set the transitions on the layout xml, we are not saving widgets here to avoid deadlock issues. if ($this->autoApplyTransitions == 1) { $widgetTransIn = $widget->getOptionValue('transIn', $this->config->getSetting('DEFAULT_TRANSITION_IN')); $widgetTransOut = $widget->getOptionValue('transOut', $this->config->getSetting('DEFAULT_TRANSITION_OUT')); $widgetTransInDuration = $widget->getOptionValue('transInDuration', $this->config->getSetting('DEFAULT_TRANSITION_DURATION')); $widgetTransOutDuration = $widget->getOptionValue('transOutDuration', $this->config->getSetting('DEFAULT_TRANSITION_DURATION')); $widget->setOptionValue('transIn', 'attrib', $widgetTransIn); $widget->setOptionValue('transInDuration', 'attrib', $widgetTransInDuration); $widget->setOptionValue('transOut', 'attrib', $widgetTransOut); $widget->setOptionValue('transOutDuration', 'attrib', $widgetTransOutDuration); } // Set enable stat collection flag $mediaNode->setAttribute('enableStat', $enableStat); // Create options nodes $optionsNode = $document->createElement('options'); $rawNode = $document->createElement('raw'); $mediaNode->appendChild($optionsNode); $mediaNode->appendChild($rawNode); // Inject the URI $uriInjected = false; if ($module->getModule()->regionSpecific == 0) { $media = $this->mediaFactory->getById($widget->getPrimaryMediaId()); $optionNode = $document->createElement('uri', $media->storedAs); $optionsNode->appendChild($optionNode); $uriInjected = true; // Add the fileId attribute to the media element $mediaNode->setAttribute('fileId', $media->mediaId); } // Tracker whether or not we have an updateInterval configured. $hasUpdatedInterval = false; foreach ($widget->widgetOptions as $option) { if (trim($option->value) === '') continue; if ($option->type == 'cdata') { $optionNode = $document->createElement($option->option); $cdata = $document->createCDATASection($option->value); $optionNode->appendChild($cdata); $rawNode->appendChild($optionNode); } else if ($option->type == 'attrib' || $option->type == 'attribute') { if ($uriInjected && $option->option == 'uri') continue; $optionNode = $document->createElement($option->option, $option->value); $optionsNode->appendChild($optionNode); } if ($option->option === 'updateInterval') { $hasUpdatedInterval = true; } } // If we do not have an update interval, should we set a default one? // https://github.com/xibosignage/xibo/issues/2319 if (!$hasUpdatedInterval && $module->getModule()->regionSpecific == 1) { // For the moment we will assume that all update intervals are the same as the cache duration // remembering that the cache duration is in seconds and the updateInterval in minutes. $optionsNode->appendChild( $document->createElement('updateInterval', $module->getCacheDuration() / 60) ); } // Handle associated audio $audioNodes = null; foreach ($widget->audio as $audio) { /** @var WidgetAudio $audio */ if ($audioNodes == null) $audioNodes = $document->createElement('audio'); // Get the full media node for this audio element $audioMedia = $this->mediaFactory->getById($audio->mediaId); $audioNode = $document->createElement('uri', $audioMedia->storedAs); $audioNode->setAttribute('volume', $audio->volume); $audioNode->setAttribute('loop', $audio->loop); $audioNode->setAttribute('mediaId', $audio->mediaId); $audioNodes->appendChild($audioNode); } if ($audioNodes != null) $mediaNode->appendChild($audioNodes); $regionNode->appendChild($mediaNode); } $this->getLog()->debug('Region duration on layout ' . $this->layoutId . ' is ' . $region->duration . '. Comparing to ' . $this->duration); // Track the max duration within the layout // Test this duration against the layout duration if ($this->duration < $region->duration) $this->duration = $region->duration; $event = new LayoutBuildRegionEvent($region->regionId, $regionNode); $this->dispatcher->dispatch($event::NAME, $event); // End of region loop. } $this->getLog()->debug('Setting Layout Duration to ' . $this->duration); $tagsNode = $document->createElement('tags'); foreach ($this->tags as $tag) { /* @var Tag $tag */ $tagNode = $document->createElement('tag', $tag->tag); $tagsNode->appendChild($tagNode); } $layoutNode->appendChild($tagsNode); // Update the layout status / duration accordingly $this->status = ($status < $this->status) ? $status : $this->status; // Fire a layout.build event, passing the layout and the generated document. $event = new LayoutBuildEvent($this, $document); $this->dispatcher->dispatch($event::NAME, $event); return $document->saveXML(); } /** * Export the Layout as a ZipArchive * @param DataSetFactory $dataSetFactory * @param string $fileName * @param array $options * @throws InvalidArgumentException * @throws XiboException */ public function toZip($dataSetFactory, $fileName, $options = []) { $options = array_merge([ 'includeData' => false ], $options); // Load the complete layout $this->load(); // We export to a ZIP file $zip = new \ZipArchive(); $result = $zip->open($fileName, \ZipArchive::CREATE | \ZipArchive::OVERWRITE); if ($result !== true) throw new InvalidArgumentException(__('Can\'t create ZIP. Error Code: ' . $result), 'fileName'); // Add a mapping file for the region names $regionMapping = []; foreach ($this->regions as $region) { /** @var Region $region */ $regionMapping[$region->regionId] = $region->name; } // Add layout information to the ZIP $zip->addFromString('layout.json', json_encode([ 'layout' => $this->layout, 'description' => $this->description, 'regions' => $regionMapping, 'layoutDefinitions' => $this ])); // Add the layout XLF $zip->addFile($this->xlfToDisk(), 'layout.xml'); // Add all media $libraryLocation = $this->config->getSetting('LIBRARY_LOCATION'); $mappings = []; foreach ($this->mediaFactory->getByLayoutId($this->layoutId, 1) as $media) { /* @var Media $media */ $zip->addFile($libraryLocation . $media->storedAs, 'library/' . $media->fileName); $mappings[] = [ 'file' => $media->fileName, 'mediaid' => $media->mediaId, 'name' => $media->name, 'type' => $media->mediaType, 'duration' => $media->duration, 'background' => 0, 'font' => 0 ]; } // Add the background image if ($this->backgroundImageId != 0) { $media = $this->mediaFactory->getById($this->backgroundImageId); $zip->addFile($libraryLocation . $media->storedAs, 'library/' . $media->fileName); $mappings[] = [ 'file' => $media->fileName, 'mediaid' => $media->mediaId, 'name' => $media->name, 'type' => $media->mediaType, 'duration' => $media->duration, 'background' => 1, 'font' => 0 ]; } // Add any fonts // parse the XLF file for any font declarations contains therein // get those font media files by name and add them to the zip $fonts = null; preg_match_all('/font-family:(.*?);/', $this->toXlf(), $fonts); if ($fonts != null) { $this->getLog()->debug('Matched fonts: %s', json_encode($fonts)); foreach ($fonts[1] as $font) { $matches = $this->mediaFactory->query(null, array('disableUserCheck' => 1, 'nameExact' => $font, 'allModules' => 1, 'type' => 'font')); if (count($matches) <= 0) { $this->getLog()->info('Unmatched font during export: %s', $font); continue; } $media = $matches[0]; $zip->addFile($libraryLocation . $media->storedAs, 'library/' . $media->fileName); $mappings[] = [ 'file' => $media->fileName, 'mediaid' => $media->mediaId, 'name' => $media->name, 'type' => $media->mediaType, 'duration' => $media->duration, 'background' => 0, 'font' => 1 ]; } } // Add the mappings file to the ZIP $zip->addFromString('mapping.json', json_encode($mappings)); // Handle any DataSet structures $dataSetIds = []; $dataSets = []; // Playlists $playlistMappings = []; $playlistDefinitions = []; $nestedPlaylistDefinitions = []; foreach ($this->getWidgets() as $widget) { /** @var Widget $widget */ if ($widget->type == 'datasetview' || $widget->type == 'datasetticker' || $widget->type == 'chart') { $dataSetId = $widget->getOptionValue('dataSetId', 0); if ($dataSetId != 0) { if (in_array($dataSetId, $dataSetIds)) continue; // Export the structure for this dataSet $dataSet = $dataSetFactory->getById($dataSetId); $dataSet->load(); // Are we also looking to export the data? if ($options['includeData']) { $dataSet->data = $dataSet->getData([], ['includeFormulaColumns' => false]); } $dataSetIds[] = $dataSet->dataSetId; $dataSets[] = $dataSet; } } elseif ($widget->type == 'subplaylist') { $playlistIds = json_decode($widget->getOptionValue('subPlaylistIds', []), true); foreach ($playlistIds as $playlistId) { $count = 1; $playlist = $this->playlistFactory->getById($playlistId); $playlist->load(); $playlist->expandWidgets(0, false); $playlistDefinitions[$playlist->playlistId] = $playlist; // this is a recursive function, we are adding Playlist definitions, Playlist mappings and DataSets existing on the nested Playlist. $playlist->generatePlaylistMapping($playlist->widgets, $playlist->playlistId,$playlistMappings, $count, $nestedPlaylistDefinitions, $dataSetIds, $dataSets, $dataSetFactory, $options['includeData']); } } } // Add the mappings file to the ZIP if ($dataSets != []) { $zip->addFromString('dataSet.json', json_encode($dataSets, JSON_PRETTY_PRINT)); } // Add the Playlist definitions to the ZIP if ($playlistDefinitions != []) { $zip->addFromString('playlist.json', json_encode($playlistDefinitions, JSON_PRETTY_PRINT)); } // Add the nested Playlist definitions to the ZIP if ($nestedPlaylistDefinitions != []) { $zip->addFromString('nestedPlaylist.json', json_encode($nestedPlaylistDefinitions, JSON_PRETTY_PRINT)); } // Add Playlist mappings file to the ZIP if ($playlistMappings != []) { $zip->addFromString('playlistMappings.json', json_encode($playlistMappings, JSON_PRETTY_PRINT)); } $zip->close(); } /** * Save the XLF to disk if necessary * @param array $options * @return string the path * @throws InvalidArgumentException * @throws NotFoundException * @throws XiboException */ public function xlfToDisk($options = []) { $options = array_merge([ 'notify' => true, 'collectNow' => true, 'exceptionOnError' => false, 'exceptionOnEmptyRegion' => true ], $options); $path = $this->getCachePath(); if ($this->status == 3 || !file_exists($path)) { $this->getLog()->debug('XLF needs building for Layout ' . $this->layoutId); $this->load(['loadPlaylists' => true]); // Layout auto Publish if ($this->config->getSetting('DEFAULT_LAYOUT_AUTO_PUBLISH_CHECKB') == 1 && $this->isChild()) { // we are editing a draft layout, the published date is set on the original layout, therefore we need our parent. $parent = $this->layoutFactory->loadById($this->parentId); $layoutCurrentPublishedDate = $this->date->parse($parent->publishedDate); $newPublishDateString = $this->date->getLocalDate($this->date->parse()->addMinutes(30), 'Y-m-d H:i:s'); $newPublishDate = $this->date->parse($newPublishDateString); if ($layoutCurrentPublishedDate->format('U') > $newPublishDate->format('U')) { // Layout is set to Publish manually on a date further than 30 min from now, we don't touch it in this case. $this->getLog()->debug('Layout is set to Publish manually on a date further than 30 min from now, do not update'); } elseif ($parent->publishedDate != null && $layoutCurrentPublishedDate->format('U') < $this->date->getLocalDate($this->date->parse()->subMinutes(5), 'U')) { // Layout is set to Publish manually at least 5 min in the past at the moment, we expect the Regular Maintenance to build it before that happens $this->getLog()->debug('Layout should be built by Regular Maintenance'); } else { $parent->setPublishedDate($newPublishDateString); $this->getLog()->debug('Layout set to automatically Publish on ' . $newPublishDateString); } } // Assume error $this->status = ModuleWidget::$STATUS_INVALID; // Reset duration $this->duration = 0; // Save the resulting XLF try { file_put_contents($path, $this->toXlf()); } catch (\Exception $e) { $this->getLog()->error('Cannot build Layout ' . $this->layoutId . '. error: ' . $e->getMessage()); // Will continue and save the status as 4 $this->status = ModuleWidget::$STATUS_INVALID; if ($e->getMessage() != '') { $this->pushStatusMessage($e->getMessage()); } else { $this->pushStatusMessage('Unexpected Error'); } // No need to notify on an errored build $options['notify'] = false; } if ($options['exceptionOnError']) { // Handle exception cases if ($this->status === ModuleWidget::$STATUS_INVALID || ($options['exceptionOnEmptyRegion'] && $this->hasEmptyRegion()) ) { $this->audit($this->layoutId, 'Publish layout failed, rollback', ['layoutId' => $this->layoutId]); throw new InvalidArgumentException(__('There is an error with this Layout: %s', implode(',', $this->getStatusMessage())), 'status'); } } // If we have an empty region and we've not exceptioned, then we need to record that in our status if ($this->hasEmptyRegion()) { $this->status = ModuleWidget::$STATUS_INVALID; $this->pushStatusMessage(__('Empty Region')); } $this->save([ 'saveRegions' => true, 'saveRegionOptions' => false, 'manageRegionAssignments' => false, 'saveTags' => false, 'setBuildRequired' => false, 'audit' => false, 'validate' => false, 'notify' => $options['notify'], 'collectNow' => $options['collectNow'] ]); } return $path; } /** * @return string */ private function getCachePath() { $libraryLocation = $this->config->getSetting('LIBRARY_LOCATION'); return $libraryLocation . $this->layoutId . '.xlf'; } /** * Publish the Draft * @throws XiboException */ public function publishDraft() { // We are the draft - make sure we have a parent if (!$this->isChild()) throw new InvalidArgumentException(__('Not a Draft'), 'statusId'); // Get my parent for later $parent = $this->layoutFactory->loadById($this->parentId); // I am the draft, so I clear my parentId, and set the parentId of my parent, to myself (swapping us) // Make me the parent. $this->getStore()->update('UPDATE `layout` SET parentId = NULL WHERE layoutId = :layoutId', [ 'layoutId' => $this->layoutId ]); // Set my parent, to be my child. $this->getStore()->update('UPDATE `layout` SET parentId = :parentId WHERE layoutId = :layoutId', [ 'parentId' => $this->layoutId, 'layoutId' => $this->parentId ]); // clear publishedDate $this->getStore()->update('UPDATE `layout` SET publishedDate = null WHERE layoutId = :layoutId', [ 'layoutId' => $this->layoutId ]); // Update any campaign links $this->getStore()->update(' UPDATE `lkcampaignlayout` SET layoutId = :layoutId WHERE layoutId = :parentId AND campaignId IN (SELECT campaignId FROM campaign WHERE isLayoutSpecific = 0) ', [ 'parentId' => $this->parentId, 'layoutId' => $this->layoutId ]); // Persist things that might have changed // NOTE: permissions are managed on the campaign, so we do not need to worry. $this->layout = $parent->layout; $this->description = $parent->description; $this->retired = $parent->retired; $this->enableStat = $parent->enableStat; // Swap all tags over, any changes we've made to the parents tags should be moved to the child. $this->getStore()->update('UPDATE `lktaglayout` SET layoutId = :layoutId WHERE layoutId = :parentId', [ 'parentId' => $parent->layoutId, 'layoutId' => $this->layoutId ]); // Update any Displays which use this as their default Layout $this->getStore()->update('UPDATE `display` SET defaultLayoutId = :layoutId WHERE defaultLayoutId = :parentId', [ 'parentId' => $parent->layoutId, 'layoutId' => $this->layoutId ]); // Swap any display group links $this->getStore()->update('UPDATE `lklayoutdisplaygroup` SET layoutId = :layoutId WHERE layoutId = :parentId', [ 'layoutId' => $this->layoutId, 'parentId' => $parent->layoutId ]); // If this is the global default layout, then add some special handling to make sure we swap the default over // to the incoming draft if ($this->parentId == $this->config->getSetting('DEFAULT_LAYOUT')) { // Change it over to me. $this->config->changeSetting('DEFAULT_LAYOUT', $this->layoutId); } // Preserve the widget information $this->addWidgetHistory($parent); // Delete the parent (make sure we set the parent to be a child of us, otherwise we will delete the linked // campaign $parent->parentId = $this->layoutId; $parent->tags = []; // Clear the tags so we don't attempt a delete. $parent->permissions = []; // Clear the permissions so we don't attempt a delete $parent->delete(); // Set my statusId to published // we do not want to notify here as we should wait for the build to happen $this->publishedStatusId = 1; $this->save([ 'saveLayout' => true, 'saveRegions' => false, 'saveTags' => false, 'setBuildRequired' => true, 'validate' => false, 'audit' => true, 'notify' => false ]); // Nullify my parentId (I no longer have a parent) $this->parentId = null; // Add a layout history $this->addLayoutHistory(); } public function setPublishedDate($publishedDate) { $this->publishedDate = $publishedDate; $this->getStore()->update('UPDATE `layout` SET publishedDate = :publishedDate WHERE layoutId = :layoutId', [ 'layoutId' => $this->layoutId, 'publishedDate' => $this->publishedDate ]); } /** * Discard the Draft * @throws XiboException */ public function discardDraft() { // We are the draft - make sure we have a parent if (!$this->isChild()) { $this->getLog()->debug('Cant discard draft ' . $this->layoutId . '. publishedStatusId = ' . $this->publishedStatusId . ', parentId = ' . $this->parentId); throw new InvalidArgumentException(__('Not a Draft'), 'statusId'); } // We just need to delete ourselves really $this->delete(); // We also need to update the parent so that it is no longer draft $parent = $this->layoutFactory->getById($this->parentId); $parent->publishedStatusId = 1; $parent->save([ self::$saveOptionsMinimum ]); } // // Add / Update // /** * Add * @throws XiboException */ private function add() { $this->getLog()->debug('Adding Layout' . $this->layout); $sql = 'INSERT INTO layout (layout, description, userID, createdDT, modifiedDT, publishedStatusId, status, width, height, schemaVersion, backgroundImageId, backgroundColor, backgroundzIndex, parentId, enableStat, duration, autoApplyTransitions) VALUES (:layout, :description, :userid, :createddt, :modifieddt, :publishedStatusId, :status, :width, :height, :schemaVersion, :backgroundImageId, :backgroundColor, :backgroundzIndex, :parentId, :enableStat, 0, :autoApplyTransitions)'; $time = $this->date->getLocalDate(); $this->layoutId = $this->getStore()->insert($sql, array( 'layout' => $this->layout, 'description' => $this->description, 'userid' => $this->ownerId, 'createddt' => $time, 'modifieddt' => $time, 'publishedStatusId' => $this->publishedStatusId, // Default to 1 (published) 'status' => 3, 'width' => $this->width, 'height' => $this->height, 'schemaVersion' => Environment::$XLF_VERSION, 'backgroundImageId' => $this->backgroundImageId, 'backgroundColor' => $this->backgroundColor, 'backgroundzIndex' => $this->backgroundzIndex, 'parentId' => ($this->parentId == null) ? null : $this->parentId, 'enableStat' => $this->enableStat, 'autoApplyTransitions' => ($this->autoApplyTransitions == null) ? 0 : $this->autoApplyTransitions )); // Add a Campaign // we do not add a campaign record for draft layouts. if ($this->parentId === null) { $campaign = $this->campaignFactory->createEmpty(); $campaign->campaign = $this->layout; $campaign->isLayoutSpecific = 1; $campaign->ownerId = $this->getOwnerId(); $campaign->assignLayout($this); // Ready to save the Campaign // adding a Layout Specific Campaign shouldn't ever notify (it can't hit anything because we've only // just added it) $campaign->save([ 'notify' => false ]); // Assign the new campaignId to this layout $this->campaignId = $campaign->campaignId; // Add a layout history $this->addLayoutHistory(); } else if ($this->campaignId == null) { throw new InvalidArgumentException(__('Draft Layouts must have a parent'), 'campaignId'); } else { // Add this draft layout as a link to the campaign $campaign = $this->campaignFactory->getById($this->campaignId); $campaign->setChildObjectDependencies($this->layoutFactory); $campaign->assignLayout($this); $campaign->save([ 'notify' => false ]); } } /** * Update * @param array $options * @throws XiboException */ private function update($options = []) { $options = array_merge([ 'notify' => true, 'collectNow' => true ], $options); $this->getLog()->debug('Editing Layout ' . $this->layout . '. Id = ' . $this->layoutId); $sql = ' UPDATE layout SET layout = :layout, description = :description, duration = :duration, modifiedDT = :modifieddt, retired = :retired, width = :width, height = :height, backgroundImageId = :backgroundImageId, backgroundColor = :backgroundColor, backgroundzIndex = :backgroundzIndex, `status` = :status, publishedStatusId = :publishedStatusId, `userId` = :userId, `schemaVersion` = :schemaVersion, `statusMessage` = :statusMessage, enableStat = :enableStat, autoApplyTransitions = :autoApplyTransitions WHERE layoutID = :layoutid '; $time = $this->date->getLocalDate(); $this->getStore()->update($sql, array( 'layoutid' => $this->layoutId, 'layout' => $this->layout, 'description' => $this->description, 'duration' => $this->duration, 'modifieddt' => $time, 'retired' => $this->retired, 'width' => $this->width, 'height' => $this->height, 'backgroundImageId' => ($this->backgroundImageId == null) ? null : $this->backgroundImageId, 'backgroundColor' => $this->backgroundColor, 'backgroundzIndex' => $this->backgroundzIndex, 'status' => $this->status, 'publishedStatusId' => $this->publishedStatusId, 'userId' => $this->ownerId, 'schemaVersion' => $this->schemaVersion, 'statusMessage' => (empty($this->statusMessage)) ? null : json_encode($this->statusMessage), 'enableStat' => $this->enableStat, 'autoApplyTransitions' => $this->autoApplyTransitions )); // Update the Campaign if ($this->parentId === null) { $campaign = $this->campaignFactory->getById($this->campaignId); $campaign->campaign = $this->layout; $campaign->ownerId = $this->ownerId; $campaign->save(['validate' => false, 'notify' => $options['notify'], 'collectNow' => $options['collectNow']]); } } /** * Handle the Playlist closure table for specified Layout object * * @param $layout * @throws InvalidArgumentException */ public function managePlaylistClosureTable($layout) { // we only need to set the closure table records for the playlists assigned directly to the regionPlaylist here // all other relations between Playlists themselves are handled on import before layout is created // as the SQL we run here is recursive everything will end up with correct parent/child relation and depth level. foreach ($layout->getWidgets() as $widget) { if ($widget->type == 'subplaylist') { $assignedPlaylists = json_decode($widget->getOptionValue('subPlaylistIds', '[]')); $assignedPlaylists = implode(',', $assignedPlaylists); foreach ($layout->regions as $region) { $regionPlaylist = $region->regionPlaylist; if ($widget->playlistId == $regionPlaylist->playlistId) { $parentId = $regionPlaylist->playlistId; $child[] = $assignedPlaylists; } } } } if (isset($parentId) && isset($child)) { foreach ($child as $childId) { $this->getLog()->debug('Manage closure table for parent ' . $parentId . ' and child ' . $childId); if ($this->getStore()->exists('SELECT parentId, childId, depth FROM lkplaylistplaylist WHERE childId = :childId AND parentId = :parentId ', [ 'parentId' => $parentId, 'childId' => $childId ])) { throw new InvalidArgumentException(__('Cannot add the same SubPlaylist twice.'), 'playlistId'); } $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' => $parentId, 'childId' => $childId ]); } } } }PK qYST6% 6% UserGroup.phpnu [ setCommonDependencies($store, $log); $this->userGroupFactory = $userGroupFactory; $this->userFactory = $userFactory; } /** * */ public function __clone() { // Clear the groupId $this->groupId = null; } /** * @return string */ public function __toString() { return sprintf('ID = %d, Group = %s, IsUserSpecific = %d', $this->groupId, $this->group, $this->isUserSpecific); } /** * Generate a unique hash for this User Group */ private function hash() { return md5(json_encode($this)); } /** * @return int */ public function getId() { return $this->groupId; } /** * @return int */ public function getOwnerId() { return 0; } /** * Set the Owner of this Group * @param User $user */ public function setOwner($user) { $this->load(); $this->isUserSpecific = 1; $this->isEveryone = 0; $this->assignUser($user); } /** * Assign User * @param User $user */ public function assignUser($user) { $this->load(); if (!in_array($user, $this->users)) $this->users[] = $user; } /** * Unassign User * @param User $user */ public function unassignUser($user) { $this->load(); $this->users = array_udiff($this->users, [$user], function($a, $b) { /** * @var User $a * @var User $b */ return $a->getId() - $b->getId(); }); } /** * Validate */ public function validate() { if (!v::stringType()->length(1, 50)->validate($this->group)) throw new InvalidArgumentException(__('User Group Name cannot be empty.') . $this, 'name'); if ($this->libraryQuota !== null && !v::intType()->validate($this->libraryQuota)) throw new InvalidArgumentException(__('Library Quota must be a whole number.'), 'libraryQuota'); try { $group = $this->userGroupFactory->getByName($this->group, $this->isUserSpecific); if ($this->groupId == null || $this->groupId != $group->groupId) throw new DuplicateEntityException(__('There is already a group with this name. Please choose another.')); } catch (NotFoundException $e) { } } /** * Load this User Group * @param array $options */ public function load($options = []) { $options = array_merge([ 'loadUsers' => true ], $options); if ($this->loaded || $this->groupId == 0) return; if ($options['loadUsers']) { if ($this->userFactory == null) throw new \RuntimeException('Cannot load without first calling setChildObjectDependencies'); // Load all assigned users $this->users = $this->userFactory->getByGroupId($this->groupId); } // Set the hash $this->hash = $this->hash(); $this->loaded = true; } /** * Save the group * @param array $options */ public function save($options = []) { $options = array_merge([ 'validate' => true, 'linkUsers' => true ], $options); if ($options['validate']) $this->validate(); if ($this->groupId == null || $this->groupId == 0) $this->add(); else if ($this->hash() != $this->hash) $this->edit(); if ($options['linkUsers']) { $this->linkUsers(); $this->unlinkUsers(); } } /** * Delete this Group */ public function delete() { // We must ensure everything is loaded before we delete if ($this->hash == null) $this->load(); // Unlink users $this->removeAssignments(); $this->getStore()->update('DELETE FROM `permission` WHERE groupId = :groupId', ['groupId' => $this->groupId]); $this->getStore()->update('DELETE FROM `group` WHERE groupId = :groupId', ['groupId' => $this->groupId]); } /** * Remove all assignments */ private function removeAssignments() { // Delete Notifications // NB: notifications aren't modelled as child objects because there could be many thousands of notifications on each // usergroup. We consider the notification to be the parent here and it manages the assignments. // This does mean that we might end up with an empty notification (not assigned to anything) $this->getStore()->update('DELETE FROM `lknotificationuser` WHERE `userId` IN (SELECT `userId` FROM `lkusergroup` WHERE `groupId` = :groupId) ', ['groupId' => $this->groupId]); $this->getStore()->update('DELETE FROM `lknotificationgroup` WHERE `groupId` = :groupId', ['groupId' => $this->groupId]); // Remove user assignments $this->users = []; $this->unlinkUsers(); } /** * Add */ private function add() { $this->groupId = $this->getStore()->insert('INSERT INTO `group` (`group`, IsUserSpecific, libraryQuota, `isSystemNotification`, `isDisplayNotification`) VALUES (:group, :isUserSpecific, :libraryQuota, :isSystemNotification, :isDisplayNotification)', [ 'group' => $this->group, 'isUserSpecific' => $this->isUserSpecific, 'libraryQuota' => $this->libraryQuota, 'isSystemNotification' => $this->isSystemNotification, 'isDisplayNotification' => $this->isDisplayNotification ]); } /** * Edit */ private function edit() { $this->getStore()->update(' UPDATE `group` SET `group` = :group, libraryQuota = :libraryQuota, `isSystemNotification` = :isSystemNotification, `isDisplayNotification` = :isDisplayNotification WHERE groupId = :groupId ', [ 'groupId' => $this->groupId, 'group' => $this->group, 'libraryQuota' => $this->libraryQuota, 'isSystemNotification' => $this->isSystemNotification, 'isDisplayNotification' => $this->isDisplayNotification ]); } /** * Link Users */ private function linkUsers() { $insert = $this->getStore()->getConnection()->prepare('INSERT INTO `lkusergroup` (groupId, userId) VALUES (:groupId, :userId) ON DUPLICATE KEY UPDATE groupId = groupId'); foreach ($this->users as $user) { /* @var User $user */ $this->getLog()->debug('Linking %s to %s', $user->userName, $this->group); $insert->execute([ 'groupId' => $this->groupId, 'userId' => $user->userId ]); } } /** * Unlink Users */ private function unlinkUsers() { $params = ['groupId' => $this->groupId]; $sql = 'DELETE FROM `lkusergroup` WHERE groupId = :groupId AND userId NOT IN (0'; $i = 0; foreach ($this->users as $user) { /* @var User $user */ $i++; $sql .= ',:userId' . $i; $params['userId' . $i] = $user->userId; } $sql .= ')'; $this->getStore()->update($sql, $params); } }PK qYi3. Resolution.phpnu [ . */ namespace Xibo\Entity; use Respect\Validation\Validator as v; use Xibo\Exception\InvalidArgumentException; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; /** * Class Resolution * @package Xibo\Entity * * @SWG\Definition() */ class Resolution implements \JsonSerializable { use EntityTrait; /** * @SWG\Property(description="The ID of this Resolution") * @var int */ public $resolutionId; /** * @SWG\Property(description="The resolution name") * @var string */ public $resolution; /** * @SWG\Property(description="The display width of the resolution") * @var double */ public $width; /** * @SWG\Property(description="The display height of the resolution") * @var double */ public $height; /** * @SWG\Property(description="The designer width of the resolution") * @var double */ public $designerWidth; /** * @SWG\Property(description="The designer height of the resolution") * @var double */ public $designerHeight; /** * @SWG\Property(description="The layout schema version") * @var int */ public $version = 2; /** * @SWG\Property(description="A flag indicating whether this resolution is enabled or not") * @var int */ public $enabled = 1; /** * @SWG\Property(description="The userId who owns this Resolution") * @var int */ public $userId; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log */ public function __construct($store, $log) { $this->setCommonDependencies($store, $log); } /** * @return int */ public function getId() { return $this->resolutionId; } /** * @return int */ public function getOwnerId() { // No owner return $this->userId; } public function validate() { if (!v::stringType()->notEmpty()->validate($this->resolution)) throw new InvalidArgumentException(__('Please provide a name'), 'name'); if (!v::intType()->notEmpty()->min(1)->validate($this->width)) throw new InvalidArgumentException(__('Please provide a width'), 'width'); if (!v::intType()->notEmpty()->min(1)->validate($this->height)) throw new InvalidArgumentException(__('Please provide a height'), 'height'); // Set the designer width and height $factor = min (800 / $this->width, 800 / $this->height); $this->designerWidth = round($this->width * $factor); $this->designerHeight = round($this->height * $factor); } /** * Save * @param bool|true $validate */ public function save($validate = true) { if ($validate) $this->validate(); if ($this->resolutionId == null || $this->resolutionId == 0) $this->add(); else $this->edit(); $this->getLog()->audit('Resolution', $this->resolutionId, 'Saving', $this->getChangedProperties()); } public function delete() { $this->getStore()->update('DELETE FROM resolution WHERE resolutionID = :resolutionId', ['resolutionId' => $this->resolutionId]); } private function add() { $this->resolutionId = $this->getStore()->insert(' INSERT INTO `resolution` (resolution, width, height, intended_width, intended_height, version, enabled, `userId`) VALUES (:resolution, :width, :height, :intended_width, :intended_height, :version, :enabled, :userId) ', [ 'resolution' => $this->resolution, 'width' => $this->designerWidth, 'height' => $this->designerHeight, 'intended_width' => $this->width, 'intended_height' => $this->height, 'version' => $this->version, 'enabled' => $this->enabled, 'userId' => $this->userId ]); } private function edit() { $this->getStore()->update(' UPDATE resolution SET resolution = :resolution, width = :width, height = :height, intended_width = :intended_width, intended_height = :intended_height, enabled = :enabled WHERE resolutionID = :resolutionId ', [ 'resolutionId' => $this->resolutionId, 'resolution' => $this->resolution, 'width' => $this->designerWidth, 'height' => $this->designerHeight, 'intended_width' => $this->width, 'intended_height' => $this->height, 'enabled' => $this->enabled ]); } }PK qYMD85 5 DataSetColumn.phpnu [ excludeProperty('priorDatasetColumnId'); $this->setCommonDependencies($store, $log); $this->dataSetColumnFactory = $dataSetColumnFactory; $this->dataTypeFactory = $dataTypeFactory; $this->dataSetColumnTypeFactory = $dataSetColumnTypeFactory; } /** * Clone */ public function __clone() { $this->priorDatasetColumnId = $this->dataSetColumnId; $this->dataSetColumnId = null; $this->dataSetId = null; } /** * List Content Array * @return array */ public function listContentArray() { return explode(',', $this->listContent); } /** * Validate * @throws InvalidArgumentException */ public function validate() { if ($this->dataSetId == 0 || $this->dataSetId == '') throw new InvalidArgumentException(__('Missing dataSetId'), 'dataSetId'); if ($this->dataTypeId == 0 || $this->dataTypeId == '') throw new InvalidArgumentException(__('Missing dataTypeId'), 'dataTypeId'); if ($this->dataSetColumnTypeId == 0 || $this->dataSetColumnTypeId == '') throw new InvalidArgumentException(__('Missing dataSetColumnTypeId'), 'dataSetColumnTypeId'); if ($this->heading == '') throw new InvalidArgumentException(__('Please provide a column heading.'), 'heading'); if (!v::stringType()->alnum()->validate($this->heading) || strtolower($this->heading) == 'id') throw new InvalidArgumentException(__('Please provide an alternative column heading %s can not be used.', $this->heading), 'heading'); if ($this->dataSetColumnTypeId == 2 && $this->formula == '') { throw new InvalidArgumentException(__('Please enter a valid formula'), 'formula'); } // Make sure this column name is unique $columns = $this->dataSetColumnFactory->getByDataSetId($this->dataSetId); foreach ($columns as $column) { if ($column->heading == $this->heading && ($this->dataSetColumnId == null || $column->dataSetColumnId != $this->dataSetColumnId)) throw new InvalidArgumentException(__('A column already exists with this name, please choose another'), 'heading'); } // Check the actual values try { $this->dataTypeFactory->getById($this->dataTypeId); } catch (NotFoundException $e) { throw new InvalidArgumentException(__('Provided Data Type doesn\'t exist'), 'datatype'); } try { $dataSetColumnType = $this->dataSetColumnTypeFactory->getById($this->dataSetColumnTypeId); // If we are a remote column, validate we have a field if (strtolower($dataSetColumnType->dataSetColumnType) === 'remote' && ($this->remoteField === '' || $this->remoteField === null)) throw new InvalidArgumentException(__('Remote field is required when the column type is set to Remote'), 'remoteField'); } catch (NotFoundException $e) { throw new InvalidArgumentException(__('Provided DataSet Column Type doesn\'t exist'), 'dataSetColumnTypeId'); } // Should we validate the list content? if ($this->dataSetColumnId != 0 && $this->listContent != '') { // Look up all DataSet data in this table to make sure that the existing data is covered by the list content $list = $this->listContentArray(); // Add an empty field $list[] = ''; // We can check this is valid by building up a NOT IN sql statement, if we get results.. we know its not good $select = ''; $dbh = $this->getStore()->getConnection(); for ($i=0; $i < count($list); $i++) { $list_val = $dbh->quote($list[$i]); $select .= $list_val . ','; } $select = rtrim($select, ','); // $select has been quoted in the for loop - always test the original value of the column (we won't have changed the actualised table yet) $SQL = 'SELECT id FROM `dataset_' . $this->dataSetId . '` WHERE `' . $this->getOriginalValue('heading') . '` NOT IN (' . $select . ')'; $sth = $dbh->prepare($SQL); $sth->execute(array( 'datasetcolumnid' => $this->dataSetColumnId )); if ($sth->fetch()) throw new InvalidArgumentException(__('New list content value is invalid as it does not include values for existing data'), 'listcontent'); } // if formula dataSetType is set and formula is not empty, try to execute the SQL to validate it - we're ignoring client side formulas here. if ($this->dataSetColumnTypeId == 2 && $this->formula != '' && substr($this->formula, 0, 1) !== '$') { try { $formula = str_replace('[DisplayId]', 0, $this->formula); $this->getStore()->select('SELECT * FROM (SELECT `id`, ' . $formula . ' AS `' . $this->heading . '` FROM `dataset_' . $this->dataSetId . '`) dataset WHERE 1 = 1 ', []); } catch (\Exception $e) { $this->getLog()->debug('Formula validation failed with following message ' . $e->getMessage()); throw new InvalidArgumentException(__('Provided formula is invalid'), 'formula'); } } } /** * Save * @param array[Optional] $options * @throws InvalidArgumentException */ public function save($options = []) { $options = array_merge(['validate' => true, 'rebuilding' => false], $options); if ($options['validate'] && !$options['rebuilding']) $this->validate(); if ($this->dataSetColumnId == 0) $this->add(); else $this->edit($options); } /** * Delete */ public function delete() { $this->getStore()->update('DELETE FROM `datasetcolumn` WHERE DataSetColumnID = :dataSetColumnId', ['dataSetColumnId' => $this->dataSetColumnId]); // Delete column if (($this->dataSetColumnTypeId == 1) || ($this->dataSetColumnTypeId == 3)) { $this->getStore()->update('ALTER TABLE `dataset_' . $this->dataSetId . '` DROP `' . $this->heading . '`', []); } } /** * Add */ private function add() { $this->dataSetColumnId = $this->getStore()->insert(' INSERT INTO `datasetcolumn` (DataSetID, Heading, DataTypeID, ListContent, ColumnOrder, DataSetColumnTypeID, Formula, RemoteField, `showFilter`, `showSort`) VALUES (:dataSetId, :heading, :dataTypeId, :listContent, :columnOrder, :dataSetColumnTypeId, :formula, :remoteField, :showFilter, :showSort) ', [ 'dataSetId' => $this->dataSetId, 'heading' => $this->heading, 'dataTypeId' => $this->dataTypeId, 'listContent' => $this->listContent, 'columnOrder' => $this->columnOrder, 'dataSetColumnTypeId' => $this->dataSetColumnTypeId, 'formula' => $this->formula, 'remoteField' => $this->remoteField, 'showFilter' => $this->showFilter, 'showSort' => $this->showSort ]); // Add Column to Underlying Table if (($this->dataSetColumnTypeId == 1) || ($this->dataSetColumnTypeId == 3)) { // Use a separate connection for DDL (it operates outside transactions) $this->getStore()->isolated('ALTER TABLE `dataset_' . $this->dataSetId . '` ADD `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL', []); } } /** * Edit * @param array $options * @throws InvalidArgumentException */ private function edit($options) { $params = [ 'dataSetId' => $this->dataSetId, 'heading' => $this->heading, 'dataTypeId' => $this->dataTypeId, 'listContent' => $this->listContent, 'columnOrder' => $this->columnOrder, 'dataSetColumnTypeId' => $this->dataSetColumnTypeId, 'formula' => $this->formula, 'dataSetColumnId' => $this->dataSetColumnId, 'remoteField' => $this->remoteField, 'showFilter' => $this->showFilter, 'showSort' => $this->showSort ]; $sql = ' UPDATE `datasetcolumn` SET dataSetId = :dataSetId, Heading = :heading, ListContent = :listContent, ColumnOrder = :columnOrder, DataTypeID = :dataTypeId, DataSetColumnTypeID = :dataSetColumnTypeId, Formula = :formula, RemoteField = :remoteField, `showFilter` = :showFilter, `showSort` = :showSort WHERE dataSetColumnId = :dataSetColumnId '; $this->getStore()->update($sql, $params); try { if ($options['rebuilding'] && ($this->dataSetColumnTypeId == 1 || $this->dataSetColumnTypeId == 3)) { $this->getStore()->isolated('ALTER TABLE `dataset_' . $this->dataSetId . '` ADD `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL', []); } else if (($this->dataSetColumnTypeId == 1 || $this->dataSetColumnTypeId == 3) && ($this->hasPropertyChanged('heading') || $this->hasPropertyChanged('dataTypeId'))) { $sql = 'ALTER TABLE `dataset_' . $this->dataSetId . '` CHANGE `' . $this->getOriginalValue('heading') . '` `' . $this->heading . '` ' . $this->sqlDataType() . ' NULL DEFAULT NULL'; $this->getStore()->isolated($sql, []); } } catch (\PDOException $PDOException) { $this->getLog()->error('Unable to change DataSetColumn because ' . $PDOException->getMessage()); throw new InvalidArgumentException(__('Existing data is incompatible with your new configuration'), 'dataSetData'); } } /** * Get the SQL Data Type for this Column Definition * @return string */ private function sqlDataType() { $dataType = null; switch ($this->dataTypeId) { case 2: $dataType = 'FLOAT'; break; case 3: $dataType = 'DATETIME'; break; case 5: $dataType = 'INT'; break; case 1: $dataType = 'TEXT'; break; case 4: default: $dataType = 'VARCHAR(1000)'; } return $dataType; } }PK qYI5\ UserType.phpnu [ setCommonDependencies($store, $log); } public function getId() { return $this->userTypeId; } public function getOwnerId() { return 1; } }PK qYit Setting.phpnu [ . */ namespace Xibo\Entity; use Xibo\Exception\NotFoundException; use Xibo\Factory\DisplayFactory; use Xibo\Factory\PermissionFactory; use Xibo\Factory\PlaylistFactory; use Xibo\Factory\WidgetAudioFactory; use Xibo\Factory\WidgetMediaFactory; use Xibo\Factory\WidgetOptionFactory; use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; use Xibo\Widget\ModuleWidget; /** * 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="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 Module Object for this Widget") * @var ModuleWidget $module */ public $module; /** * @SWG\Property(description="The name of the Playlist this Widget is on") * @var string $playlist */ public $playlist; /** * Hash Key of Media Assignments * @var string */ private $mediaHash = null; /** * Temporary Id used during import/upgrade/sub-playlist ordering * @var string read only string */ public $tempId = null; /** * Flag to indicate whether the widget is newly added * @var bool */ public $isNew = false; /** @var int[] Original Module Media Ids */ private $originalModuleMediaIds = []; /** @var array[int] Original Media IDs */ private $originalMediaIds = []; /** @var array[WidgetAudio] Original Widget Audio */ private $originalAudio = []; /** * Minimum duration for widgets * @var int */ public static $widgetMinDuration = 1; //
/** @var DateServiceInterface */ private $dateService; /** * @var WidgetOptionFactory */ private $widgetOptionFactory; /** * @var WidgetMediaFactory */ private $widgetMediaFactory; /** @var WidgetAudioFactory */ private $widgetAudioFactory; /** * @var PermissionFactory */ private $permissionFactory; /** @var DisplayFactory */ private $displayFactory; /** @var PlaylistFactory */ private $playlistFactory; //
/** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param DateServiceInterface $date * @param WidgetOptionFactory $widgetOptionFactory * @param WidgetMediaFactory $widgetMediaFactory * @param WidgetAudioFactory $widgetAudioFactory * @param PermissionFactory $permissionFactory * @param DisplayFactory $displayFactory */ public function __construct($store, $log, $date, $widgetOptionFactory, $widgetMediaFactory, $widgetAudioFactory, $permissionFactory, $displayFactory) { $this->setCommonDependencies($store, $log); $this->excludeProperty('module'); $this->dateService = $date; $this->widgetOptionFactory = $widgetOptionFactory; $this->widgetMediaFactory = $widgetMediaFactory; $this->widgetAudioFactory = $widgetAudioFactory; $this->permissionFactory = $permissionFactory; $this->displayFactory = $displayFactory; } /** * @param PlaylistFactory $playlistFactory * @return $this */ public function setChildObjectDepencencies($playlistFactory) { $this->playlistFactory = $playlistFactory; return $this; } 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 = []; } /** * 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); } /** * 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) ); } /** * 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 * @return WidgetOption * @throws NotFoundException */ public function getOption($option) { foreach ($this->widgetOptions as $widgetOption) { /* @var WidgetOption $widgetOption */ if (strtolower($widgetOption->option) == strtolower($option)) return $widgetOption; } throw new NotFoundException('Widget Option not found'); } /** * Get Widget Option Value * @param string $option * @param mixed $default * @return mixed */ public function getOptionValue($option, $default) { try { $widgetOption = $this->getOption($option); $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($option, $type, $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())); } /** * Clear Media * this must only clear module media, not "primary" media */ public function clearCachedMedia() { $this->load(); $this->mediaIds = array_values(array_diff($this->mediaIds, $this->originalModuleMediaIds)); } /** * 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 && $this->dateService->parse($this->toDt, 'U') < $this->dateService->parse()); } /** * Calculates the duration of this widget according to some rules * @param $module ModuleWidget * @param bool $import * @return $this */ public function calculateDuration($module, $import = false) { $this->getLog()->debug('Calculating Duration - existing value is ' . $this->calculatedDuration); // Does our widget have a durationIsPerItem and a Number of Items? $numItems = $this->getOptionValue('numItems', 0); // Determine the duration of this widget if ($this->type === 'subplaylist') { // We use the module to calculate the duration $this->calculatedDuration = $module->getSubPlaylistResolvedDuration(); } else 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); } // For import // in the layout.xml file the duration associated with widget that has all the above parameters // will already be the calculatedDuration ie $this->duration from xml is duration * (numItems/itemsPerPage) // since we preserve the itemsPerPage, durationIsPerItem and numItems on imported layout, we need to ensure we set the duration correctly // this will ensure that both, the widget duration and calculatedDuration will be correct on import. if ($import) { $this->duration = (($this->useDuration == 1) ? $this->duration / $numItems : $module->getModule()->defaultDuration); } $this->calculatedDuration = (($this->useDuration == 1) ? $this->duration : $module->getModule()->defaultDuration) * $numItems; } else if ($this->useDuration == 1) { // Widget duration is as specified $this->calculatedDuration = $this->duration; } else if ($this->type === 'video' || $this->type === 'audio') { // The calculated duration is the "real" duration (caters for 0 videos) $this->calculatedDuration = $module->getDuration(['real' => true]); } else { // The module default duration. $this->calculatedDuration = $module->getModule()->defaultDuration; } $this->getLog()->debug('Set to ' . $this->calculatedDuration); return $this; } /** * Load the Widget */ public function load() { if ($this->loaded || $this->widgetId == null || $this->widgetId == 0) return; // Load permissions $this->permissions = $this->permissionFactory->getByObjectId(get_class(), $this->widgetId); // Load the widget options $this->widgetOptions = $this->widgetOptionFactory->getByWidgetId($this->widgetId); // Load any media assignments for this widget $this->mediaIds = $this->widgetMediaFactory->getByWidgetId($this->widgetId); $this->originalMediaIds = $this->mediaIds; $this->originalModuleMediaIds = $this->widgetMediaFactory->getModuleOnlyByWidgetId($this->widgetId); // Load any widget audio assignments $this->audio = $this->widgetAudioFactory->getByWidgetId($this->widgetId); $this->originalAudio = $this->audio; $this->hash = $this->hash(); $this->mediaHash = $this->mediaHash(); $this->loaded = true; } /** * Save the widget * @param array $options */ public function save($options = []) { // Default options $options = array_merge([ 'saveWidgetOptions' => true, 'saveWidgetAudio' => true, 'saveWidgetMedia' => true, 'notify' => true, 'notifyPlaylists' => true, 'notifyDisplays' => false, 'audit' => true, 'alwaysUpdate' => 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 if ($options['audit']) { $campaignId = 0; $layoutId = 0; $sql = '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 ;'; $params = ['playlistId' => $this->playlistId]; $results = $this->store->select($sql, $params); foreach ($results as $row) { $campaignId = $row['campaignId']; $layoutId = $row['layoutId']; } } // Add/Edit $isNew = false; if ($this->widgetId == null || $this->widgetId == 0) { $this->add(); $isNew = true; } else if ($this->hash != $this->hash() || $options['alwaysUpdate']) { $this->update(); } // Save the widget options if ($options['saveWidgetOptions']) { foreach ($this->widgetOptions as $widgetOption) { /* @var \Xibo\Entity\WidgetOption $widgetOption */ // Assert the widgetId $widgetOption->widgetId = $this->widgetId; $widgetOption->save(); } } // Save the widget audio if ($options['saveWidgetAudio']) { foreach ($this->audio as $audio) { /* @var \Xibo\Entity\WidgetAudio $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) { $changedProperties = null; if ($campaignId != 0 && $layoutId != 0) { $this->audit($this->widgetId, 'Added', ['widgetId' => $this->widgetId, 'type' => $this->type, 'layoutId' => $layoutId, 'campaignId' => $campaignId]); } } else { $changedProperties = $this->getChangedProperties(); $changedItems = []; 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, 'Saved', $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(); // Delete Permissions foreach ($this->permissions as $permission) { /* @var Permission $permission */ $permission->deleteAll(); } // Delete all Options foreach ($this->widgetOptions as $widgetOption) { /* @var \Xibo\Entity\WidgetOption $widgetOption */ // Assert the widgetId $widgetOption->widgetId = $this->widgetId; $widgetOption->delete(); } // Delete the widget audio foreach ($this->audio as $audio) { /* @var \Xibo\Entity\WidgetAudio $audio */ // Assert the widgetId $audio->widgetId = $this->widgetId; $audio->delete(); } // Unlink Media $this->mediaIds = []; $this->unlinkMedia(); // Delete this $this->getStore()->update('DELETE FROM `widget` WHERE widgetId = :widgetId', array('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', ['widgetId' => $this->widgetId, 'playlistId' => $this->playlistId]); } /** * 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' => $this->dateService->getLocalDate() ]); } // 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' => $this->dateService->getLocalDate() ]); } // 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->displayFactory->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`) VALUES (:playlistId, :ownerId, :type, :duration, :displayOrder, :useDuration, :calculatedDuration, :fromDt, :toDt, :createdDt, :modifiedDt) '; $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) ? time() : $this->createdDt, 'modifiedDt' => time() )); } 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 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' => time() ]; $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 )); } } /** * 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; } $sql .= ')'; $this->getStore()->update($sql, $params); } }PK qYj EntityTrait.phpnu [ . */ namespace Xibo\Entity; use Xibo\Helper\ObjectVars; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; /** * Class EntityTrait * used by all entities * @package Xibo\Entity */ trait EntityTrait { private $hash = null; private $loaded = false; private $permissionsClass = null; private $canChangeOwner = true; public $buttons = []; private $jsonExclude = ['buttons', 'jsonExclude', 'originalValues']; /** @var array Original values hydrated */ protected $originalValues = []; /** * @var StorageServiceInterface */ private $store; /** * @var LogServiceInterface */ private $log; /** * Set common dependencies. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @return $this */ protected function setCommonDependencies($store, $log) { $this->store = $store; $this->log = $log; return $this; } /** * Get Store * @return StorageServiceInterface */ protected function getStore() { return $this->store; } /** * Get Log * @return LogServiceInterface */ protected function getLog() { return $this->log; } /** * Hydrate an entity with properties * * @param array $properties * @param array $options * * @return self */ public function hydrate(array $properties, $options = []) { $intProperties = (array_key_exists('intProperties', $options)) ? $options['intProperties'] : []; $stringProperties = (array_key_exists('stringProperties', $options)) ? $options['stringProperties'] : []; $htmlStringProperties = (array_key_exists('htmlStringProperties', $options)) ? $options['htmlStringProperties'] : []; foreach ($properties as $prop => $val) { if (property_exists($this, $prop)) { if ((stripos(strrev($prop), 'dI') === 0 || in_array($prop, $intProperties)) && !in_array($prop, $stringProperties)) $val = intval($val); else if (in_array($prop, $stringProperties)) $val = filter_var($val, FILTER_SANITIZE_STRING); else if (in_array($prop, $htmlStringProperties)) $val = htmlentities($val); $this->{$prop} = $val; $this->originalValues[$prop] = $val; } } return $this; } /** * Reset originals to current values */ public function setOriginals() { foreach ($this->jsonSerialize() as $key => $value) { $this->originalValues[$key] = $value; } } /** * Get the original value of a property * @param string $property * @return null|mixed */ public function getOriginalValue($property) { return (isset($this->originalValues[$property])) ? $this->originalValues[$property] : null; } /** * Has the provided property been changed from its original value * @param string $property * @return bool */ public function hasPropertyChanged($property) { if (!property_exists($this, $property)) return true; return $this->getOriginalValue($property) != $this->{$property}; } /** * @param $property * @return bool */ public function propertyOriginallyExisted($property) { return array_key_exists($property, $this->originalValues); } /** * Get all changed properties for this entity */ public function getChangedProperties() { $changedProperties = []; foreach ($this->jsonSerialize() as $key => $value) { if (!is_array($value) && !is_object($value) && $this->propertyOriginallyExisted($key) && $this->hasPropertyChanged($key)) { $changedProperties[$key] = $this->getOriginalValue($key) . ' > ' . $value; } } return $changedProperties; } /** * Json Serialize * @return array */ public function jsonSerialize() { $exclude = $this->jsonExclude; $properties = ObjectVars::getObjectVars($this); $json = []; foreach ($properties as $key => $value) { if (!in_array($key, $exclude)) { $json[$key] = $value; } } return $json; } /** * To Array * @return array */ public function toArray() { return $this->jsonSerialize(); } /** * Add a property to the excluded list * @param string $property */ public function excludeProperty($property) { $this->jsonExclude[] = $property; } /** * Remove a property from the excluded list * @param string $property */ public function includeProperty($property) { $this->jsonExclude = array_diff($this->jsonExclude, [$property]); } /** * Get the Permissions Class * @return string */ public function permissionsClass() { return ($this->permissionsClass == null) ? get_class($this) : $this->permissionsClass; } /** * Set the Permissions Class * @param string $class */ protected function setPermissionsClass($class) { $this->permissionsClass = $class; } /** * Can the owner change? * @return bool */ public function canChangeOwner() { return $this->canChangeOwner && method_exists($this, 'setOwner'); } /** * @param bool $bool Can the owner be changed? */ protected function setCanChangeOwner($bool) { $this->canChangeOwner = $bool; } /** * @param $entityId * @param $message * @param array[Optional] $changedProperties */ protected function audit($entityId, $message, $changedProperties = null) { $class = substr(get_class($this), strrpos(get_class($this), '\\') + 1); if ($changedProperties === null) { // No properties provided, so we should work them out // If we have originals, then get changed, otherwise get the current object state $changedProperties = (count($this->originalValues) <= 0) ? $this->toArray() : $this->getChangedProperties(); } else if (count($changedProperties) <= 0) { // We provided changed properties, so we only audit if there are some return; } $this->getLog()->audit($class, $entityId, $message, $changedProperties); } /** * Compare two arrays, both keys and values. * * @param $array1 * @param $array2 * @param bool $compareValues * @return array */ public function compareMultidimensionalArrays($array1, $array2, $compareValues = true) { $result = []; // go through arrays, compare keys and values // the compareValues flag is there for tag unlink - we're interested only in array keys foreach ($array1 as $key => $value) { if (!is_array($array2) || !array_key_exists($key, $array2)) { $result[$key] = $value; continue; } if ($value != $array2[$key] && $compareValues) { $result[$key] = $value; } } return $result; } }PK qY*h Bandwidth.phpnu [ setCommonDependencies($store, $log); } public function save() { try { $this->getStore()->updateWithDeadlockLoop(' INSERT INTO `bandwidth` (Month, Type, DisplayID, Size) VALUES (:month, :type, :displayId, :size) ON DUPLICATE KEY UPDATE Size = Size + :size2 ', [ 'month' => strtotime(date('m') . '/02/' . date('Y') . ' 00:00:00'), 'type' => $this->type, 'displayId' => $this->displayId, 'size' => $this->size, 'size2' => $this->size ]); } catch (DeadlockException $deadlockException) { $this->getLog()->error('Deadlocked inserting bandwidth'); } } }PK qYᗄ@ @ Playlist.phpnu [ . */ namespace Xibo\Entity; use Xibo\Exception\DuplicateEntityException; use Xibo\Exception\InvalidArgumentException; use Xibo\Exception\NotFoundException; use Xibo\Factory\ModuleFactory; use Xibo\Factory\PermissionFactory; use Xibo\Factory\PlaylistFactory; use Xibo\Factory\TagFactory; use Xibo\Factory\WidgetFactory; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; use Xibo\Widget\SubPlaylist; /** * Class Playlist * @package Xibo\Entity * * @SWG\Definition() */ class Playlist implements \JsonSerializable { use EntityTrait; /** * @SWG\Property(description="The ID of this Playlist") * @var int */ public $playlistId; /** * @SWG\Property(description="The userId of the User that owns this Playlist") * @var int */ public $ownerId; /** * @SWG\Property(description="The Name of the Playlist") * @var string */ public $name; /** * @SWG\Property(description="The RegionId if this Playlist is specific to a Region") * @var int */ public $regionId; /** * @SWG\Property(description="Flag indicating if this is a dynamic Playlist") * @var int */ public $isDynamic; /** * @SWG\Property(description="Filter Name for a Dynamic Playlist") * @var string */ public $filterMediaName; /** * @SWG\Property(description="Filter Tags for a Dynamic Playlist") * @var string */ public $filterMediaTags; /** * @var string * @SWG\Property( * description="The datetime the Layout was created" * ) */ public $createdDt; /** * @var string * @SWG\Property( * description="The datetime the Layout was last modified" * ) */ public $modifiedDt; /** * @var int * @SWG\Property( * description="A read-only estimate of this Layout's total duration in seconds. This is equal to the longest region duration and is valid when the layout status is 1 or 2." * ) */ public $duration = 0; /** * @var int * @SWG\Property( * description="Flag indicating whether this Playlists requires a duration update" * ) */ public $requiresDurationUpdate; /** * @var string * @SWG\Property( * description="The option to enable the collection of Playlist Proof of Play statistics" * ) */ public $enableStat; /** * @SWG\Property(description="An array of Tags") * @var Tag[] */ public $tags = []; /** * @SWG\Property(description="An array of Widgets assigned to this Playlist") * @var Widget[] */ public $widgets = []; /** * @SWG\Property(description="An array of permissions") * @var Permission[] */ public $permissions = []; /** * Temporary Id used during import/upgrade * @var string read only string */ public $tempId = null; public $tagValues; // Read only properties public $owner; public $groupsWithPermissions; private $unassignTags = []; //
/** * @var DateServiceInterface */ private $dateService; /** * @var PermissionFactory */ private $permissionFactory; /** * @var WidgetFactory */ private $widgetFactory; /** * @var TagFactory */ private $tagFactory; /** * @var PlaylistFactory */ private $playlistFactory; /** @var ModuleFactory */ private $moduleFactory; /** * @var ConfigServiceInterface */ private $config; //
/** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param ConfigServiceInterface $config * @param DateServiceInterface $date * @param PermissionFactory $permissionFactory * @param PlaylistFactory $playlistFactory * @param WidgetFactory $widgetFactory * @param TagFactory $tagFactory */ public function __construct($store, $log, $config, $date, $permissionFactory, $playlistFactory, $widgetFactory, $tagFactory) { $this->setCommonDependencies($store, $log); $this->config = $config; $this->dateService = $date; $this->permissionFactory = $permissionFactory; $this->playlistFactory = $playlistFactory; $this->widgetFactory = $widgetFactory; $this->tagFactory = $tagFactory; } /** * @param ModuleFactory $moduleFactory * @return $this */ public function setModuleFactory($moduleFactory) { $this->moduleFactory = $moduleFactory; return $this; } /** * Clone this Playlist */ public function __clone() { $this->hash = null; $this->playlistId = null; $this->regionId = null; $this->permissions = []; $this->widgets = array_map(function ($object) { return clone $object; }, $this->widgets); } /** * @return string */ public function __toString() { return sprintf('Playlist %s. Widgets = %d. PlaylistId = %d. RegionId = %d', $this->name, count($this->widgets), $this->playlistId, $this->regionId); } /** * @return string */ private function hash() { return md5($this->regionId . $this->playlistId . $this->ownerId . $this->name . $this->duration . $this->requiresDurationUpdate); } /** * Get the Id * @return int */ public function getId() { return $this->playlistId; } /** * Get the OwnerId * @return int */ public function getOwnerId() { return $this->ownerId; } /** * Sets the Owner * @param int $ownerId */ public function setOwner($ownerId) { $this->load(); $this->ownerId = $ownerId; foreach ($this->widgets as $widget) { /* @var Widget $widget */ $widget->setOwner($ownerId); } } /** * Is this Playlist a Region Playlist (region specific) * @return bool */ public function isRegionPlaylist() { return ($this->regionId != null); } /** * Validate this playlist * @throws DuplicateEntityException */ public function validate() { // check for duplicates, // we check for empty playlist name due to layouts existing in the CMS before upgrade to v2 if ($this->name != '') { $duplicates = $this->playlistFactory->query(null, [ 'userId' => $this->ownerId, 'playlistExact' => $this->name, 'regionSpecific' => 0, 'disableUserCheck' => 1, 'notPlaylistId' => ($this->playlistId == null) ? 0 : $this->playlistId, ]); if (count($duplicates) > 0) { throw new DuplicateEntityException(sprintf(__("You already own a Playlist called '%s'. Please choose another name."), $this->name)); } } } /** * Is this Playlist editable. * Are we a standalone playlist OR are we on a draft layout * @return bool */ public function isEditable() { if ($this->isRegionPlaylist()) { // Run a lookup to see if we're on a draft layout $this->getLog()->debug('Checking whether we are on a Layout which is in the Draft State'); $exists = $this->getStore()->exists(' SELECT `layout`.layoutId FROM `region` INNER JOIN `layout` ON layout.layoutId = region.layoutId WHERE regionId = :regionId AND parentId IS NOT NULL ', [ 'regionId' => $this->regionId ]); $this->getLog()->debug('We are ' . (($exists) ? 'editable' : 'not editable')); return $exists; } else { $this->getLog()->debug('Non-region Playlist - we\'re always Editable' ); return true; } } /** * Get Widget at Index * @param int $index * @param Widget[]|null $widgets * @return Widget * @throws NotFoundException */ public function getWidgetAt($index, $widgets = null) { if ($widgets === null) $widgets = $this->widgets; if ($index <= count($widgets)) { $zeroBased = $index - 1; if (isset($widgets[$zeroBased])) { return $widgets[$zeroBased]; } } throw new NotFoundException(sprintf(__('Widget not found at index %d'), $index)); } /** * Get Widget by Id * @param int $widgetId * @param Widget[]|null $widgets * @return Widget * @throws NotFoundException */ public function getWidget($widgetId, $widgets = null) { if ($widgets === null) $widgets = $this->widgets; foreach ($widgets as $widget) { if ($widget->widgetId == $widgetId) { return $widget; } } throw new NotFoundException(sprintf(__('Widget not found with ID %d'), $widgetId)); } /** * @param Widget $widget * @param int $displayOrder */ public function assignWidget($widget, $displayOrder = null) { $this->load(); // Has a display order been provided? if ($displayOrder !== null) { // We need to shuffle any existing widget down to make space for this one. foreach ($this->widgets as $existingWidget) { if ($existingWidget->displayOrder < $displayOrder) { // Earlier in the list, so do nothing. continue; } else { // This widget is >= the display order and therefore needs to be moved down one position. $existingWidget->displayOrder = $existingWidget->displayOrder + 1; } // Set the incoming widget to the requested display order. $widget->displayOrder = $displayOrder; } } else { // Take the next available one $widget->displayOrder = count($this->widgets) + 1; } $this->widgets[] = $widget; } /** * Delete a Widget * @param Widget $widget * @param array $options Delete Options * @return $this * @throws \Xibo\Exception\InvalidArgumentException */ public function deleteWidget($widget, $options = []) { $this->load(); if ($widget->playlistId != $this->playlistId) { throw new InvalidArgumentException(__('Cannot delete a Widget that isn\'t assigned to me'), 'playlistId'); } // Delete $widget->delete($options); // Remove the Deleted Widget from our Widgets $this->widgets = array_udiff($this->widgets, [$widget], function($a, $b) { /* @var \Xibo\Entity\Widget $a */ /* @var \Xibo\Entity\Widget $b */ return $a->widgetId - $b->widgetId; }); return $this; } /** * @param Tag[] $tags */ public function replaceTags($tags = []) { if (!is_array($this->tags) || count($this->tags) <= 0) $this->tags = $this->tagFactory->loadByPlaylistId($this->playlistId); if ($this->tags != $tags) { $this->unassignTags = array_udiff($this->tags, $tags, function ($a, $b) { /* @var Tag $a */ /* @var Tag $b */ return $a->tagId - $b->tagId; }); $this->getLog()->debug('Tags to be removed: %s', json_encode($this->unassignTags)); // Replace the arrays $this->tags = $tags; $this->getLog()->debug('Tags remaining: %s', json_encode($this->tags)); } else { $this->getLog()->debug('Tags were not changed'); } } /** * Unassign tag * @param Tag $tag * @return $this */ public function unassignTag($tag) { $this->load(); $this->tags = array_udiff($this->tags, [$tag], function($a, $b) { /* @var Tag $a */ /* @var Tag $b */ return $a->tagId - $b->tagId; }); $this->unassignTags[] = $tag; $this->getLog()->debug('Tags after removal %s', json_encode($this->tags)); return $this; } /** * Load * @param array $loadOptions * @return $this */ public function load($loadOptions = []) { if ($this->playlistId == null || $this->loaded) return $this; // Options $options = array_merge([ 'loadPermissions' => true, 'loadWidgets' => true, 'loadTags' => true ], $loadOptions); $this->getLog()->debug('Load Playlist with ' . json_encode($options)); // Load permissions if ($options['loadPermissions']) $this->permissions = $this->permissionFactory->getByObjectId(get_class(), $this->playlistId); // Load all tags if ($options['loadTags']) $this->tags = $this->tagFactory->loadByPlaylistId($this->playlistId); // Load the widgets if ($options['loadWidgets']) { foreach ($this->widgetFactory->getByPlaylistId($this->playlistId) as $widget) { /* @var Widget $widget */ $widget->load(); $this->widgets[] = $widget; } } $this->hash = $this->hash(); $this->loaded = true; return $this; } /** * Save * @param array $options * @throws \Xibo\Exception\DuplicateEntityException * @throws \Xibo\Exception\InvalidArgumentException */ public function save($options = []) { // Default options $options = array_merge([ 'saveTags' => true, 'saveWidgets' => true, 'notify' => true, 'validate' => true, 'auditPlaylist' => true ], $options); if ($options['validate']) { $this->validate(); } // if we are auditing and editing a regionPlaylist then get layout specific campaignId $campaignId = 0; $layoutId = 0; if ($options['auditPlaylist'] && $this->regionId != null) { $sql = 'SELECT campaign.campaignId, layout.layoutId FROM region 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 region.regionId = :regionId ;'; $params = ['regionId' => $this->regionId]; $results = $this->store->select($sql, $params); foreach ($results as $row) { $campaignId = $row['campaignId']; $layoutId = $row['layoutId']; } } if ($this->playlistId == null || $this->playlistId == 0) { $this->add(); } else if ($this->hash != $this->hash()) { $this->update(); } else { // Nothing changed wrt the Playlist itself. $options['auditPlaylist'] = false; } // Save the widgets? if ($options['saveWidgets']) { // Sort the widgets by their display order usort($this->widgets, function ($a, $b) { /** * @var Widget $a * @var Widget $b */ return $a->displayOrder - $b->displayOrder; }); // Assert the Playlist on all widgets and apply a display order // this keeps the widgets in numerical order on each playlist $i = 0; foreach ($this->widgets as $widget) { /* @var Widget $widget */ $i++; // Assert the playlistId $widget->playlistId = $this->playlistId; // Assert the displayOrder $widget->displayOrder = $i; $widget->save($options); } } // Save the tags? if ($options['saveTags']) { $this->getLog()->debug('Saving tags on ' . $this); // Save the tags if (is_array($this->tags)) { foreach ($this->tags as $tag) { /* @var Tag $tag */ $this->getLog()->debug('Assigning tag ' . $tag->tag); $tag->assignPlaylist($this->playlistId); $tag->save(); } } // Remove unwanted ones if (is_array($this->unassignTags)) { foreach ($this->unassignTags as $tag) { /* @var Tag $tag */ $this->getLog()->debug('Unassigning tag ' . $tag->tag); $tag->unassignPlaylist($this->playlistId); $tag->save(); } } } // Audit if ($options['auditPlaylist']) { $change = $this->getChangedProperties(); // if we are editing a regionPlaylist then add the layout specific campaignId to the audit log. if ($this->regionId != null) { $change['campaignId'][] = $campaignId; $change['layoutId'][] = $layoutId; } $this->audit($this->playlistId, 'Saved', $change); } } /** * Delete * @param array $options * @throws InvalidArgumentException */ public function delete($options = []) { $options = array_merge([ 'regionDelete' => false ], $options); // We must ensure everything is loaded before we delete if (!$this->loaded) { $this->load(); } if (!$options['regionDelete'] && $this->regionId != 0) throw new InvalidArgumentException(__('This Playlist belongs to a Region, please delete the Region instead.'), 'regionId'); // Notify we're going to delete // we do this here, because once we've deleted we lose the references for the storage query $this->notifyLayouts(); // Delete me from any other Playlists using me as a sub-playlist foreach ($this->playlistFactory->query(null, ['childId' => $this->playlistId, 'depth' => 1]) as $parent) { // $parent is a playlist to which we belong. // find out widget and delete it $this->getLog()->debug('This playlist is a sub-playlist in ' . $parent->name . ' we will need to remove it'); $parent->load(); foreach ($parent->widgets as $widget) { if ($widget->type === 'subplaylist') { // we get an array with all subplaylists assigned to the parent $subPlaylistIds = json_decode($widget->getOptionValue('subPlaylistIds', '[]')); foreach ($subPlaylistIds as $subplaylist) { // find the matching playlistId to the playlistId we want to delete if ($subplaylist == $this->playlistId) { // if there is only one element in the subPlaylistIds array then remove the widget if (count($subPlaylistIds) === 1) { $widget->delete(['notify' => false]); } else { // if the subPlaylistIds has more than one element, we want to just unassign our playlistId from it and save the widget, // we don't want to remove the whole widget in this case $updatedSubplaylistIds = array_diff($subPlaylistIds, [$this->playlistId]); $widget->setOptionValue('subPlaylistIds', 'attrib', json_encode($updatedSubplaylistIds)); $widget->save(); } } } } } } // We want to remove all link records from the closure table using the parentId $this->getStore()->update('DELETE FROM `lkplaylistplaylist` WHERE parentId = :playlistId', ['playlistId' => $this->playlistId]); // Delete my closure table records $this->getStore()->update('DELETE FROM `lkplaylistplaylist` WHERE childId = :playlistId', ['playlistId' => $this->playlistId]); // Unassign tags foreach ($this->tags as $tag) { /* @var Tag $tag */ $tag->unassignPlaylist($this->playlistId); $tag->save(); } // Delete Permissions foreach ($this->permissions as $permission) { /* @var Permission $permission */ $permission->deleteAll(); } // Delete widgets foreach ($this->widgets as $widget) { /* @var Widget $widget */ // Assert the playlistId $widget->playlistId = $this->playlistId; $widget->delete(); } // Delete this playlist $this->getStore()->update('DELETE FROM `playlist` WHERE playlistId = :playlistId', array('playlistId' => $this->playlistId)); // Audit $this->audit($this->playlistId, 'Deleted', ['playlistId' => $this->playlistId, 'regionId' => $this->regionId]); } /** * Add */ private function add() { $this->getLog()->debug('Adding Playlist ' . $this->name); $time = date('Y-m-d H:i:s'); $sql = ' INSERT INTO `playlist` (`name`, `ownerId`, `regionId`, `isDynamic`, `filterMediaName`, `filterMediaTags`, `createdDt`, `modifiedDt`, `requiresDurationUpdate`, `enableStat`) VALUES (:name, :ownerId, :regionId, :isDynamic, :filterMediaName, :filterMediaTags, :createdDt, :modifiedDt, :requiresDurationUpdate, :enableStat) '; $this->playlistId = $this->getStore()->insert($sql, array( 'name' => $this->name, 'ownerId' => $this->ownerId, 'regionId' => $this->regionId == 0 ? null : $this->regionId, 'isDynamic' => $this->isDynamic, 'filterMediaName' => $this->filterMediaName, 'filterMediaTags' => $this->filterMediaTags, 'createdDt' => $time, 'modifiedDt' => $time, 'requiresDurationUpdate' => ($this->requiresDurationUpdate === null) ? 0 : $this->requiresDurationUpdate, 'enableStat' => $this->enableStat )); // Insert my self link $this->getStore()->insert('INSERT INTO `lkplaylistplaylist` (`parentId`, `childId`, `depth`) VALUES (:parentId, :childId, 0)', [ 'parentId' => $this->playlistId, 'childId' => $this->playlistId ]); } /** * Update */ private function update() { $this->getLog()->debug('Updating Playlist ' . $this->name . '. Id = ' . $this->playlistId); $sql = ' UPDATE `playlist` SET `name` = :name, `ownerId` = :ownerId, `regionId` = :regionId, `modifiedDt` = :modifiedDt, `duration` = :duration, `isDynamic` = :isDynamic, `filterMediaName` = :filterMediaName, `filterMediaTags` = :filterMediaTags, `requiresDurationUpdate` = :requiresDurationUpdate, `enableStat` = :enableStat WHERE `playlistId` = :playlistId '; $this->getStore()->update($sql, array( 'playlistId' => $this->playlistId, 'name' => $this->name, 'ownerId' => $this->ownerId, 'regionId' => $this->regionId == 0 ? null : $this->regionId, 'duration' => $this->duration, 'isDynamic' => $this->isDynamic, 'filterMediaName' => $this->filterMediaName, 'filterMediaTags' => $this->filterMediaTags, 'modifiedDt' => date('Y-m-d H:i:s'), 'requiresDurationUpdate' => $this->requiresDurationUpdate, 'enableStat' => $this->enableStat )); } /** * Notify all Layouts of a change to this playlist * This only sets the Layout Status to require a build and to update the layout modified date * once the build is triggered, either from the UI or maintenance it will assess the layout * and call save() if required. * Layout->save() will ultimately notify the interested display groups. */ public function notifyLayouts() { // Notify the Playlist $this->getStore()->update('UPDATE `playlist` SET requiresDurationUpdate = 1, `modifiedDT` = :modifiedDt WHERE playlistId = :playlistId', [ 'playlistId' => $this->playlistId, 'modifiedDt' => $this->dateService->getLocalDate() ]); $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' => $this->dateService->getLocalDate() ]); } /** * Expand this Playlists widgets according to any sub-playlists that are present * @param int $parentWidgetId this tracks the top level widgetId * @param bool $expandSubplaylists * @return Widget[] * @throws InvalidArgumentException * @throws NotFoundException */ public function expandWidgets($parentWidgetId = 0, $expandSubplaylists = true) { $this->load(); $widgets = []; // Start with our own Widgets foreach ($this->widgets as $widget) { // some basic checking on whether this widets date/time are conductive to it being added to the // list. This is really an "expires" check, because we will rely on the player otherwise if ($widget->isExpired()) continue; // Persist the parentWidgetId in a temporary variable // if we have a parentWidgetId of 0, then we are top-level and we should use our widgetId $widget->tempId = $parentWidgetId == 0 ? $widget->widgetId : $parentWidgetId; // If we're a standard widget, add right away if ($widget->type !== 'subplaylist') { $widgets[] = $widget; } else { if ($expandSubplaylists === true) { /** @var SubPlaylist $module */ $module = $this->moduleFactory->createWithWidget($widget); $module->isValid(); $widgets = array_merge($widgets, $module->getSubPlaylistResolvedWidgets($widget->tempId)); } } } return $widgets; } /** * Update Playlist Duration * this is called by the system maintenance task to keep all Playlists durations updated * we should edit this playlist duration (noting the delta) and then find all Playlists of which this is * a sub-playlist and update their durations also (cascade upward) * @return $this * @throws NotFoundException * @throws \Xibo\Exception\DuplicateEntityException */ public function updateDuration() { // Update this Playlists Duration - get a SUM of all widget durations $this->load([ 'loadPermissions' => false, 'loadWidgets' => true, 'loadTags' => false ]); $duration = 0; $removedWidget = false; // What is the next time we need to update this Playlist (0 is never) $nextUpdate = 0; foreach ($this->widgets as $widget) { // Is this widget expired? if ($widget->isExpired()) { // Remove this widget. if ($widget->getOptionValue('deleteOnExpiry', 0) == 1) { // Don't notify at all because we're going to do that when we finish updating our duration. $widget->delete([ 'notify' => false, 'notifyPlaylists' => false, 'forceNotifyPlaylists' => false, 'notifyDisplays' => false ]); $removedWidget = true; } // Do not assess it continue; } // If we're a standard widget, add right away if ($widget->type !== 'subplaylist') { $duration += $widget->calculatedDuration; // Does this expire? // Log this as the new next update if ($widget->hasExpiry() && ($nextUpdate == 0 || $nextUpdate > $widget->toDt)) { $nextUpdate = $widget->toDt; } } else { // Add the sub playlist duration /** @var SubPlaylist $module */ $module = $this->moduleFactory->createWithWidget($widget); $duration += $module->getSubPlaylistResolvedDuration(); } } // Set our "requires duration" $delta = $duration - $this->duration; $this->getLog()->debug('Delta duration after updateDuration ' . $delta); $this->duration = $duration; $this->requiresDurationUpdate = $nextUpdate; $this->save(['saveTags' => false, 'saveWidgets' => false]); if ($removedWidget) { $this->notifyLayouts(); } if ($delta !== 0) { // Use the closure table to update all parent playlists (including this one). $this->getStore()->update(' UPDATE `playlist` SET duration = duration + :delta WHERE playlistId IN ( SELECT DISTINCT parentId FROM `lkplaylistplaylist` WHERE childId = :playlistId AND parentId <> :playlistId ) ', [ 'delta' => $delta, 'playlistId' => $this->playlistId ]); } return $this; } /** * Clone the closure table for a new PlaylistId * usually this is used on Draft creation * @param int $newParentId */ public function cloneClosureTable($newParentId) { $this->getStore()->update(' INSERT INTO `lkplaylistplaylist` (parentId, childId, depth) SELECT :newParentId, childId, depth FROM lkplaylistplaylist WHERE parentId = :parentId AND depth > 0 ', [ 'newParentId' => $newParentId, 'parentId' => $this->playlistId ]); } /** * Recursive function, that goes through all widgets on nested Playlists. * * generates nestedPlaylistDefinitions with Playlist ID as the key - later saved as nestedPlaylist.json on export * generates playlistMappings which contains all relations between playlists (parent/child) - later saved as playlistMappings.json on export * Adds dataSets data to $dataSets parameter - later saved as dataSet.json on export * * playlistMappings, nestedPLaylistDefinitions, dataSets and dataSetIds are passed by reference. * * * @param $widgets array An array of widgets assigned to the Playlist * @param $parentId int Playlist Id of the Playlist that is a parent to our current Playlist * @param $playlistMappings array An array of Playlists with ParentId and PlaylistId as keys * @param $count * @param $nestedPlaylistDefinitions array An array of Playlists including widdgets with playlistId as the key * @param $dataSetIds array Array of dataSetIds * @param $dataSets array Array of dataSets with dataSets from widgets on the layout level and nested Playlists * @param $dataSetFactory * @param $includeData bool Flag indicating whether we should include DataSet data in the export * @return mixed * @throws NotFoundException */ public function generatePlaylistMapping($widgets, $parentId, &$playlistMappings, &$count, &$nestedPlaylistDefinitions, &$dataSetIds, &$dataSets, $dataSetFactory, $includeData) { foreach ($widgets as $playlistWidget) { if ($playlistWidget->type == 'subplaylist') { $nestedPlaylistIds = json_decode($playlistWidget->getOptionValue('subPlaylistIds', []), true); foreach ($nestedPlaylistIds as $nestedPlaylistId) { $nestedPlaylist = $this->playlistFactory->getById($nestedPlaylistId); $nestedPlaylist->load(); $this->getLog()->debug('playlist mappings parent id ' . $parentId); $nestedPlaylistDefinitions[$nestedPlaylist->playlistId] = $nestedPlaylist; $playlistMappings[$parentId][$nestedPlaylist->playlistId] = [ 'parentId' => $parentId, 'playlist' => $nestedPlaylist->name, 'playlistId' => $nestedPlaylist->playlistId ]; $count++; // this is a recursive function, we need to go through all levels of nested Playlists. $this->generatePlaylistMapping($nestedPlaylist->widgets, $nestedPlaylist->playlistId, $playlistMappings, $count, $nestedPlaylistDefinitions,$dataSetIds, $dataSets, $dataSetFactory, $includeData); } } // if we have any widgets that use DataSets we want the dataSetId and data added if ($playlistWidget->type == 'datasetview' || $playlistWidget->type == 'datasetticker' || $playlistWidget->type == 'chart') { $dataSetId = $playlistWidget->getOptionValue('dataSetId', 0); if ($dataSetId != 0) { if (in_array($dataSetId, $dataSetIds)) continue; // Export the structure for this dataSet $dataSet = $dataSetFactory->getById($dataSetId); $dataSet->load(); // Are we also looking to export the data? if ($includeData) { $dataSet->data = $dataSet->getData([], ['includeFormulaColumns' => false]); } $dataSetIds[] = $dataSet->dataSetId; $dataSets[] = $dataSet; } } } return $playlistMappings; } }PK qYrMa Session.phpnu [ setCommonDependencies($store, $log); } public function getId() { return $this->userId; } public function getOwnerId() { return 1; } }PK qY%| | Media.phpnu [ . */ namespace Xibo\Entity; use Respect\Validation\Validator as v; use Xibo\Exception\ConfigurationException; use Xibo\Exception\DuplicateEntityException; use Xibo\Exception\InvalidArgumentException; use Xibo\Exception\NotFoundException; use Xibo\Exception\XiboException; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\MediaFactory; use Xibo\Factory\PermissionFactory; use Xibo\Factory\PlayerVersionFactory; use Xibo\Factory\PlaylistFactory; use Xibo\Factory\ScheduleFactory; use Xibo\Factory\TagFactory; use Xibo\Factory\WidgetFactory; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; /** * Class Media * @package Xibo\Entity * * @SWG\Definition() */ class Media implements \JsonSerializable { use EntityTrait; /** * @SWG\Property(description="The Media ID") * @var int */ public $mediaId; /** * @SWG\Property(description="The ID of the User that owns this Media") * @var int */ public $ownerId; /** * @SWG\Property(description="The Parent ID of this Media if it has been revised") * @var int */ public $parentId; /** * @SWG\Property(description="The Name of this Media") * @var string */ public $name; /** * @SWG\Property(description="The module type of this Media") * @var string */ public $mediaType; /** * @SWG\Property(description="The file name of the media as stored in the library") * @var string */ public $storedAs; /** * @SWG\Property(description="The original file name as it was uploaded") * @var string */ public $fileName; // Thing that might be referred to /** * @SWG\Property(description="Tags associated with this Media") * @var Tag[] */ public $tags = []; public $tagValues; /** * @SWG\Property(description="The file size in bytes") * @var int */ public $fileSize; /** * @SWG\Property(description="The duration to use when assigning this media to a Layout widget") * @var int */ public $duration = 0; /** * @SWG\Property(description="Flag indicating whether this media is valid.") * @var int */ public $valid = 1; /** * @SWG\Property(description="Flag indicating whether this media is a system file or not") * @var int */ public $moduleSystemFile = 0; /** * @SWG\Property(description="Timestamp indicating when this media should expire") * @var int */ public $expires = 0; /** * @SWG\Property(description="Flag indicating whether this media is retired") * @var int */ public $retired = 0; /** * @SWG\Property(description="Flag indicating whether this media has been edited and replaced with a newer file") * @var int */ public $isEdited = 0; /** * @SWG\Property(description="A MD5 checksum of the stored media file") * @var string */ public $md5; /** * @SWG\Property(description="The username of the User that owns this media") * @var string */ public $owner; /** * @SWG\Property(description="A comma separated list of groups/users with permissions to this Media") * @var string */ public $groupsWithPermissions; /** * @SWG\Property(description="A flag indicating whether this media has been released") * @var int */ public $released = 1; /** * @SWG\Property(description="An API reference") * @var string */ public $apiRef; /** * @var string * @SWG\Property( * description="The datetime the Media was created" * ) */ public $createdDt; /** * @var string * @SWG\Property( * description="The datetime the Media was last modified" * ) */ public $modifiedDt; /** * @var string * @SWG\Property( * description="The option to enable the collection of Media Proof of Play statistics" * ) */ public $enableStat; // Private private $unassignTags = []; private $requestOptions = []; // New file revision public $isSaveRequired; public $isRemote; public $cloned = false; public $newExpiry; public $alwaysCopy = false; private $widgets = []; private $displayGroups = []; private $layoutBackgroundImages = []; private $permissions = []; /** * @var ConfigServiceInterface */ private $config; /** * @var MediaFactory */ private $mediaFactory; /** * @var TagFactory */ private $tagFactory; /** * @var LayoutFactory */ private $layoutFactory; /** * @var WidgetFactory */ private $widgetFactory; /** * @var DisplayGroupFactory */ private $displayGroupFactory; /** * @var PermissionFactory */ private $permissionFactory; /** * @var PlayerVersionFactory */ private $playerVersionFactory; /** * @var PlaylistFactory */ private $playlistFactory; /** @var DisplayFactory */ private $displayFactory; /** @var ScheduleFactory */ private $scheduleFactory; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param ConfigServiceInterface $config * @param MediaFactory $mediaFactory * @param PermissionFactory $permissionFactory * @param TagFactory $tagFactory * @param PlaylistFactory $playlistFactory */ public function __construct($store, $log, $config, $mediaFactory, $permissionFactory, $tagFactory, $playlistFactory) { $this->setCommonDependencies($store, $log); $this->config = $config; $this->mediaFactory = $mediaFactory; $this->permissionFactory = $permissionFactory; $this->tagFactory = $tagFactory; $this->playlistFactory = $playlistFactory; } /** * Set Child Object Dependencies * @param LayoutFactory $layoutFactory * @param WidgetFactory $widgetFactory * @param DisplayGroupFactory $displayGroupFactory * @param DisplayFactory $displayFactory * @param ScheduleFactory $scheduleFactory * @param PlayerVersionFactory $playerVersionFactory * @return $this */ public function setChildObjectDependencies($layoutFactory, $widgetFactory, $displayGroupFactory, $displayFactory, $scheduleFactory, $playerVersionFactory) { $this->layoutFactory = $layoutFactory; $this->widgetFactory = $widgetFactory; $this->displayGroupFactory = $displayGroupFactory; $this->displayFactory = $displayFactory; $this->scheduleFactory = $scheduleFactory; $this->playerVersionFactory = $playerVersionFactory; return $this; } public function __clone() { // Clear the ID's and all widget/displayGroup assignments $this->mediaId = null; $this->widgets = []; $this->displayGroups = []; $this->layoutBackgroundImages = []; $this->permissions = []; // We need to do something with the name $this->name = sprintf(__('Copy of %s on %s'), $this->name, date('Y-m-d H:i:s')); // Set so that when we add, we copy the existing file in the library $this->fileName = $this->storedAs; $this->storedAs = null; $this->cloned = true; } /** * Get Id * @return int */ public function getId() { return $this->mediaId; } /** * Get Owner Id * @return int */ public function getOwnerId() { return $this->ownerId; } /** * Sets the Owner * @param int $ownerId */ public function setOwner($ownerId) { $this->ownerId = $ownerId; } /** * @return int */ private function countUsages() { $this->load(['fullInfo' => true]); return count($this->widgets) + count($this->displayGroups) + count($this->layoutBackgroundImages); } /** * Is this media used * @param int $usages threshold * @return bool */ public function isUsed($usages = 0) { return $this->countUsages() > $usages; } /** * Assign Tag * @param Tag $tag * @return $this * @throws XiboException */ public function assignTag($tag) { $this->load(); if ($this->tags != [$tag]) { if (!in_array($tag, $this->tags)) { $this->tags[] = $tag; } } else { $this->getLog()->debug('No Tags to assign'); } return $this; } /** * Unassign tag * @param Tag $tag * @return $this * @throws XiboException */ public function unassignTag($tag) { $this->load(); $this->tags = array_udiff($this->tags, [$tag], function ($a, $b) { /* @var Tag $a */ /* @var Tag $b */ return $a->tagId - $b->tagId; }); $this->unassignTags[] = $tag; $this->getLog()->debug('Tags after removal %s', json_encode($this->tags)); return $this; } /** * @param array[Tag] $tags */ public function replaceTags($tags = []) { if (!is_array($this->tags) || count($this->tags) <= 0) $this->tags = $this->tagFactory->loadByMediaId($this->mediaId); if ($this->tags != $tags) { $this->unassignTags = array_udiff($this->tags, $tags, function ($a, $b) { /* @var Tag $a */ /* @var Tag $b */ return $a->tagId - $b->tagId; }); $this->getLog()->debug('Tags to be removed: %s', json_encode($this->unassignTags)); // Replace the arrays $this->tags = $tags; $this->getLog()->debug('Tags remaining: %s', json_encode($this->tags)); } else { $this->getLog()->debug('Tags were not changed'); } } /** * Validate * @param array $options * @throws XiboException */ public function validate($options) { if (!v::stringType()->notEmpty()->validate($this->mediaType)) throw new InvalidArgumentException(__('Unknown Module Type'), 'type'); if (!v::stringType()->notEmpty()->length(1, 100)->validate($this->name)) throw new InvalidArgumentException(__('The name must be between 1 and 100 characters'), 'name'); // Check the naming of this item to ensure it doesn't conflict $params = array(); $checkSQL = 'SELECT `name` FROM `media` WHERE `name` = :name AND userid = :userId'; if ($this->mediaId != 0) { $checkSQL .= ' AND mediaId <> :mediaId AND IsEdited = 0 '; $params['mediaId'] = $this->mediaId; } else if ($options['oldMedia'] != null && $this->name == $options['oldMedia']->name) { $checkSQL .= ' AND IsEdited = 0 '; } $params['name'] = $this->name; $params['userId'] = $this->ownerId; $result = $this->getStore()->select($checkSQL, $params); if (count($result) > 0) throw new DuplicateEntityException(__('Media you own already has this name. Please choose another.')); } /** * Load * @param array $options * @throws XiboException */ public function load($options = []) { if ($this->loaded || $this->mediaId == null) return; $options = array_merge([ 'deleting' => false, 'fullInfo' => false ], $options); $this->getLog()->debug('Loading Media. Options = %s', json_encode($options)); // Tags $this->tags = $this->tagFactory->loadByMediaId($this->mediaId); // Are we loading for a delete? If so load the child models, unless we're a module file in which case // we've no need. if ($this->mediaType !== 'module' && ($options['deleting'] || $options['fullInfo'])) { if ($this->widgetFactory === null) throw new ConfigurationException(__('Call setChildObjectDependencies before load')); // Permissions $this->permissions = $this->permissionFactory->getByObjectId(get_class($this), $this->mediaId); // Widgets $this->widgets = $this->widgetFactory->getByMediaId($this->mediaId); // Layout Background Images $this->layoutBackgroundImages = $this->layoutFactory->getByBackgroundImageId($this->mediaId); // Display Groups $this->displayGroups = $this->displayGroupFactory->getByMediaId($this->mediaId); } $this->loaded = true; } /** * Save this media * @param array $options * @throws ConfigurationException * @throws DuplicateEntityException * @throws InvalidArgumentException * @throws XiboException */ public function save($options = []) { $this->getLog()->debug('Save for mediaId: ' . $this->mediaId); $options = array_merge([ 'validate' => true, 'oldMedia' => null, 'deferred' => false, 'saveTags' => true ], $options); if ($options['validate'] && $this->mediaType != 'module') $this->validate($options); // Add or edit if ($this->mediaId == null || $this->mediaId == 0) { $this->add(); // Always set force to true as we always want to save new files $this->isSaveRequired = true; } else { $this->edit(); // If the media file is invalid, then force an update (only applies to module files) $expires = $this->getOriginalValue('expires'); $this->isSaveRequired = ($this->isSaveRequired || $this->valid == 0 || ($expires > 0 && $expires < time())); } if ($options['deferred']) { $this->getLog()->debug('Media Update deferred until later'); } else { $this->getLog()->debug('Media Update happening now'); // Call save file if ($this->isSaveRequired) $this->saveFile(); } if ($options['saveTags']) { // Save the tags if (is_array($this->tags)) { foreach ($this->tags as $tag) { /* @var Tag $tag */ $tag->assignMedia($this->mediaId); $tag->save(); } } // Remove unwanted ones if (is_array($this->unassignTags)) { foreach ($this->unassignTags as $tag) { /* @var Tag $tag */ $tag->unassignMedia($this->mediaId); $tag->save(); } } } } /** * Save Async * @param array $options * @return $this */ public function saveAsync($options = []) { $options = array_merge([ 'deferred' => true, 'requestOptions' => [] ], $options); $this->requestOptions = $options['requestOptions']; $this->save($options); return $this; } /** * Delete * @param array $options * @throws \Xibo\Exception\NotFoundException */ public function delete($options = []) { $options = array_merge([ 'rollback' => false ], $options); if ($options['rollback']) { $this->deleteRecord(); $this->deleteFile(); return; } $this->load(['deleting' => true]); // If there is a parent, bring it back try { $parentMedia = $this->mediaFactory->getParentById($this->mediaId); $parentMedia->isEdited = 0; $parentMedia->parentId = null; $parentMedia->save(['validate' => false]); } catch (NotFoundException $e) { // This is fine, no parent $parentMedia = null; } foreach ($this->permissions as $permission) { /* @var Permission $permission */ $permission->delete(); } foreach ($this->tags as $tag) { /* @var Tag $tag */ $tag->unassignMedia($this->mediaId); $tag->save(); } foreach ($this->widgets as $widget) { /* @var \Xibo\Entity\Widget $widget */ $widget->unassignMedia($this->mediaId); if ($parentMedia != null) { // Assign the parent media to the widget instead $widget->assignMedia($parentMedia->mediaId); // Swap any audio nodes over to this new widget media assignment. $this->getStore()->update(' UPDATE `lkwidgetaudio` SET mediaId = :mediaId WHERE widgetId = :widgetId AND mediaId = :oldMediaId ' , [ 'mediaId' => $parentMedia->mediaId, 'widgetId' => $widget->widgetId, 'oldMediaId' => $this->mediaId ]); } else { // Also delete the `lkwidgetaudio` $widget->unassignAudioById($this->mediaId); } // This action might result in us deleting a widget (unless we are a temporary file with an expiry date) if ($this->mediaType != 'module' && count($widget->mediaIds) <= 0) { $widget->setChildObjectDepencencies($this->playlistFactory); $widget->delete(); } else { $widget->save(['saveWidgetOptions' => false]); } } foreach ($this->displayGroups as $displayGroup) { /* @var \Xibo\Entity\DisplayGroup $displayGroup */ $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory); $displayGroup->unassignMedia($this); if ($parentMedia != null) $displayGroup->assignMedia($parentMedia); $displayGroup->save(['validate' => false]); } foreach ($this->layoutBackgroundImages as $layout) { /* @var Layout $layout */ $layout->backgroundImageId = null; $layout->save(Layout::$saveOptionsMinimum); } $this->deleteRecord(); $this->deleteFile(); // Update any background images if ($this->mediaType == 'image' && $parentMedia != null) { $this->getLog()->debug('Updating layouts with the old media %d as the background image.', $this->mediaId); // Get all Layouts with this as the background image foreach ($this->layoutFactory->query(null, ['backgroundImageId' => $this->mediaId]) as $layout) { /* @var Layout $layout */ $this->getLog()->debug('Found layout that needs updating. ID = %d. Setting background image id to %d', $layout->layoutId, $parentMedia->mediaId); $layout->backgroundImageId = $parentMedia->mediaId; $layout->save(); } } $this->audit($this->mediaId, 'Deleted'); } /** * Add * @throws ConfigurationException */ private function add() { $this->mediaId = $this->getStore()->insert(' INSERT INTO `media` (`name`, `type`, duration, originalFilename, userID, retired, moduleSystemFile, released, apiRef, valid, `createdDt`, `modifiedDt`, `enableStat`) VALUES (:name, :type, :duration, :originalFileName, :userId, :retired, :moduleSystemFile, :released, :apiRef, :valid, :createdDt, :modifiedDt, :enableStat) ', [ 'name' => $this->name, 'type' => $this->mediaType, 'duration' => $this->duration, 'originalFileName' => basename($this->fileName), 'userId' => $this->ownerId, 'retired' => $this->retired, 'moduleSystemFile' => (($this->moduleSystemFile) ? 1 : 0), 'released' => $this->released, 'apiRef' => $this->apiRef, 'valid' => 0, 'createdDt' => date('Y-m-d H:i:s'), 'modifiedDt' => date('Y-m-d H:i:s'), 'enableStat' => $this->enableStat ]); } /** * Edit */ private function edit() { $sql = ' UPDATE `media` SET `name` = :name, duration = :duration, retired = :retired, moduleSystemFile = :moduleSystemFile, editedMediaId = :editedMediaId, isEdited = :isEdited, userId = :userId, released = :released, apiRef = :apiRef, modifiedDt = :modifiedDt, `enableStat` = :enableStat, expires = :expires WHERE mediaId = :mediaId '; $params = [ 'name' => $this->name, 'duration' => $this->duration, 'retired' => $this->retired, 'moduleSystemFile' => $this->moduleSystemFile, 'editedMediaId' => $this->parentId, 'isEdited' => $this->isEdited, 'userId' => $this->ownerId, 'released' => $this->released, 'apiRef' => $this->apiRef, 'mediaId' => $this->mediaId, 'modifiedDt' => date('Y-m-d H:i:s'), 'enableStat' => $this->enableStat, 'expires' => $this->expires ]; $this->getStore()->update($sql, $params); } /** * Delete record */ private function deleteRecord() { $this->getStore()->update('DELETE FROM media WHERE MediaID = :mediaId', ['mediaId' => $this->mediaId]); } /** * Save File to Library * works on files that are already in the File system * @throws ConfigurationException */ public function saveFile() { $libraryFolder = $this->config->getSetting('LIBRARY_LOCATION'); // Work out the extension $lastPeriod = strrchr($this->fileName, '.'); // Determine the save name if ($lastPeriod === false) { $saveName = $this->mediaId; } else { $saveName = $this->mediaId . '.' . strtolower(substr($lastPeriod, 1)); } if(isset($this->urlDownload) && $this->urlDownload === true) { // for upload via URL, handle cases where URL do not have specified extension in url // we either have a long string after lastPeriod or nothing if (isset($this->extension) && (strlen($lastPeriod) > 3 || $lastPeriod === false)) { $saveName = $this->mediaId . '.' . $this->extension; } $this->storedAs = $saveName; } $this->getLog()->debug('saveFile for "' . $this->name . '" [' . $this->mediaId . '] with storedAs = "' . $this->storedAs . '", fileName = "' . $this->fileName . '" to "' . $saveName . '". Always Copy = "' . $this->alwaysCopy . '", Cloned = "' . $this->cloned . '"'); // If the storesAs is empty, then set it to be the moved file name if (empty($this->storedAs) && !$this->alwaysCopy) { // We could be a fresh file entirely, or we could be a clone if ($this->cloned) { $this->getLog()->debug('Copying cloned file: ' . $libraryFolder . $this->fileName); // Copy the file into the library if (!@copy($libraryFolder . $this->fileName, $libraryFolder . $saveName)) throw new ConfigurationException(__('Problem copying file in the Library Folder')); } else { $this->getLog()->debug('Moving temporary file: ' . $libraryFolder . 'temp/' . $this->fileName); // Move the file into the library if (!$this->moveFile($libraryFolder . 'temp/' . $this->fileName, $libraryFolder . $saveName)) throw new ConfigurationException(__('Problem moving uploaded file into the Library Folder')); } // Set the storedAs $this->storedAs = $saveName; } else { // We have pre-defined where we want this to be stored if (empty($this->storedAs)) { // Assume we want to set this automatically (i.e. we are set to always copy) $this->storedAs = $saveName; } if ($this->isRemote) { $this->getLog()->debug('Moving temporary file: ' . $libraryFolder . 'temp/' . $this->name); // Move the file into the library if (!$this->moveFile($libraryFolder . 'temp/' . $this->name, $libraryFolder . $this->storedAs)) throw new ConfigurationException(__('Problem moving downloaded file into the Library Folder')); } else { $this->getLog()->debug('Copying specified file: ' . $this->fileName); if (!@copy($this->fileName, $libraryFolder . $this->storedAs)) { $this->getLog()->error('Cannot copy %s to %s', $this->fileName, $libraryFolder . $this->storedAs); throw new ConfigurationException(__('Problem copying provided file into the Library Folder')); } } } // Work out the MD5 $this->md5 = md5_file($libraryFolder . $this->storedAs); $this->fileSize = filesize($libraryFolder . $this->storedAs); // Set to valid $this->valid = 1; // Resize image dimensions if threshold exceeds $this->assessDimensions(); // Update the MD5 and storedAs to suit $this->getStore()->update('UPDATE `media` SET md5 = :md5, fileSize = :fileSize, storedAs = :storedAs, expires = :expires, released = :released, valid = 1 WHERE mediaId = :mediaId', [ 'fileSize' => $this->fileSize, 'md5' => $this->md5, 'storedAs' => $this->storedAs, 'expires' => $this->expires, 'released' => $this->released, 'mediaId' => $this->mediaId ]); } private function assessDimensions() { if ($this->mediaType === 'image' || ($this->mediaType === 'module' && $this->moduleSystemFile === 0)) { $libraryFolder = $this->config->getSetting('LIBRARY_LOCATION'); $filePath = $libraryFolder . $this->storedAs; list($imgWidth, $imgHeight) = @getimagesize($filePath); $resizeThreshold = $this->config->getSetting('DEFAULT_RESIZE_THRESHOLD'); $resizeLimit = $this->config->getSetting('DEFAULT_RESIZE_LIMIT'); // Media released set to 0 for large size images // if image size is greater than Resize Limit then we flag that image as too big if ($resizeLimit > 0 && ($imgWidth > $resizeLimit || $imgHeight > $resizeLimit)) { $this->released = 2; $this->getLog()->debug('Image size is too big. MediaId '. $this->mediaId); } elseif ($resizeThreshold > 0) { if ($imgWidth > $imgHeight) { // 'landscape'; if ($imgWidth <= $resizeThreshold) { $this->released = 1; } else { if ($resizeThreshold > 0) { $this->released = 0; $this->getLog()->debug('Image exceeded threshold, released set to 0. MediaId '. $this->mediaId); } } } else { // 'portrait'; if ($imgHeight <= $resizeThreshold) { $this->released = 1; } else { if ($resizeThreshold > 0) { $this->released = 0; $this->getLog()->debug('Image exceeded threshold, released set to 0. MediaId '. $this->mediaId); } } } } } } /** * Release an image from image processing * @param $md5 * @param $fileSize */ public function release($md5, $fileSize) { // Update the MD5 and fileSize $this->getStore()->update('UPDATE `media` SET md5 = :md5, fileSize = :fileSize, released = :released, modifiedDt = :modifiedDt WHERE mediaId = :mediaId', [ 'fileSize' => $fileSize, 'md5' => $md5, 'released' => 1, 'mediaId' => $this->mediaId, 'modifiedDt' => date('Y-m-d H:i:s') ]); $this->getLog()->debug('Updating image md5 and fileSize. MediaId '. $this->mediaId); } /** * Delete a Library File */ private function deleteFile() { // Make sure storedAs isn't null if ($this->storedAs == null) { $this->getLog()->error('Deleting media [%s] with empty stored as. Skipping library file delete.', $this->name); return; } // Library location $libraryLocation = $this->config->getSetting("LIBRARY_LOCATION"); // 3 things to check for.. // the actual file, the thumbnail, the background if (file_exists($libraryLocation . $this->storedAs)) unlink($libraryLocation . $this->storedAs); if (file_exists($libraryLocation . 'tn_' . $this->storedAs)) unlink($libraryLocation . 'tn_' . $this->storedAs); } /** * Workaround for moving files across file systems * @param $from * @param $to * @return bool */ private function moveFile($from, $to) { // Try to move the file first $moved = rename($from, $to); if (!$moved) { $this->getLog()->info('Cannot move file: ' . $from . ' to ' . $to . ', will try and copy/delete instead.'); // Copy $moved = copy($from, $to); // Delete if (!@unlink($from)) { $this->getLog()->error('Cannot delete file: ' . $from . ' after copying to ' . $to); } } return $moved; } /** * Download URL * @return string */ public function downloadUrl() { return $this->fileName; } /** * Download Sink * @return string */ public function downloadSink() { return $this->config->getSetting('LIBRARY_LOCATION') . 'temp' . DIRECTORY_SEPARATOR . $this->name; } /** * Get optional options for downloading media files * @return array */ public function downloadRequestOptions() { return $this->requestOptions; } }PK qY? ;# # Application.phpnu [ setCommonDependencies($store, $log); $this->applicationRedirectUriFactory = $applicationRedirectUriFactory; $this->applicationScopeFactory = $applicationScopeFactory; } /** * @param ApplicationRedirectUri $redirectUri */ public function assignRedirectUri($redirectUri) { $this->load(); // Assert client id $redirectUri->clientId = $this->key; if (!in_array($redirectUri, $this->redirectUris)) $this->redirectUris[] = $redirectUri; } /** * Unassign RedirectUri * @param ApplicationRedirectUri $redirectUri */ public function unassignRedirectUri($redirectUri) { $this->load(); $this->redirectUris = array_udiff($this->redirectUris, [$redirectUri], function($a, $b) { /** * @var ApplicationRedirectUri $a * @var ApplicationRedirectUri $b */ return $a->getId() - $b->getId(); }); } /** * @param ApplicationScope $scope */ public function assignScope($scope) { $this->load(); if (!in_array($scope, $this->scopes)) $this->scopes[] = $scope; } /** * @param ApplicationScope $scope */ public function unassignScope($scope) { $this->load(); $this->scopes = array_udiff($this->scopes, [$scope], function($a, $b) { /** * @var ApplicationScope $a * @var ApplicationScope $b */ return $a->getId() - $b->getId(); }); } /** * Load */ public function load() { if ($this->loaded) return; $this->redirectUris = $this->applicationRedirectUriFactory->getByClientId($this->key); // Get scopes $this->scopes = $this->applicationScopeFactory->getByClientId($this->key); $this->loaded = true; } public function save() { if ($this->key == null || $this->key == '') $this->add(); else $this->edit(); $this->getLog()->debug('Saving redirect uris: %s', json_encode($this->redirectUris)); foreach ($this->redirectUris as $redirectUri) { /* @var \Xibo\Entity\ApplicationRedirectUri $redirectUri */ $redirectUri->save(); } $this->manageScopeAssignments(); } public function delete() { $this->load(); foreach ($this->redirectUris as $redirectUri) { /* @var \Xibo\Entity\ApplicationRedirectUri $redirectUri */ $redirectUri->delete(); } // Clear out everything owned by this client $this->deleteTokens(); $this->getStore()->update('DELETE FROM `oauth_session_scopes` WHERE id IN (SELECT session_id FROM `oauth_sessions` WHERE `client_id` = :id)', ['id' => $this->key]); $this->getStore()->update('DELETE FROM `oauth_sessions` WHERE `client_id` = :id', ['id' => $this->key]); $this->getStore()->update('DELETE FROM `oauth_client_scopes` WHERE `clientId` = :id', ['id' => $this->key]); $this->getStore()->update('DELETE FROM `oauth_clients` WHERE `id` = :id', ['id' => $this->key]); } public function resetKeys() { $this->secret = SecureKey::generate(254); $this->deleteTokens(); } private function deleteTokens() { $this->getStore()->update('DELETE FROM `oauth_access_token_scopes` WHERE access_token IN (SELECT access_token FROM `oauth_access_tokens` WHERE session_id IN (SELECT session_id FROM `oauth_sessions` WHERE `client_id` = :id))', ['id' => $this->key]); $this->getStore()->update('DELETE FROM `oauth_refresh_tokens` WHERE access_token IN (SELECT access_token FROM `oauth_access_tokens` WHERE session_id IN (SELECT session_id FROM `oauth_sessions` WHERE `client_id` = :id))', ['id' => $this->key]); $this->getStore()->update('DELETE FROM `oauth_access_tokens` WHERE session_id IN (SELECT session_id FROM `oauth_sessions` WHERE `client_id` = :id)', ['id' => $this->key]); $this->getStore()->update('DELETE FROM `oauth_auth_code_scopes` WHERE auth_code IN (SELECT auth_code FROM `oauth_auth_codes` WHERE session_id IN (SELECT session_id FROM `oauth_sessions` WHERE `client_id` = :id))', ['id' => $this->key]); $this->getStore()->update('DELETE FROM `oauth_auth_codes` WHERE session_id IN (SELECT session_id FROM `oauth_sessions` WHERE `client_id` = :id)', ['id' => $this->key]); } private function add() { $this->key = SecureKey::generate(); // Simple Insert for now $this->getStore()->insert(' INSERT INTO `oauth_clients` (`id`, `secret`, `name`, `userId`, `authCode`, `clientCredentials`) VALUES (:id, :secret, :name, :userId, :authCode, :clientCredentials) ', [ 'id' => $this->key, 'secret' => $this->secret, 'name' => $this->name, 'userId' => $this->userId, 'authCode' => $this->authCode, 'clientCredentials' => $this->clientCredentials ]); } private function edit() { $this->getStore()->update(' UPDATE `oauth_clients` SET `id` = :id, `secret` = :secret, `name` = :name, `userId` = :userId, `authCode` = :authCode, `clientCredentials` = :clientCredentials WHERE `id` = :id ', [ 'id' => $this->key, 'secret' => $this->secret, 'name' => $this->name, 'userId' => $this->userId, 'authCode' => $this->authCode, 'clientCredentials' => $this->clientCredentials ]); } /** * Compare the original assignments with the current assignments and delete any that are missing, add any new ones */ private function manageScopeAssignments() { $i = 0; $params = ['clientId' => $this->key]; $unassignIn = ''; foreach ($this->scopes as $link) { $this->getStore()->update(' INSERT INTO `oauth_client_scopes` (clientId, scopeId) VALUES (:clientId, :scopeId) ON DUPLICATE KEY UPDATE scopeId = scopeId', [ 'clientId' => $this->key, 'scopeId' => $link->id ]); $i++; $unassignIn .= ',:scopeId' . $i; $params['scopeId' . $i] = $link->id; } // Unlink any NOT in the collection $sql = 'DELETE FROM `oauth_client_scopes` WHERE clientId = :clientId AND scopeId NOT IN (\'0\'' . $unassignIn . ')'; $this->getStore()->update($sql, $params); } }PK qY"L L Schedule.phpnu [ . */ namespace Xibo\Entity; use Jenssegers\Date\Date; use Respect\Validation\Validator as v; use Stash\Interfaces\PoolInterface; use Xibo\Exception\ConfigurationException; use Xibo\Exception\InvalidArgumentException; use Xibo\Exception\NotFoundException; use Xibo\Exception\XiboException; use Xibo\Factory\CampaignFactory; use Xibo\Factory\DayPartFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; use Xibo\Factory\ScheduleExclusionFactory; use Xibo\Factory\ScheduleReminderFactory; use Xibo\Factory\UserFactory; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; /** * Class Schedule * @package Xibo\Entity * * @SWG\Definition() */ class Schedule implements \JsonSerializable { use EntityTrait; public static $LAYOUT_EVENT = 1; public static $COMMAND_EVENT = 2; public static $OVERLAY_EVENT = 3; public static $INTERRUPT_EVENT = 4; public static $CAMPAIGN_EVENT = 5; public static $DATE_MIN = 0; public static $DATE_MAX = 2147483647; /** * @SWG\Property( * description="The ID of this Event" * ) * @var int */ public $eventId; /** * @SWG\Property( * description="The Event Type ID" * ) * @var int */ public $eventTypeId; /** * @SWG\Property( * description="The CampaignID this event is for" * ) * @var int */ public $campaignId; /** * @SWG\Property( * description="The CommandId this event is for" * ) * @var int */ public $commandId; /** * @SWG\Property( * description="Display Groups assigned to this Scheduled Event.", * type="array", * @SWG\Items(ref="#/definitions/DisplayGroup") * ) * @var DisplayGroup[] */ public $displayGroups = []; /** * @SWG\Property( * description="Schedule Reminders assigned to this Scheduled Event.", * type="array", * @SWG\Items(ref="#/definitions/ScheduleReminder") * ) * @var ScheduleReminder[] */ public $scheduleReminders = []; /** * @SWG\Property( * description="The userId that owns this event." * ) * @var int */ public $userId; /** * @SWG\Property( * description="A Unix timestamp representing the from date of this event in CMS time." * ) * @var int */ public $fromDt; /** * @SWG\Property( * description="A Unix timestamp representing the to date of this event in CMS time." * ) * @var int */ public $toDt; /** * @SWG\Property( * description="Integer indicating the event priority." * ) * @var int */ public $isPriority; /** * @SWG\Property( * description="The display order for this event." * ) * @var int */ public $displayOrder; /** * @SWG\Property( * description="If this event recurs when what is the recurrence period.", * enum={"None", "Minute", "Hour", "Day", "Week", "Month", "Year"} * ) * @var string */ public $recurrenceType; /** * @SWG\Property( * description="If this event recurs when what is the recurrence frequency.", * ) * @var int */ public $recurrenceDetail; /** * @SWG\Property( * description="A Unix timestamp indicating the end time of the recurring events." * ) * @var int */ public $recurrenceRange; /** * @SWG\Property(description="Recurrence repeats on days - 0 to 7 where 0 is a monday") * @var string */ public $recurrenceRepeatsOn; /** * @SWG\Property(description="Recurrence monthly repeats on - 0 is day of month, 1 is weekday of week") * @var int */ public $recurrenceMonthlyRepeatsOn; /** * @SWG\Property( * description="The Campaign/Layout Name", * readOnly=true * ) * @var string */ public $campaign; /** * @SWG\Property( * description="The Command Name", * readOnly=true * ) * @var string */ public $command; /** * @SWG\Property( * description="The Day Part Id" * ) * @var int */ public $dayPartId; /** * @SWG\Property(description="Is this event an always on event?") * @var int */ public $isAlways; /** * @SWG\Property(description="Does this event have custom from/to date times?") * @var int */ public $isCustom; /** * Last Recurrence Watermark * @var int */ public $lastRecurrenceWatermark; /** * @SWG\Property(description="Flag indicating whether the event should be synchronised across displays") * @var int */ public $syncEvent = 0; /** * @SWG\Property(description="Flag indicating whether the event will sync to the Display timezone") * @var int */ public $syncTimezone; /** * @SWG\Property(description="Seconds (0-3600) of each full hour that is scheduled that this Layout should occupy") * @var int */ public $shareOfVoice; /** * @SWG\Property(description="Flag (0-1), whether this event is using Geo Location") * @var int */ public $isGeoAware; /** * @SWG\Property(description="Geo JSON representing the area of this event") * @var string */ public $geoLocation; /** * @var ScheduleEvent[] */ private $scheduleEvents = []; /** * @var ConfigServiceInterface */ private $config; /** @var DateServiceInterface */ private $dateService; /** @var PoolInterface */ private $pool; /** * @var DisplayGroupFactory */ private $displayGroupFactory; /** @var DisplayFactory */ private $displayFactory; /** @var DayPartFactory */ private $dayPartFactory; /** @var CampaignFactory */ private $campaignFactory; /** @var ScheduleReminderFactory */ private $scheduleReminderFactory; /** @var ScheduleExclusionFactory */ private $scheduleExclusionFactory; /** * @var UserFactory */ private $userFactory; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param ConfigServiceInterface $config * @param PoolInterface $pool * @param DateServiceInterface $date * @param DisplayGroupFactory $displayGroupFactory * @param DayPartFactory $dayPartFactory * @param UserFactory $userFactory * @param ScheduleReminderFactory $scheduleReminderFactory * @param ScheduleExclusionFactory $scheduleExclusionFactory */ public function __construct($store, $log, $config, $pool, $date, $displayGroupFactory, $dayPartFactory, $userFactory, $scheduleReminderFactory, $scheduleExclusionFactory) { $this->setCommonDependencies($store, $log); $this->config = $config; $this->pool = $pool; $this->dateService = $date; $this->displayGroupFactory = $displayGroupFactory; $this->dayPartFactory = $dayPartFactory; $this->userFactory = $userFactory; $this->scheduleReminderFactory = $scheduleReminderFactory; $this->scheduleExclusionFactory = $scheduleExclusionFactory; $this->excludeProperty('lastRecurrenceWatermark'); } /** * @param CampaignFactory $campaignFactory * @return $this */ public function setCampaignFactory($campaignFactory) { $this->campaignFactory = $campaignFactory; return $this; } public function __clone() { $this->eventId = null; } /** * @param DisplayFactory $displayFactory * @return $this */ public function setDisplayFactory($displayFactory) { $this->displayFactory = $displayFactory; return $this; } /** * @param DateServiceInterface $dateService * @deprecated dateService is set by the factory * @return $this */ public function setDateService($dateService) { $this->dateService = $dateService; return $this; } /** * @return DateServiceInterface * @throws ConfigurationException */ private function getDate() { if ($this->dateService == null) throw new ConfigurationException('Application Error: Date Service is not set on Schedule Entity'); return $this->dateService; } /** * @return int */ public function getId() { return $this->eventId; } /** * @return int */ public function getOwnerId() { return $this->userId; } /** * Sets the Owner * @param int $ownerId */ public function setOwner($ownerId) { $this->userId = $ownerId; } /** * Are the provided dates within the schedule look ahead * @return bool * @throws XiboException */ private function inScheduleLookAhead() { if ($this->isAlwaysDayPart()) return true; // From Date and To Date are in UNIX format $currentDate = $this->getDate()->parse(); $rfLookAhead = clone $currentDate; $rfLookAhead->addSeconds(intval($this->config->getSetting('REQUIRED_FILES_LOOKAHEAD'))); // Dial current date back to the start of the day $currentDate->startOfDay(); // Test dates if ($this->recurrenceType != '') { // A recurring event $this->getLog()->debug('Checking look ahead based on recurrence'); // we should check whether the event from date is before the lookahead (i.e. the event has recurred once) // we should also check whether the recurrence range is still valid (i.e. we've not stopped recurring and we don't recur forever) return ( $this->fromDt <= $rfLookAhead->format('U') && ($this->recurrenceRange == 0 || $this->recurrenceRange > $currentDate->format('U')) ); } else if (!$this->isCustomDayPart() || $this->eventTypeId == self::$COMMAND_EVENT) { // Day parting event (non recurring) or command event // only test the from date. $this->getLog()->debug('Checking look ahead based from date ' . $currentDate->toRssString()); return ($this->fromDt >= $currentDate->format('U') && $this->fromDt <= $rfLookAhead->format('U')); } else { // Compare the event dates $this->getLog()->debug('Checking look ahead based event dates ' . $currentDate->toRssString() . ' / ' . $rfLookAhead->toRssString()); return ($this->fromDt <= $rfLookAhead->format('U') && $this->toDt >= $currentDate->format('U')); } } /** * Load */ public function load($options = []) { $options = array_merge([ 'loadScheduleReminders' => false ], $options); // If we are already loaded, then don't do it again if ($this->loaded || $this->eventId == null || $this->eventId == 0) return; $this->displayGroups = $this->displayGroupFactory->getByEventId($this->eventId); // Load schedule reminders if ($options['loadScheduleReminders']) { $this->scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId'=> $this->eventId]); } // Set the original values now that we're loaded. $this->setOriginals(); // We are fully loaded $this->loaded = true; } /** * Assign DisplayGroup * @param DisplayGroup $displayGroup */ public function assignDisplayGroup($displayGroup) { $this->load(); if (!in_array($displayGroup, $this->displayGroups)) $this->displayGroups[] = $displayGroup; } /** * Unassign DisplayGroup * @param DisplayGroup $displayGroup */ public function unassignDisplayGroup($displayGroup) { $this->load(); $this->displayGroups = array_udiff($this->displayGroups, [$displayGroup], function ($a, $b) { /** * @var DisplayGroup $a * @var DisplayGroup $b */ return $a->getId() - $b->getId(); }); } /** * Validate * @throws XiboException */ public function validate() { if (count($this->displayGroups) <= 0) { throw new InvalidArgumentException(__('No display groups selected'), 'displayGroups'); } $this->getLog()->debug('EventTypeId: ' . $this->eventTypeId . '. DayPartId: ' . $this->dayPartId . ', CampaignId: ' . $this->campaignId . ', CommandId: ' . $this->commandId); if ($this->eventTypeId == Schedule::$LAYOUT_EVENT || $this->eventTypeId == Schedule::$CAMPAIGN_EVENT || $this->eventTypeId == Schedule::$OVERLAY_EVENT || $this->eventTypeId == Schedule::$INTERRUPT_EVENT ) { // Validate layout if (!v::intType()->notEmpty()->validate($this->campaignId)) throw new InvalidArgumentException(__('Please select a Campaign/Layout for this event.'), 'campaignId'); if ($this->isCustomDayPart()) { // validate the dates if ($this->toDt <= $this->fromDt) throw new InvalidArgumentException(__('Can not have an end time earlier than your start time'), 'start/end'); } $this->commandId = null; // additional validation for Interrupt Layout event type if ($this->eventTypeId == Schedule::$INTERRUPT_EVENT) { // Hack : If this is an interrupt, check that the column is a SMALLINT and if it isn't alter the table $sql = 'SELECT DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = :table_name AND COLUMN_NAME = :column_name'; $params = ['table_name' => 'schedule', 'column_name' => 'shareOfVoice' ]; $results = $this->store->select($sql, $params); if (count($results) > 0) { $dataType = $results[0]['DATA_TYPE']; if ($dataType !== 'smallint') { $this->store->update('ALTER TABLE `schedule` MODIFY `shareOfVoice` SMALLINT', []); // convert any existing interrupt schedules? $this->store->update('UPDATE `schedule` SET `shareOfVoice` = 3600 * (shareOfVoice / 100) WHERE shareOfVoice > 0', []); } } if (!v::intType()->notEmpty()->min(0)->max(3600)->validate($this->shareOfVoice)) { throw new InvalidArgumentException(__('Share of Voice must be a whole number between 0 and 3600'), 'shareOfVoice'); } } } else if ($this->eventTypeId == Schedule::$COMMAND_EVENT) { // Validate command if (!v::intType()->notEmpty()->validate($this->commandId)) throw new InvalidArgumentException(__('Please select a Command for this event.'), 'command'); $this->campaignId = null; $this->toDt = null; } else { // No event type selected throw new InvalidArgumentException(__('Please select the Event Type'), 'eventTypeId'); } // Make sure we have a sensible recurrence setting if (!$this->isCustomDayPart() && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) throw new InvalidArgumentException(__('Repeats selection is invalid for Always or Daypart events'), 'recurrencyType'); // Check display order is positive if ($this->displayOrder < 0) throw new InvalidArgumentException(__('Display Order must be 0 or a positive number'), 'displayOrder'); // Check priority is positive if ($this->isPriority < 0) throw new InvalidArgumentException(__('Priority must be 0 or a positive number'), 'isPriority'); // Check recurrenceDetail every is positive if ($this->recurrenceType != '' && ($this->recurrenceDetail === null || $this->recurrenceDetail <= 0)) throw new InvalidArgumentException(__('Repeat every must be a positive number'), 'recurrenceDetail'); } /** * Save * @param array $options * @throws XiboException */ public function save($options = []) { $options = array_merge([ 'validate' => true, 'audit' => true, 'deleteOrphaned' => false, 'notify' => true ], $options); if ($options['validate']) $this->validate(); // Handle "always" day parts if ($this->isAlwaysDayPart()) { $this->fromDt = self::$DATE_MIN; $this->toDt = self::$DATE_MAX; } if ($this->eventId == null || $this->eventId == 0) { $this->add(); $auditMessage = 'Added'; $this->loaded = true; $isEdit = false; } else { // If this save action means there aren't any display groups assigned // and if we're set to deleteOrphaned, then delete if ($options['deleteOrphaned'] && count($this->displayGroups) <= 0) { $this->delete(); return; } else { $this->edit(); $auditMessage = 'Saved'; } $isEdit = true; } // Manage display assignments if ($this->loaded) { // Manage assignments $this->manageAssignments($isEdit && $options['notify']); } // Notify if ($options['notify']) { // Only if the schedule effects the immediate future - i.e. within the RF Look Ahead if ($this->inScheduleLookAhead()) { $this->getLog()->debug('Schedule changing is within the schedule look ahead, will notify ' . count($this->displayGroups) . ' display groups'); foreach ($this->displayGroups as $displayGroup) { /* @var DisplayGroup $displayGroup */ $this->displayFactory->getDisplayNotifyService()->collectNow()->notifyByDisplayGroupId($displayGroup->displayGroupId); } } else { $this->getLog()->debug('Schedule changing is not within the schedule look ahead'); } } if ($options['audit']) $this->audit($this->getId(), $auditMessage); // Drop the cache for this event $this->dropEventCache(); } /** * Delete this Schedule Event */ public function delete() { $this->load(); // Notify display groups $notify = $this->displayGroups; // Delete display group assignments $this->displayGroups = []; $this->unlinkDisplayGroups(); // Delete schedule exclusions $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]); foreach ($scheduleExclusions as $exclusion) { $exclusion->delete(); } // Delete schedule reminders if ($this->scheduleReminderFactory !== null) { $scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $this->eventId]); foreach ($scheduleReminders as $reminder) { $reminder->delete(); } } // Delete the event itself $this->getStore()->update('DELETE FROM `schedule` WHERE eventId = :eventId', ['eventId' => $this->eventId]); // Notify // Only if the schedule effects the immediate future - i.e. within the RF Look Ahead if ($this->inScheduleLookAhead() && $this->displayFactory !== null) { $this->getLog()->debug('Schedule changing is within the schedule look ahead, will notify ' . count($notify) . ' display groups'); foreach ($notify as $displayGroup) { /* @var DisplayGroup $displayGroup */ $this->displayFactory->getDisplayNotifyService()->collectNow()->notifyByDisplayGroupId($displayGroup->displayGroupId); } } else if ($this->displayFactory === null) { $this->getLog()->info('Notify disabled, dependencies not set'); } // Drop the cache for this event $this->dropEventCache(); // Audit $this->audit($this->getId(), 'Deleted', $this->toArray()); } /** * Add */ private function add() { $this->eventId = $this->getStore()->insert(' INSERT INTO `schedule` (eventTypeId, CampaignId, commandId, userID, is_priority, FromDT, ToDT, DisplayOrder, recurrence_type, recurrence_detail, recurrence_range, `recurrenceRepeatsOn`, `recurrenceMonthlyRepeatsOn`, `dayPartId`, `syncTimezone`, `syncEvent`, `shareOfVoice`, `isGeoAware`, `geoLocation`) VALUES (:eventTypeId, :campaignId, :commandId, :userId, :isPriority, :fromDt, :toDt, :displayOrder, :recurrenceType, :recurrenceDetail, :recurrenceRange, :recurrenceRepeatsOn, :recurrenceMonthlyRepeatsOn, :dayPartId, :syncTimezone, :syncEvent, :shareOfVoice, :isGeoAware, :geoLocation) ', [ 'eventTypeId' => $this->eventTypeId, 'campaignId' => $this->campaignId, 'commandId' => $this->commandId, 'userId' => $this->userId, 'isPriority' => $this->isPriority, 'fromDt' => $this->fromDt, 'toDt' => $this->toDt, 'displayOrder' => $this->displayOrder, 'recurrenceType' => $this->recurrenceType, 'recurrenceDetail' => $this->recurrenceDetail, 'recurrenceRange' => $this->recurrenceRange, 'recurrenceRepeatsOn' => $this->recurrenceRepeatsOn, 'recurrenceMonthlyRepeatsOn' => ($this->recurrenceMonthlyRepeatsOn == null) ? 0 : $this->recurrenceMonthlyRepeatsOn, 'dayPartId' => $this->dayPartId, 'syncTimezone' => $this->syncTimezone, 'syncEvent' => $this->syncEvent, 'shareOfVoice' => $this->shareOfVoice, 'isGeoAware' => $this->isGeoAware, 'geoLocation' => $this->geoLocation ]); } /** * Edit */ private function edit() { $this->getStore()->update(' UPDATE `schedule` SET eventTypeId = :eventTypeId, campaignId = :campaignId, commandId = :commandId, is_priority = :isPriority, userId = :userId, fromDt = :fromDt, toDt = :toDt, displayOrder = :displayOrder, recurrence_type = :recurrenceType, recurrence_detail = :recurrenceDetail, recurrence_range = :recurrenceRange, `recurrenceRepeatsOn` = :recurrenceRepeatsOn, `recurrenceMonthlyRepeatsOn` = :recurrenceMonthlyRepeatsOn, `dayPartId` = :dayPartId, `syncTimezone` = :syncTimezone, `syncEvent` = :syncEvent, `shareOfVoice` = :shareOfVoice, `isGeoAware` = :isGeoAware, `geoLocation` = :geoLocation WHERE eventId = :eventId ', [ 'eventTypeId' => $this->eventTypeId, 'campaignId' => ($this->campaignId !== 0) ? $this->campaignId : null, 'commandId' => $this->commandId, 'userId' => $this->userId, 'isPriority' => $this->isPriority, 'fromDt' => $this->fromDt, 'toDt' => $this->toDt, 'displayOrder' => $this->displayOrder, 'recurrenceType' => $this->recurrenceType, 'recurrenceDetail' => $this->recurrenceDetail, 'recurrenceRange' => $this->recurrenceRange, 'recurrenceRepeatsOn' => $this->recurrenceRepeatsOn, 'recurrenceMonthlyRepeatsOn' => $this->recurrenceMonthlyRepeatsOn, 'dayPartId' => $this->dayPartId, 'syncTimezone' => $this->syncTimezone, 'syncEvent' => $this->syncEvent, 'shareOfVoice' => $this->shareOfVoice, 'isGeoAware' => $this->isGeoAware, 'geoLocation' => $this->geoLocation, 'eventId' => $this->eventId ]); } /** * Get events between the provided dates. * @param Date $fromDt * @param Date $toDt * @return ScheduleEvent[] * @throws XiboException */ public function getEvents($fromDt, $toDt) { // Events scheduled "always" will return one event if ($this->isAlwaysDayPart()) { // Create events with min/max dates $this->addDetail(Schedule::$DATE_MIN, Schedule::$DATE_MAX); return $this->scheduleEvents; } // Copy the dates as we are going to be operating on them. $fromDt = $fromDt->copy(); $toDt = $toDt->copy(); if ($this->pool == null) throw new ConfigurationException(__('Cache pool not available')); if ($this->eventId == null) throw new InvalidArgumentException(__('Unable to generate schedule, unknown event'), 'eventId'); // What if we are requesting a single point in time? if ($fromDt == $toDt) { $this->log->debug('Requesting event for a single point in time: ' . $this->getDate()->getLocalDate($fromDt)); } $events = []; $fromTimeStamp = $fromDt->format('U'); $toTimeStamp = $toDt->format('U'); // Rewind the from date to the start of the month $fromDt->startOfMonth(); if ($fromDt == $toDt) { $this->log->debug('From and To Dates are the same after rewinding 1 month, the date is the 1st of the month, adding a month to toDate.'); $toDt->addMonth(); } // Load the dates into a date object for parsing $eventStart = $this->getDate()->parse($this->fromDt, 'U'); $eventEnd = ($this->toDt == null) ? $eventStart->copy() : $this->getDate()->parse($this->toDt, 'U'); // Does the original event go over the month boundary? if ($eventStart->month !== $eventEnd->month) { // We expect some residual events to spill out into the month we are generating // wind back the generate from date $fromDt->subMonth(); $this->getLog()->debug('Expecting events from the prior month to spill over into this one, pulled back the generate from dt to ' . $fromDt->toRssString()); } else { $this->getLog()->debug('The main event has a start and end date within the month, no need to pull it in from the prior month. [eventId:' . $this->eventId . ']'); } // Keep a cache of schedule exclusions, so we look them up by eventId only one time per event $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]); // Request month cache while ($fromDt < $toDt) { // Empty scheduleEvents as we are looping through each month // we dont want to save previous month events $this->scheduleEvents = []; // Events for the month. $this->generateMonth($fromDt, $eventStart, $eventEnd); $this->getLog()->debug('Filtering Events: ' . json_encode($this->scheduleEvents, JSON_PRETTY_PRINT) . '. fromTimeStamp: ' . $fromTimeStamp . ', toTimeStamp: ' . $toTimeStamp); foreach ($this->scheduleEvents as $scheduleEvent) { // Find the excluded recurring events $exclude = false; foreach ($scheduleExclusions as $exclusion) { if ($scheduleEvent->fromDt == $exclusion->fromDt && $scheduleEvent->toDt == $exclusion->toDt) { $exclude = true; continue; } } if ($exclude) { continue; } if (in_array($scheduleEvent, $events)) { continue; } if ($scheduleEvent->toDt == null) { if ($scheduleEvent->fromDt >= $fromTimeStamp && $scheduleEvent->toDt < $toTimeStamp) { $events[] = $scheduleEvent; } } else { if ($scheduleEvent->fromDt <= $toTimeStamp && $scheduleEvent->toDt > $fromTimeStamp) { $events[] = $scheduleEvent; } } } // Move the month forwards $fromDt->addMonth(); } // Clear our cache of schedule exclusions $scheduleExclusions = null; $this->getLog()->debug('Filtered ' . count($this->scheduleEvents) . ' to ' . count($events) . ', events: ' . json_encode($events, JSON_PRETTY_PRINT)); return $events; } /** * Generate Instances * @param Date $generateFromDt * @param Date $start * @param Date $end * @throws XiboException */ private function generateMonth($generateFromDt, $start, $end) { // Operate on copies of the dates passed. $start = $start->copy(); $end = $end->copy(); $generateFromDt->copy()->startOfMonth(); $generateToDt = $generateFromDt->copy()->addMonth(); $this->getLog()->debug('Request for schedule events on eventId ' . $this->eventId . ' from: ' . $this->getDate()->getLocalDate($generateFromDt) . ' to: ' . $this->getDate()->getLocalDate($generateToDt) . ' [eventId:' . $this->eventId . ']' ); // If we are a daypart event, look up the start/end times for the event $this->calculateDayPartTimes($start, $end); // Does the original event fall into this window? if ($start <= $generateToDt && $end > $generateFromDt) { // Add the detail for the main event (this is the event that originally triggered the generation) $this->getLog()->debug('Adding original event: ' . $start->toAtomString() . ' - ' . $end->toAtomString()); $this->addDetail($start->format('U'), $end->format('U')); } // If we don't have any recurrence, we are done if (empty($this->recurrenceType) || empty($this->recurrenceDetail)) return; // Detect invalid recurrences and quit early if (!$this->isCustomDayPart() && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) return; // Check the cache $item = $this->pool->getItem('schedule/' . $this->eventId . '/' . $generateFromDt->format('Y-m')); if ($item->isHit()) { $this->scheduleEvents = $item->get(); $this->getLog()->debug('Returning from cache! [eventId:' . $this->eventId . ']'); return; } $this->getLog()->debug('Cache miss! [eventId:' . $this->eventId . ']'); // vv anything below here means that the event window requested is not in the cache vv // WE ARE NOT IN THE CACHE // this means we need to always walk the tree from the last watermark // if the last watermark is after the from window, then we need to walk from the beginning // Handle recurrence $originalStart = $start->copy(); $lastWatermark = ($this->lastRecurrenceWatermark != 0) ? $this->getDate()->parse($this->lastRecurrenceWatermark, 'U') : $this->getDate()->parse(self::$DATE_MIN, 'U'); $this->getLog()->debug('Recurrence calculation required - last water mark is set to: ' . $lastWatermark->toRssString() . '. Event dates: ' . $start->toRssString() . ' - ' . $end->toRssString() . ' [eventId:' . $this->eventId . ']'); // Set the temp starts // the start date should be the latest of the event start date and the last recurrence date if ($lastWatermark > $start && $lastWatermark < $generateFromDt) { $this->getLog()->debug('The last watermark is later than the event start date and the generate from dt, using the watermark for forward population' . ' [eventId:' . $this->eventId . ']'); // Need to set the toDt based on the original event duration and the watermark start date $eventDuration = $start->diffInSeconds($end, true); /** @var Date $start */ $start = $lastWatermark->copy(); $end = $start->copy()->addSeconds($eventDuration); if ($start <= $generateToDt && $end >= $generateFromDt) { $this->addDetail($start->format('U'), $end->format('U')); $this->getLog()->debug('The event start/end is inside the month' ); } } // range should be the smallest of the recurrence range and the generate window todt // the start/end date should be the the first recurrence in the current window if ($this->recurrenceRange != 0) { $range = $this->getDate()->parse($this->recurrenceRange, 'U'); // Override the range to be within the period we are looking $range = ($range < $generateToDt) ? $range : $generateToDt->copy(); } else { $range = $generateToDt->copy(); } $this->getLog()->debug('[' . $generateFromDt->toRssString() . ' - ' . $generateToDt->toRssString() . '] Looping from ' . $start->toRssString() . ' to ' . $range->toRssString() . ' [eventId:' . $this->eventId . ']'); // loop until we have added the recurring events for the schedule while ($start < $range) { $this->getLog()->debug('Loop: ' . $start->toRssString() . ' to ' . $range->toRssString() . ' [eventId:' . $this->eventId . ', end: ' . $end->toRssString() . ']'); // add the appropriate time to the start and end switch ($this->recurrenceType) { case 'Minute': $start->minute($start->minute + $this->recurrenceDetail); $end->minute($end->minute + $this->recurrenceDetail); break; case 'Hour': $start->hour($start->hour + $this->recurrenceDetail); $end->hour($end->hour + $this->recurrenceDetail); break; case 'Day': $start->day($start->day + $this->recurrenceDetail); $end->day($end->day + $this->recurrenceDetail); break; case 'Week': // recurrenceRepeatsOn will contain an array we can use to determine which days it should repeat // on. Roll forward 7 days, adding each day we hit // if we go over the start of the week, then jump forward by the recurrence range if (!empty($this->recurrenceRepeatsOn)) { // Parse days selected and add the necessary events $daysSelected = explode(',', $this->recurrenceRepeatsOn); // Are we on the start day of this week already? $onStartOfWeek = ($start->copy()->setTimeFromTimeString('00:00:00') == $start->copy()->startOfWeek()->setTimeFromTimeString('00:00:00')); // What is the end of this week $endOfWeek = $start->copy()->endOfWeek(); $this->getLog()->debug('Days selected: ' . $this->recurrenceRepeatsOn . '. End of week = ' . $endOfWeek . ' start date ' . $start . ' [eventId:' . $this->eventId . ']'); for ($i = 1; $i <= 7; $i++) { // Add a day to the start dates // after the first pass, we will already be on the first day of the week if ($i > 1 || !$onStartOfWeek) { $start->day($start->day + 1); $end->day($end->day + 1); } $this->getLog()->debug('Assessing start date ' . $start->toAtomString() . ', isoDayOfWeek is ' . $start->dayOfWeekIso . ' [eventId:' . $this->eventId . ']'); // If we go over the recurrence range, stop // if we go over the start of the week, stop if ($start > $range || $start > $endOfWeek) { break; } // Is this day set? if (!in_array($start->dayOfWeekIso, $daysSelected)) { continue; } if ($start >= $generateFromDt) { $this->getLog()->debug('Adding detail for ' . $start->toAtomString() . ' to ' . $end->toAtomString()); if ($this->eventTypeId == self::$COMMAND_EVENT) { $this->addDetail($start->format('U'), null); } else { // If we are a daypart event, look up the start/end times for the event $this->calculateDayPartTimes($start, $end); $this->addDetail($start->format('U'), $end->format('U')); } } else { $this->getLog()->debug('Event is outside range'); } } $this->getLog()->debug('Finished 7 day roll forward, start date is ' . $start . ' [eventId:' . $this->eventId . ']'); // If we haven't passed the end of the week, roll forward if ($start < $endOfWeek) { $start->day($start->day + 1); $end->day($end->day + 1); } // Wind back a week and then add our recurrence detail $start->day($start->day - 7); $end->day($end->day - 7); $this->getLog()->debug('Resetting start date to ' . $start . ' [eventId:' . $this->eventId . ']'); } // Jump forward a week from the original start date (when we entered this loop) $start->day($start->day + ($this->recurrenceDetail * 7)); $end->day($end->day + ($this->recurrenceDetail * 7)); break; case 'Month': // Are we repeating on the day of the month, or the day of the week if ($this->recurrenceMonthlyRepeatsOn == 1) { // Week day repeat $difference = $end->diffInSeconds($start); // Work out the position in the month of this day and the ordinal $ordinals = ['first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh']; $ordinal = $ordinals[ceil($originalStart->day / 7) - 1]; $start->month($start->month + $this->recurrenceDetail)->modify($ordinal . ' ' . $originalStart->format('l') . ' of ' . $start->format('F Y'))->setTimeFrom($originalStart); $this->getLog()->debug('Setting start to: ' . $ordinal . ' ' . $start->format('l') . ' of ' . $start->format('F Y')); // Base the end on the start + difference $end = $start->copy()->addSeconds($difference); } else { // Day repeat $start->month($start->month + $this->recurrenceDetail); $end->month($end->month + $this->recurrenceDetail); } break; case 'Year': $start->year($start->year + $this->recurrenceDetail); $end->year($end->year + $this->recurrenceDetail); break; default: throw new InvalidArgumentException('Invalid recurrence type', 'recurrenceType'); } // after we have added the appropriate amount, are we still valid if ($start > $range) { $this->getLog()->debug('Breaking mid loop because we\'ve exceeded the range. Start: ' . $start->toRssString() . ', range: ' . $range->toRssString() . ' [eventId:' . $this->eventId . ']'); break; } // Push the watermark $lastWatermark = $start->copy(); // Don't add if we are weekly recurrency (handles it's own adding) if ($this->recurrenceType == 'Week' && !empty($this->recurrenceRepeatsOn)) continue; if ($start <= $generateToDt && $end >= $generateFromDt) { if ($this->eventTypeId == self::$COMMAND_EVENT) $this->addDetail($start->format('U'), null); else { // If we are a daypart event, look up the start/end times for the event $this->calculateDayPartTimes($start, $end); $this->addDetail($start->format('U'), $end->format('U')); } } } $this->getLog()->debug('Our last recurrence watermark is: ' . $lastWatermark->toRssString() . '[eventId:' . $this->eventId . ']'); // Update our schedule with the new last watermark $lastWatermarkTimeStamp = $lastWatermark->format('U'); if ($lastWatermarkTimeStamp != $this->lastRecurrenceWatermark) { $this->lastRecurrenceWatermark = $lastWatermarkTimeStamp; $this->getStore()->update('UPDATE `schedule` SET lastRecurrenceWatermark = :lastRecurrenceWatermark WHERE eventId = :eventId', [ 'eventId' => $this->eventId, 'lastRecurrenceWatermark' => $this->lastRecurrenceWatermark ]); } // Update the cache $item->set($this->scheduleEvents); $item->expiresAt(Date::now()->addMonths(2)); $this->pool->saveDeferred($item); return; } /** * Drop the event cache * @param $key */ private function dropEventCache($key = null) { $compKey = 'schedule/' . $this->eventId; if ($key !== null) $compKey .= '/' . $key; $this->pool->deleteItem($compKey); } /** * Calculate the DayPart times * @param Date $start * @param Date $end * @throws XiboException */ private function calculateDayPartTimes($start, $end) { $dayOfWeekLookup = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; if (!$this->isAlwaysDayPart() && !$this->isCustomDayPart()) { // End is always based on Start $end->setTimestamp($start->format('U')); $dayPart = $this->dayPartFactory->getById($this->dayPartId); $this->getLog()->debug('Start and end time for dayPart is ' . $dayPart->startTime . ' - ' . $dayPart->endTime); // What day of the week does this start date represent? // dayOfWeek is 0 for Sunday to 6 for Saturday $found = false; foreach ($dayPart->exceptions as $exception) { // Is there an exception for this day of the week? if ($exception['day'] == $dayOfWeekLookup[$start->dayOfWeek]) { $start->setTimeFromTimeString($exception['start']); $end->setTimeFromTimeString($exception['end']); if ($start >= $end) $end->addDay(); $this->getLog()->debug('Found exception Start and end time for dayPart exception is ' . $exception['start'] . ' - ' . $exception['end']); $found = true; break; } } if (!$found) { // Set the time section of our dates based on the daypart date $start->setTimeFromTimeString($dayPart->startTime); $end->setTimeFromTimeString($dayPart->endTime); if ($start >= $end) { $this->getLog()->debug('Start is ahead of end - adding a day to the end date'); $end->addDay(); } } } } /** * Add Detail * @param int $fromDt * @param int $toDt */ private function addDetail($fromDt, $toDt) { $this->scheduleEvents[] = new ScheduleEvent($fromDt, $toDt); } /** * Manage the assignments * @param bool $notify should we notify or not? * @throws \Xibo\Exception\XiboException */ private function manageAssignments($notify) { $this->linkDisplayGroups(); $this->unlinkDisplayGroups(); $this->getLog()->debug('manageAssignments: Assessing whether we need to notify'); $originalDisplayGroups = $this->getOriginalValue('displayGroups'); // Get the difference between the original display groups assigned and the new display groups assigned if ($notify && $originalDisplayGroups !== null && $this->inScheduleLookAhead()) { $diff = []; foreach ($originalDisplayGroups as $element) { /** @var \Xibo\Entity\DisplayGroup $element */ $diff[$element->getId()] = $element; } if (count($diff) > 0) { $this->getLog()->debug('manageAssignments: There are ' . count($diff) . ' existing DisplayGroups on this Event'); $ids = array_map(function ($element) { return $element->getId(); }, $this->displayGroups); $except = array_diff(array_keys($diff), $ids); if (count($except) > 0) { foreach ($except as $item) { $this->getLog()->debug('manageAssignments: calling notify on displayGroupId ' . $diff[$item]->getId()); $this->displayFactory->getDisplayNotifyService()->collectNow()->notifyByDisplayGroupId($diff[$item]->getId()); } } else { $this->getLog()->debug('manageAssignments: No need to notify'); } } else { $this->getLog()->debug('manageAssignments: No change to DisplayGroup assignments'); } } else { $this->getLog()->debug('manageAssignments: Not in look-ahead'); } } /** * Link Layout */ private function linkDisplayGroups() { // TODO: Make this more efficient by storing the prepared SQL statement $sql = 'INSERT INTO `lkscheduledisplaygroup` (eventId, displayGroupId) VALUES (:eventId, :displayGroupId) ON DUPLICATE KEY UPDATE displayGroupId = displayGroupId'; $i = 0; foreach ($this->displayGroups as $displayGroup) { $i++; $this->getStore()->insert($sql, array( 'eventId' => $this->eventId, 'displayGroupId' => $displayGroup->displayGroupId )); } } /** * Unlink Layout */ private function unlinkDisplayGroups() { // Unlink any layouts that are NOT in the collection $params = ['eventId' => $this->eventId]; $sql = 'DELETE FROM `lkscheduledisplaygroup` WHERE eventId = :eventId AND displayGroupId NOT IN (0'; $i = 0; foreach ($this->displayGroups as $displayGroup) { $i++; $sql .= ',:displayGroupId' . $i; $params['displayGroupId' . $i] = $displayGroup->displayGroupId; } $sql .= ')'; $this->getStore()->update($sql, $params); } /** * Is this event an always daypart event * @return bool * @throws \Xibo\Exception\NotFoundException */ public function isAlwaysDayPart() { $dayPart = $this->dayPartFactory->getById($this->dayPartId); return $dayPart->isAlways === 1; } /** * Is this event a custom daypart event * @return bool * @throws \Xibo\Exception\NotFoundException */ public function isCustomDayPart() { $dayPart = $this->dayPartFactory->getById($this->dayPartId); return $dayPart->isCustom === 1; } /** * Get next reminder date * @param Date $now * @param ScheduleReminder $reminder * @param int $remindSeconds * @return int|null * @throws NotFoundException * @throws XiboException */ public function getNextReminderDate($now, $reminder, $remindSeconds) { // Determine toDt so that we don't getEvents which never ends // adding the recurrencedetail at the end (minute/hour/week) to make sure we get at least 2 next events $toDt = $now->copy(); // For a future event we need to forward now to event fromDt $fromDt = $this->getDate()->parse($this->fromDt, 'U'); if ( $fromDt > $toDt ) { $toDt = $fromDt; } switch ($this->recurrenceType) { case 'Minute': $toDt->minute(($toDt->minute + $this->recurrenceDetail) + $this->recurrenceDetail); break; case 'Hour': $toDt->hour(($toDt->hour + $this->recurrenceDetail) + $this->recurrenceDetail); break; case 'Day': $toDt->day(($toDt->day + $this->recurrenceDetail) + $this->recurrenceDetail); break; case 'Week': $toDt->day(($toDt->day + $this->recurrenceDetail * 7 ) + $this->recurrenceDetail); break; case 'Month': $toDt->month(($toDt->month + $this->recurrenceDetail ) + $this->recurrenceDetail); break; case 'Year': $toDt->year(($toDt->year + $this->recurrenceDetail ) + $this->recurrenceDetail); break; default: throw new InvalidArgumentException('Invalid recurrence type', 'recurrenceType'); } // toDt is set so that we get two next events from now $scheduleEvents = $this->getEvents($now, $toDt); foreach($scheduleEvents as $event) { if ($reminder->option == ScheduleReminder::$OPTION_BEFORE_START) { $reminderDt = $event->fromDt - $remindSeconds; if ($reminderDt >= $now->format('U')) { return $reminderDt; } } elseif ($reminder->option == ScheduleReminder::$OPTION_AFTER_START) { $reminderDt = $event->fromDt + $remindSeconds; if ($reminderDt >= $now->format('U')) { return $reminderDt; } } elseif ($reminder->option == ScheduleReminder::$OPTION_BEFORE_END) { $reminderDt = $event->toDt - $remindSeconds; if ($reminderDt >= $now->format('U')) { return $reminderDt; } } elseif ($reminder->option == ScheduleReminder::$OPTION_AFTER_END) { $reminderDt = $event->toDt + $remindSeconds; if ($reminderDt >= $now->format('U')) { return $reminderDt; } } } // No next event exist throw new NotFoundException('reminderDt not found as next event does not exist'); } /** * Get event title * @return string * @throws XiboException */ public function getEventTitle() { // Setting for whether we show Layouts with out permissions $showLayoutName = ($this->config->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1); // Load the display groups $this->load(); $displayGroupList = ''; if (count($this->displayGroups) >= 0) { $array = array_map(function ($object) { return $object->displayGroup; }, $this->displayGroups); $displayGroupList = implode(', ', $array); } $user = $this->userFactory->getById($this->userId); // Event Title if ($this->campaignId == 0) { // Command $title = __('%s scheduled on %s', $this->command, $displayGroupList); } else { // Should we show the Layout name, or not (depending on permission) // Make sure we only run the below code if we have to, its quite expensive if (!$showLayoutName && !$user->isSuperAdmin()) { // Campaign $campaign = $this->campaignFactory->getById($this->campaignId); if (!$user->checkViewable($campaign)) $this->campaign = __('Private Item'); } $title = __('%s scheduled on %s', $this->campaign, $displayGroupList); } return $title; } }PK qYxU U RegionOption.phpnu [ . */ namespace Xibo\Entity; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; /** * Class RegionOption * @package Xibo\Entity * * @SWG\Definition() */ class RegionOption implements \JsonSerializable { use EntityTrait; /** * @SWG\Property(description="The regionId that this Option applies to") * @var int */ public $regionId; /** * @SWG\Property(description="The option name") * @var string */ public $option; /** * @SWG\Property(description="The option value") * @var string */ public $value; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log */ public function __construct($store, $log) { $this->setCommonDependencies($store, $log); } /** * Clone */ public function __clone() { $this->regionId = null; } public function save() { $sql = 'INSERT INTO `regionoption` (`regionId`, `option`, `value`) VALUES (:regionId, :option, :value) ON DUPLICATE KEY UPDATE `value` = :value2'; $this->getStore()->insert($sql, array( 'regionId' => $this->regionId, 'option' => $this->option, 'value' => $this->value, 'value2' => $this->value, )); } public function delete() { $sql = 'DELETE FROM `regionoption` WHERE `regionId` = :regionId AND `option` = :option'; $this->getStore()->update($sql, array('regionId' => $this->regionId, 'option' => $this->option)); } }PK qYyL L PlayerVersion.phpnu [ . */ namespace Xibo\Entity; use Xibo\Exception\XiboException; use Xibo\Factory\MediaFactory; use Xibo\Factory\PlayerVersionFactory; use Xibo\Service\ConfigServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; /** * Class PlayerVersion * @package Xibo\Entity * * @SWG\Definition() */ class PlayerVersion implements \JsonSerializable { use EntityTrait; /** * @SWG\Property(description="Version ID") * @var int */ public $versionId; /** * @SWG\Property(description="Player type") * @var string */ public $type; /** * @SWG\Property(description="Version number") * @var string */ public $version; /** * @SWG\Property(description="Code number") * @var int */ public $code; /** * @SWG\Property(description="A comma separated list of groups/users with permissions to this Media") * @var string */ public $groupsWithPermissions; /** * @SWG\Property(description="The Media ID") * @var int */ public $mediaId; /** * @SWG\Property(description="Player version to show") * @var string */ public $playerShowVersion; /** * @SWG\Property(description="Original name of the uploaded installer file") * @var string */ public $originalFileName; /** * @SWG\Property(description="Stored As") * @var string */ public $storedAs; /** * @var ConfigServiceInterface */ private $config; /** * @var MediaFactory */ private $mediaFactory; /** * @var PlayerVersionFactory */ private $playerVersionFactory; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log * @param ConfigServiceInterface $config * @param MediaFactory $mediaFactory * @param PlayerVersionFactory $playerVersionFactory */ public function __construct($store, $log, $config, $mediaFactory, $playerVersionFactory) { $this->setCommonDependencies($store, $log); $this->config = $config; $this->mediaFactory = $mediaFactory; $this->playerVersionFactory = $playerVersionFactory; } /** * Add */ private function add() { $this->versionId = $this->getStore()->insert(' INSERT INTO `player_software` (`player_type`, `player_version`, `player_code`, `mediaId`, `playerShowVersion`) VALUES (:type, :version, :code, :mediaId, :playerShowVersion) ', [ 'type' => $this->type, 'version' => $this->version, 'code' => $this->code, 'mediaId' => $this->mediaId, 'playerShowVersion' => $this->playerShowVersion ]); } /** * Edit */ private function edit() { $sql = ' UPDATE `player_software` SET `player_version` = :version, `player_code` = :code, `playerShowVersion` = :playerShowVersion WHERE versionId = :versionId '; $params = [ 'version' => $this->version, 'code' => $this->code, 'playerShowVersion' => $this->playerShowVersion, 'versionId' => $this->versionId ]; $this->getStore()->update($sql, $params); } /** * Delete * @throws XiboException */ public function delete() { $this->load(); $this->getStore()->update('DELETE FROM `player_software` WHERE `versionId` = :versionId', [ 'versionId' => $this->versionId ]); } /** * Load */ public function load() { if ($this->loaded || $this->versionId == null) return; $this->loaded = true; } /** * Save this media * @param array $options */ public function save($options = []) { $options = array_merge([ 'validate' => true ], $options); if ($this->versionId == null || $this->versionId == 0) $this->add(); else $this->edit(); } }PK qY|kPh DataSetColumnType.phpnu [ setCommonDependencies($store, $log); } }PK qYAg g ScheduleEvent.phpnu [ fromDt = $fromDt; $this->toDt = $toDt; } /** * @return string */ public function __toString() { return $this->fromDt . $this->toDt; } }PK qY&z2