diff --git a/Dockerfile b/Dockerfile index 866b117863..9f7960ef37 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,6 +73,7 @@ RUN apk update && apk upgrade && apk add tar \ php7-simplexml \ php7-mbstring \ php7-memcached \ + php7-zlib \ mysql-client \ ssmtp \ apache2 \ diff --git a/Dockerfile.dev b/Dockerfile.dev index 6830d3b491..63207f0f8f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -45,6 +45,7 @@ RUN apk update && apk upgrade && apk add tar \ php7-simplexml \ php7-mbstring \ php7-memcached \ + php7-zlib \ mysql-client \ ssmtp \ apache2 \ diff --git a/install/master/constraints.sql b/install/master/constraints.sql index b4428c7fcc..8e779ae796 100644 --- a/install/master/constraints.sql +++ b/install/master/constraints.sql @@ -120,3 +120,7 @@ CREATE INDEX requiredfile_displayId_type_index ON requiredfile (displayId, type) CREATE INDEX idx_lkregionplaylist_playlistId ON lkregionplaylist (playlistId); +CREATE INDEX lkdgdg_parentId_childId_depth_index ON lkdgdg (parentId, childId, depth); + +CREATE INDEX lkdgdg_childId_parentId_depth_index ON lkdgdg (childId, parentId, depth); + diff --git a/install/master/data.sql b/install/master/data.sql index b7e7efb2a9..8dc34d9312 100644 --- a/install/master/data.sql +++ b/install/master/data.sql @@ -1,5 +1,5 @@ INSERT INTO `version` (`app_ver`, `XmdsVersion`, `XlfVersion`, `DBVersion`) VALUES -('1.8.5', 5, 2, 136); +('1.8.6', 5, 2, 137); INSERT INTO `group` (`groupID`, `group`, `IsUserSpecific`, `IsEveryone`, `isSystemNotification`) VALUES (1, 'Users', 0, 0, 0), @@ -236,7 +236,8 @@ INSERT INTO `setting` (`settingid`, `setting`, `value`, `fieldType`, `helptext`, (97, 'DISPLAY_PROFILE_SCREENSHOT_INTERVAL_ENABLED', '1', 'checkbox', NULL, NULL, 'displays', 1, 'Enable the option to set the screenshot interval?', '', 90, '0', 1, 'checkbox'), (98, 'DISPLAY_PROFILE_SCREENSHOT_SIZE_DEFAULT', '200', 'number', 'The default size in pixels for the Display Screenshots', NULL, 'displays', 1, 'Display Screenshot Default Size', '', 100, '200', 1, 'int'), (99, 'LATEST_NEWS_URL', 'http://xibo.org.uk/feed', 'text', 'RSS/Atom Feed to be displayed on the Status Dashboard', '', 'general', 0, 'Latest News URL', '', 111, '', 0, 'string'), -(100, 'DISPLAY_LOCK_NAME_TO_DEVICENAME', '0', 'checkbox', NULL, NULL, 'displays', 1, 'Lock the Display Name to the device name provided by the Player?', '', 80, '0', 1, 'checkbox'); +(100, 'DISPLAY_LOCK_NAME_TO_DEVICENAME', '0', 'checkbox', NULL, NULL, 'displays', 1, 'Lock the Display Name to the device name provided by the Player?', '', 80, '0', 1, 'checkbox'), +(101, 'mail_from_name', '', 'text', 'Mail will be sent under this name', null, 'maintenance', 1, 'Sending email name', '', 45, '', 1, 'string'); INSERT INTO `usertype` (`usertypeid`, `usertype`) VALUES (1, 'Super Admin'), @@ -322,4 +323,9 @@ INSERT INTO task (taskId, name, class, status, options, schedule, isActive, conf (3, 'Email Notifications', '\\Xibo\\XTR\\EmailNotificationsTask', 2, '[]', '*/5 * * * * *', 1, '/tasks/email-notifications.task'), (4, 'Stats Archive', '\\Xibo\\XTR\\StatsArchiveTask', 2, '{"periodSizeInDays":"7","maxPeriods":"4"}', '0 0 * * Mon', 0, '/tasks/stats-archiver.task'), (5, 'Remove old Notifications', '\\Xibo\\XTR\\NotificationTidyTask', 2, '{"maxAgeDays":"7","systemOnly":"1","readOnly":"0"}', '15 0 * * *', 1, '/tasks/notification-tidy.task'), - (6, 'Fetch Remote DataSets', '\\Xibo\\XTR\\RemoteDataSetFetchTask', 2, '[]', '30 * * * * *', 1, '/tasks/remote-dataset.task'); \ No newline at end of file + (6, 'Fetch Remote DataSets', '\\Xibo\\XTR\\RemoteDataSetFetchTask', 2, '[]', '30 * * * * *', 1, '/tasks/remote-dataset.task'); + + +INSERT INTO daypart (name, description, isRetired, userid, startTime, endTime, exceptions, isAlways, isCustom) VALUES + ('Custom', 'User specifies the from/to date', 0, 1, '', '', '', 0, 1), + ('Always', 'Event runs always', 0, 1, '', '', '', 1, 0); \ No newline at end of file diff --git a/install/master/structure.sql b/install/master/structure.sql index 0f013e5cbf..13e1086d6c 100644 --- a/install/master/structure.sql +++ b/install/master/structure.sql @@ -1032,9 +1032,7 @@ CREATE TABLE IF NOT EXISTS `useroption` ( CREATE TABLE IF NOT EXISTS `lkdgdg` ( `parentId` int(11) NOT NULL, `childId` int(11) NOT NULL, - `depth` int(11) NOT NULL, - UNIQUE KEY `parentId` (`parentId`,`childId`,`depth`), - UNIQUE KEY `childId` (`childId`,`parentId`,`depth`) + `depth` int(11) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- @@ -1127,8 +1125,10 @@ CREATE TABLE `daypart` ( `startTime` VARCHAR(8) DEFAULT '00:00:00', `endTime` VARCHAR(8) DEFAULT '00:00:00', `exceptions` TEXT NULL, + `isAlways` TINYINT(4) DEFAULT 0 NOT NULL, + `isCustom` TINYINT(4) DEFAULT 0 NOT NULL, PRIMARY KEY (`dayPartId`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=2; +) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_INCREMENT=1; CREATE TABLE `task` ( `taskId` INT(11) NOT NULL AUTO_INCREMENT, diff --git a/install/steps/137.json b/install/steps/137.json new file mode 100644 index 0000000000..2562f8d8dc --- /dev/null +++ b/install/steps/137.json @@ -0,0 +1,38 @@ +{ + "dbVersion": 137, + "appVersion": "1.8.6", + "steps": [ + { + "step": "DayPart columns to indicate always and custom", + "action": "ALTER TABLE daypart ADD isAlways TINYINT DEFAULT 0 NOT NULL, ADD isCustom TINYINT DEFAULT 0 NOT NULL;" + }, + { + "step": "Insert the always/custom day parts", + "action": "INSERT INTO daypart (name, description, isRetired, userid, startTime, endTime, exceptions, isAlways, isCustom) VALUES ('Custom', 'User specifies the from/to date', 0, 1, '', '', '', 0, 1),('Always', 'Event runs always', 0, 1, '', '', '', 1, 0);" + }, + { + "step": "Back fill the existing schedules with the correct day parts - custom", + "action": "UPDATE `schedule` SET dayPartId = (SELECT dayPartId FROM daypart WHERE isCustom = 1) WHERE dayPartId = 0;" + }, + { + "step": "Back fill the existing schedules with the correct day parts - always", + "action": "UPDATE `schedule` SET dayPartId = (SELECT dayPartId FROM daypart WHERE isAlways = 1) WHERE dayPartId = 1;" + }, + { + "step": "Add email from setting", + "action": "INSERT INTO setting (setting, value, fieldType, helptext, options, cat, userChange, title, validation, ordering, `default`, userSee, type) VALUES ('mail_from_name', '', 'text', 'Mail will be sent under this name', null, 'maintenance', 1, 'Sending email name', '', 45, '', 1, 'string');" + }, + { + "step": "Drop parent/child/depth unique keys on Display Group closure", + "action": "DROP INDEX parentId ON lkdgdg; DROP INDEX childId ON lkdgdg;" + }, + { + "step": "Create parent/child/depth index on Display Group closure", + "action": "CREATE INDEX lkdgdg_parentId_childId_depth_index ON lkdgdg (parentId, childId, depth);" + }, + { + "step": "Create parent/child/depth index on Display Group closure", + "action": "CREATE INDEX lkdgdg_childId_parentId_depth_index ON lkdgdg (childId, parentId, depth);" + } + ] +} \ No newline at end of file diff --git a/lib/Controller/Campaign.php b/lib/Controller/Campaign.php index 036a460eda..50ddbd75d6 100644 --- a/lib/Controller/Campaign.php +++ b/lib/Controller/Campaign.php @@ -444,10 +444,25 @@ public function layoutsForm($campaignId) if (!$this->getUser()->checkEditable($campaign)) throw new AccessDeniedException(); + $layouts = []; + foreach ($this->layoutFactory->getByCampaignId($campaignId, false) as $layout) { + if (!$this->getUser()->checkViewable($layout)) { + // Hide all layout details from the user + $emptyLayout = $this->layoutFactory->createEmpty(); + $emptyLayout->layoutId = $layout->layoutId; + $emptyLayout->layout = __('Layout'); + $emptyLayout->locked = true; + + $layouts[] = $emptyLayout; + } else { + $layouts[] = $layout; + } + } + $this->getState()->template = 'campaign-form-layouts'; $this->getState()->setData([ 'campaign' => $campaign, - 'layouts' => $this->layoutFactory->getByCampaignId($campaignId), + 'layouts' => $layouts, 'help' => $this->getHelp()->link('Campaign', 'Layouts') ]); } @@ -540,7 +555,9 @@ public function assignLayout($campaignId) $layout = $this->layoutFactory->getById($this->getSanitizer()->getInt('layoutId', $object)); - if (!$this->getUser()->checkViewable($layout)) + // Check to see if this layout is already assigned + // if it is, then we have permission to move it around + if (!$this->getUser()->checkViewable($layout) && !$campaign->isLayoutAssigned($layout)) throw new AccessDeniedException(__('You do not have permission to assign the provided Layout')); // Set the Display Order @@ -562,7 +579,7 @@ public function assignLayout($campaignId) $layout = $this->layoutFactory->getById($this->getSanitizer()->getInt('layoutId', $object)); - if (!$this->getUser()->checkViewable($layout)) + if (!$this->getUser()->checkViewable($layout) && !$campaign->isLayoutAssigned($layout)) throw new AccessDeniedException(__('You do not have permission to assign the provided Layout')); // Set the Display Order @@ -644,7 +661,7 @@ public function unassignLayout($campaignId) foreach ($layouts as $object) { $layout = $this->layoutFactory->getById($this->getSanitizer()->getInt('layoutId', $object)); - if (!$this->getUser()->checkViewable($layout)) + if (!$this->getUser()->checkViewable($layout) && !$campaign->isLayoutAssigned($layout)) throw new AccessDeniedException(__('You do not have permission to assign the provided Layout')); // Set the Display Order diff --git a/lib/Controller/Clock.php b/lib/Controller/Clock.php index a6bb877100..b0e0618e99 100644 --- a/lib/Controller/Clock.php +++ b/lib/Controller/Clock.php @@ -74,15 +74,23 @@ public function __construct($log, $sanitizerService, $state, $user, $help, $date * ) * ) * ) + * + * @throws \Exception */ - function clock() + public function clock() { - $output = $this->getDate()->getLocalDate(null, 'H:i T'); $this->session->refreshExpiry = false; - $this->getState()->setData(array('time' => $output)); - $this->getState()->html = $output; - $this->getState()->clockUpdate = true; - $this->getState()->success = true; + if ($this->getApp()->request()->isAjax() || $this->isApi()) { + $output = $this->getDate()->getLocalDate(null, 'H:i T'); + + $this->getState()->setData(array('time' => $output)); + $this->getState()->html = $output; + $this->getState()->clockUpdate = true; + $this->getState()->success = true; + } else { + $this->setNoOutput(true); + echo $this->getDate()->getLocalDate(null, 'c'); + } } } diff --git a/lib/Controller/DayPart.php b/lib/Controller/DayPart.php index 367a619d1c..26a3bc1d8b 100644 --- a/lib/Controller/DayPart.php +++ b/lib/Controller/DayPart.php @@ -21,6 +21,7 @@ namespace Xibo\Controller; use Xibo\Exception\AccessDeniedException; +use Xibo\Exception\XiboException; use Xibo\Factory\DayPartFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; @@ -99,7 +100,9 @@ public function grid() { $filter = [ 'dayPartId' => $this->getSanitizer()->getInt('dayPartId'), - 'name' => $this->getSanitizer()->getString('name') + 'name' => $this->getSanitizer()->getString('name'), + 'isAlways' => 0, + 'isCustom' => 0 ]; $dayParts = $this->dayPartFactory->query($this->gridRenderSort(), $this->gridRenderFilter($filter)); @@ -177,6 +180,9 @@ public function editForm($dayPartId) if (!$this->getUser()->checkEditable($dayPart)) throw new AccessDeniedException(); + if ($dayPart->isAlways === 1 || $dayPart->isCustom === 1) + throw new AccessDeniedException(); + $this->getState()->template = 'daypart-form-edit'; $this->getState()->setData([ 'dayPart' => $dayPart, @@ -197,6 +203,9 @@ public function deleteForm($dayPartId) if (!$this->getUser()->checkDeleteable($dayPart)) throw new AccessDeniedException(); + if ($dayPart->isAlways === 1 || $dayPart->isCustom === 1) + throw new AccessDeniedException(); + // Get a count of schedules for this day part $schedules = $this->scheduleFactory->getByDayPartId($dayPartId); @@ -284,7 +293,9 @@ public function add() $dayPart = $this->dayPartFactory->createEmpty(); $this->handleCommonInputs($dayPart); - $dayPart->save(); + $dayPart + ->setScheduleFactory($this->scheduleFactory) + ->save(); // Return $this->getState()->hydrate([ @@ -370,6 +381,8 @@ public function add() * @SWG\Schema(ref="#/definitions/DayPart") * ) * ) + * + * @throws XiboException */ public function edit($dayPartId) { @@ -381,8 +394,13 @@ public function edit($dayPartId) if (!$this->getUser()->checkEditable($dayPart)) throw new AccessDeniedException(); + if ($dayPart->isAlways === 1 || $dayPart->isCustom === 1) + throw new AccessDeniedException(); + $this->handleCommonInputs($dayPart); - $dayPart->save(); + $dayPart + ->setScheduleFactory($this->scheduleFactory) + ->save(); // Return $this->getState()->hydrate([ @@ -471,6 +489,8 @@ private function handleCommonInputs($dayPart) * description="successful operation" * ) * ) + * + * @throws XiboException */ public function delete($dayPartId) { @@ -479,7 +499,10 @@ public function delete($dayPartId) if (!$this->getUser()->checkDeleteable($dayPart)) throw new AccessDeniedException(); - $dayPart->setDateService($this->getDate())->delete(); + $dayPart + ->setDateService($this->getDate()) + ->setScheduleFactory($this->scheduleFactory) + ->delete(); // Return $this->getState()->hydrate([ diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index 7a6fab824d..88da3bc1d9 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -25,6 +25,7 @@ use Xibo\Exception\AccessDeniedException; use Xibo\Exception\ConfigurationException; use Xibo\Exception\NotFoundException; +use Xibo\Exception\XiboException; use Xibo\Factory\DisplayEventFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; @@ -32,9 +33,11 @@ use Xibo\Factory\LayoutFactory; use Xibo\Factory\LogFactory; use Xibo\Factory\MediaFactory; +use Xibo\Factory\NotificationFactory; use Xibo\Factory\RequiredFileFactory; use Xibo\Factory\ScheduleFactory; use Xibo\Factory\TagFactory; +use Xibo\Factory\UserGroupFactory; use Xibo\Helper\ByteFormatter; use Xibo\Helper\WakeOnLan; use Xibo\Service\ConfigServiceInterface; @@ -111,6 +114,12 @@ class Display extends Base /** @var TagFactory */ private $tagFactory; + /** @var NotificationFactory */ + private $notificationFactory; + + /** @var UserGroupFactory */ + private $userGroupFactory; + /** * Set common dependencies. * @param LogServiceInterface $log @@ -133,8 +142,10 @@ class Display extends Base * @param DisplayEventFactory $displayEventFactory * @param RequiredFileFactory $requiredFileFactory * @param TagFactory $tagFactory + * @param NotificationFactory $notificationFactory + * @param UserGroupFactory $userGroupFactory */ - public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $store, $pool, $playerAction, $displayFactory, $displayGroupFactory, $logFactory, $layoutFactory, $displayProfileFactory, $mediaFactory, $scheduleFactory, $displayEventFactory, $requiredFileFactory, $tagFactory) + public function __construct($log, $sanitizerService, $state, $user, $help, $date, $config, $store, $pool, $playerAction, $displayFactory, $displayGroupFactory, $logFactory, $layoutFactory, $displayProfileFactory, $mediaFactory, $scheduleFactory, $displayEventFactory, $requiredFileFactory, $tagFactory, $notificationFactory, $userGroupFactory) { $this->setCommonDependencies($log, $sanitizerService, $state, $user, $help, $date, $config); @@ -151,6 +162,8 @@ public function __construct($log, $sanitizerService, $state, $user, $help, $date $this->displayEventFactory = $displayEventFactory; $this->requiredFileFactory = $requiredFileFactory; $this->tagFactory = $tagFactory; + $this->notificationFactory = $notificationFactory; + $this->userGroupFactory = $userGroupFactory; } /** @@ -1289,14 +1302,14 @@ public function wakeOnLan($displayId) /** * Validate the display list * @param array[Display] $displays - * @return array[Display] + * @throws XiboException */ public function validateDisplays($displays) { - $timedOutDisplays = []; - // Get the global time out (overrides the alert time out on the display if 0) $globalTimeout = $this->getConfig()->GetSetting('MAINTENANCE_ALERT_TOUT') * 60; + $emailAlerts = ($this->getConfig()->GetSetting("MAINTENANCE_EMAIL_ALERTS") == 'On'); + $alwaysAlert = ($this->getConfig()->GetSetting("MAINTENANCE_ALWAYS_ALERT") == 'On'); foreach ($displays as $display) { /* @var \Xibo\Entity\Display $display */ @@ -1316,9 +1329,11 @@ public function validateDisplays($displays) if ($timeOut < time()) { $this->getLog()->debug('Timed out display. Last Accessed: ' . date('Y-m-d h:i:s', $display->lastAccessed) . '. Time out: ' . date('Y-m-d h:i:s', $timeOut)); - // If this is the first switch (i.e. the row was logged in before) - if ($display->loggedIn == 1) { + // Is this the first time this display has gone "off-line" + $displayOffline = ($display->loggedIn == 1); + // If this is the first switch (i.e. the row was logged in before) + if ($displayOffline) { // Update the display and set it as logged out $display->loggedIn = 0; $display->save(\Xibo\Entity\Display::$saveOptionsMinimum); @@ -1335,11 +1350,27 @@ public function validateDisplays($displays) $event->save(); } - // Store this row - $timedOutDisplays[] = $display; + // Should we create a notification + if ($emailAlerts && $display->emailAlert == 1 && ($displayOffline || $alwaysAlert)) { + // Alerts enabled for this display + // Display just gone offline, or always alert + // Fields for email + $subject = sprintf(__("Email Alert for Display %s"), $display->display); + $body = sprintf(__("Display %s with ID %d was last seen at %s."), $display->display, $display->displayId, $this->getDate()->getLocalDate($display->lastAccessed)); + + // Add to system + $notification = $this->notificationFactory->createSystemNotification($subject, $body, $this->getDate()->parse()); + + // Add in any displayNotificationGroups, with permissions + foreach ($this->userGroupFactory->getDisplayNotificationGroups($display->displayGroupId) as $group) { + $notification->assignUserGroup($group); + } + + $notification->save(); + } else if ($displayOffline) { + $this->getLog()->info('Not sending an email for offline display - emailAlert = ' . $display->emailAlert . ', alwaysAlert = ' . $alwaysAlert); + } } } - - return $timedOutDisplays; } } diff --git a/lib/Controller/Library.php b/lib/Controller/Library.php index 1b32d4767d..19810f971d 100644 --- a/lib/Controller/Library.php +++ b/lib/Controller/Library.php @@ -1426,8 +1426,6 @@ public function usage($mediaId) /* @var \Xibo\Entity\Schedule $row */ // Generate this event - $row->setDayPartFactory($this->dayPartFactory); - // Assess the date? if ($mediaDate !== null) { try { diff --git a/lib/Controller/Schedule.php b/lib/Controller/Schedule.php index f2f071acc6..57924de183 100644 --- a/lib/Controller/Schedule.php +++ b/lib/Controller/Schedule.php @@ -239,8 +239,6 @@ function eventData() /* @var \Xibo\Entity\Schedule $row */ // Generate this event - $row->setDayPartFactory($this->dayPartFactory); - try { $scheduleEvents = $row->getEvents($start, $end); } catch (XiboException $e) { @@ -418,9 +416,7 @@ public function eventList($displayGroupId) // Assess schedules $schedule = $this->scheduleFactory->createEmpty()->hydrate($event, ['intProperties' => ['isPriority', 'syncTimezone', 'displayOrder']]); - $schedule - ->setDayPartFactory($this->dayPartFactory) - ->load(); + $schedule->load(); $this->getLog()->debug('EventId ' . $schedule->eventId . ' exists in the schedule window, checking its instances for activity'); @@ -753,11 +749,16 @@ function addForm() * ) * ) * ) + * + * @throws XiboException */ public function add() { $this->getLog()->debug('Add Schedule'); + // Get the custom day part to use as a default day part + $customDayPart = $this->dayPartFactory->getCustomDayPart(); + $schedule = $this->scheduleFactory->createEmpty(); $schedule->userId = $this->getUser()->userId; $schedule->eventTypeId = $this->getSanitizer()->getInt('eventTypeId'); @@ -765,7 +766,12 @@ public function add() $schedule->commandId = $this->getSanitizer()->getInt('commandId'); $schedule->displayOrder = $this->getSanitizer()->getInt('displayOrder', 0); $schedule->isPriority = $this->getSanitizer()->getInt('isPriority', 0); - $schedule->dayPartId = $this->getSanitizer()->getInt('dayPartId', 0); + $schedule->dayPartId = $this->getSanitizer()->getInt('dayPartId', $customDayPart->dayPartId); + + // Workaround for cases where we're supplied 0 as the dayPartId (legacy custom dayPart) + if ($schedule->dayPartId === 0) + $schedule->dayPartId = $customDayPart->dayPartId; + $schedule->syncTimezone = $this->getSanitizer()->getCheckbox('syncTimezone', 0); $schedule->recurrenceType = $this->getSanitizer()->getString('recurrenceType'); $schedule->recurrenceDetail = $this->getSanitizer()->getInt('recurrenceDetail'); @@ -776,7 +782,7 @@ public function add() $schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId)); } - if ($schedule->dayPartId != \Xibo\Entity\Schedule::$DAY_PART_ALWAYS) { + if (!$schedule->isAlwaysDayPart()) { // Handle the dates $fromDt = $this->getSanitizer()->getDate('fromDt'); $toDt = $this->getSanitizer()->getDate('toDt'); @@ -787,7 +793,7 @@ public function add() $this->getLog()->debug('Times received are: FromDt=' . $this->getDate()->getLocalDate($fromDt) . '. ToDt=' . $this->getDate()->getLocalDate($toDt) . '. recurrenceRange=' . $this->getDate()->getLocalDate($recurrenceRange)); - if ($schedule->dayPartId != \Xibo\Entity\Schedule::$DAY_PART_CUSTOM && $schedule->dayPartId != \Xibo\Entity\Schedule::$DAY_PART_ALWAYS) { + if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) { // Daypart selected // expect only a start date (no time) $schedule->fromDt = $fromDt->startOfDay()->format('U'); @@ -847,7 +853,7 @@ function editForm($eventId) throw new AccessDeniedException(); // Fix the event dates for display - if ($schedule->dayPartId == \Xibo\Entity\Schedule::$DAY_PART_ALWAYS) { + if ($schedule->isAlwaysDayPart()) { $schedule->fromDt = ''; $schedule->toDt = ''; } else { @@ -1019,6 +1025,8 @@ function editForm($eventId) * @SWG\Schema(ref="#/definitions/Schedule") * ) * ) + * + * @throws XiboException */ public function edit($eventId) { @@ -1045,7 +1053,7 @@ public function edit($eventId) $schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId)); } - if ($schedule->dayPartId != \Xibo\Entity\Schedule::$DAY_PART_ALWAYS) { + if (!$schedule->isAlwaysDayPart()) { // Handle the dates $fromDt = $this->getSanitizer()->getDate('fromDt'); $toDt = $this->getSanitizer()->getDate('toDt'); @@ -1056,14 +1064,12 @@ public function edit($eventId) $this->getLog()->debug('Times received are: FromDt=' . $this->getDate()->getLocalDate($fromDt) . '. ToDt=' . $this->getDate()->getLocalDate($toDt) . '. recurrenceRange=' . $this->getDate()->getLocalDate($recurrenceRange)); - if ($schedule->dayPartId != \Xibo\Entity\Schedule::$DAY_PART_CUSTOM && $schedule->dayPartId != \Xibo\Entity\Schedule::$DAY_PART_ALWAYS) { + if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) { // Daypart selected // expect only a start date (no time) $schedule->fromDt = $fromDt->startOfDay()->format('U'); $schedule->toDt = null; - - if ($recurrenceRange != null) - $schedule->recurrenceRange = $recurrenceRange->format('U'); + $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U'); } else if (!($this->isApi() || str_contains($this->getConfig()->GetSetting('DATE_FORMAT'), 's'))) { // In some circumstances we want to trim the seconds from the provided dates. @@ -1076,16 +1082,14 @@ public function edit($eventId) if ($toDt !== null) $schedule->toDt = $toDt->setTime($toDt->hour, $toDt->minute, 0)->format('U'); - if ($recurrenceRange != null) - $schedule->recurrenceRange = $recurrenceRange->setTime($recurrenceRange->hour, $recurrenceRange->minute, 0)->format('U'); + $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->setTime($recurrenceRange->hour, $recurrenceRange->minute, 0)->format('U'); } else { $schedule->fromDt = $fromDt->format('U'); if ($toDt !== null) $schedule->toDt = $toDt->format('U'); - if ($recurrenceRange != null) - $schedule->recurrenceRange = $recurrenceRange->format('U'); + $schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U'); } $this->getLog()->debug('Processed start is: FromDt=' . $fromDt->toRssString()); diff --git a/lib/Controller/Template.php b/lib/Controller/Template.php index ab25440504..c369fdc974 100644 --- a/lib/Controller/Template.php +++ b/lib/Controller/Template.php @@ -21,6 +21,7 @@ namespace Xibo\Controller; use Xibo\Exception\AccessDeniedException; +use Xibo\Exception\XiboException; use Xibo\Factory\LayoutFactory; use Xibo\Factory\TagFactory; use Xibo\Service\ConfigServiceInterface; @@ -279,6 +280,8 @@ function addTemplateForm($layoutId) * ) * ) * ) + * + * @throws XiboException */ function add($layoutId) { @@ -305,6 +308,7 @@ function add($layoutId) $layout->tags = $this->tagFactory->tagsFromString($this->getSanitizer()->getString('tags')); $layout->tags[] = $this->tagFactory->getByTag('template'); $layout->description = $this->getSanitizer()->getString('description'); + $layout->setOwner($this->getUser()->userId, true); $layout->save(); // Return diff --git a/lib/Entity/Campaign.php b/lib/Entity/Campaign.php index 783fd24307..32791eb474 100644 --- a/lib/Entity/Campaign.php +++ b/lib/Entity/Campaign.php @@ -25,6 +25,7 @@ use Respect\Validation\Validator as v; use Xibo\Exception\InvalidArgumentException; use Xibo\Exception\NotFoundException; +use Xibo\Exception\XiboException; use Xibo\Factory\DisplayFactory; use Xibo\Factory\LayoutFactory; use Xibo\Factory\PermissionFactory; @@ -80,9 +81,20 @@ class Campaign implements \JsonSerializable public $totalDuration; public $tags = []; - + + /** + * @var Layout[] + */ private $layouts = []; + + /** + * @var Permission[] + */ private $permissions = []; + + /** + * @var Schedule[] + */ private $events = []; // Private @@ -471,6 +483,28 @@ public function unassignLayout($layout) $this->layoutAssignmentsChanged = true; } + /** + * Is the provided layout already assigned to this campaign + * @param Layout $checkLayout + * @return bool + * @throws XiboException + */ + public function isLayoutAssigned($checkLayout) + { + $assigned = false; + + $this->load(); + + foreach ($this->layouts as $layout) { + if ($layout->layoutId === $checkLayout->layoutId) { + $assigned = true; + break; + } + } + + return $assigned; + } + private function add() { $this->campaignId = $this->getStore()->insert('INSERT INTO `campaign` (Campaign, IsLayoutSpecific, UserId) VALUES (:campaign, :isLayoutSpecific, :userId)', array( diff --git a/lib/Entity/DataSet.php b/lib/Entity/DataSet.php index 4850ab0141..4f21363ef4 100644 --- a/lib/Entity/DataSet.php +++ b/lib/Entity/DataSet.php @@ -20,6 +20,7 @@ use Xibo\Factory\DisplayFactory; use Xibo\Factory\PermissionFactory; use Xibo\Service\ConfigServiceInterface; +use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Service\SanitizerServiceInterface; use Xibo\Storage\StorageServiceInterface; @@ -206,6 +207,9 @@ class DataSet implements \JsonSerializable /** @var DisplayFactory */ private $displayFactory; + /** @var DateServiceInterface */ + private $date; + /** * Entity constructor. * @param StorageServiceInterface $store @@ -217,8 +221,9 @@ class DataSet implements \JsonSerializable * @param DataSetColumnFactory $dataSetColumnFactory * @param PermissionFactory $permissionFactory * @param DisplayFactory $displayFactory + * @param DateServiceInterface $date */ - public function __construct($store, $log, $sanitizer, $config, $pool, $dataSetFactory, $dataSetColumnFactory, $permissionFactory, $displayFactory) + public function __construct($store, $log, $sanitizer, $config, $pool, $dataSetFactory, $dataSetColumnFactory, $permissionFactory, $displayFactory, $date) { $this->setCommonDependencies($store, $log); $this->sanitizer = $sanitizer; @@ -228,6 +233,7 @@ public function __construct($store, $log, $sanitizer, $config, $pool, $dataSetFa $this->dataSetColumnFactory = $dataSetColumnFactory; $this->permissionFactory = $permissionFactory; $this->displayFactory = $displayFactory; + $this->date = $date; } /** @@ -388,6 +394,9 @@ public function getData($filterBy = [], $options = []) // Keep track of the columns we are allowed to order by $allowedOrderCols = ['id']; + // Are there any client side formulas + $clientSideFormula = []; + // Select (columns) foreach ($this->getColumn() as $column) { /* @var DataSetColumn $column */ @@ -398,6 +407,13 @@ public function getData($filterBy = [], $options = []) // Formula column? if ($column->dataSetColumnTypeId == 2) { + + // Is this a client side column? + if (substr($column->formula, 0, 1) === '$') { + $clientSideFormula[] = $column; + continue; + } + $formula = str_replace($this->blackList, '', htmlspecialchars_decode($column->formula, ENT_QUOTES)); $formula = str_replace('[DisplayId]', $displayId, $formula); @@ -472,8 +488,6 @@ public function getData($filterBy = [], $options = []) $sql = $select . $body . $order . $limit; - - $data = $this->getStore()->select($sql, $params); // If there are limits run some SQL to work out the full payload of rows @@ -482,7 +496,34 @@ public function getData($filterBy = [], $options = []) $this->countLast = intval($results[0]['total']); } - return $data; + // Are there any client side formulas? + if (count($clientSideFormula) > 0) { + $renderedData = []; + foreach ($data as $item) { + foreach ($clientSideFormula as $column) { + // Run the formula and add the resulting value to the list + $value = null; + try { + if (substr($column->formula, 0, strlen('$dateFormat(')) === '$dateFormat(') { + // Pull out the column name and date format + $details = explode(',', str_replace(')', '', str_replace('$dateFormat(', '', $column->formula))); + + $value = $this->date->parse($item[$details[0]])->format($details[1]); + } + } catch (\Exception $e) { + $this->getLog()->error('DataSet client side formula error in dataSetId ' . $this->dataSetId . ' with column formula ' . $column->formula); + } + + $item[$column->heading] = $value; + } + + $renderedData[] = $item; + } + } else { + $renderedData = $data; + } + + return $renderedData; } /** diff --git a/lib/Entity/DayPart.php b/lib/Entity/DayPart.php index 723636817b..140855e24b 100644 --- a/lib/Entity/DayPart.php +++ b/lib/Entity/DayPart.php @@ -57,6 +57,18 @@ class DayPart implements \JsonSerializable public $endTime; public $exceptions; + /** + * @SWG\Property(description="A readonly flag determining whether this DayPart is always") + * @var int + */ + public $isAlways = 0; + + /** + * @SWG\Property(description="A readonly flag determining whether this DayPart is custom") + * @var int + */ + public $isCustom = 0; + private $timeHash; /** @var DateServiceInterface */ @@ -84,12 +96,20 @@ class DayPart implements \JsonSerializable * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log - * @param ScheduleFactory $scheduleFactory */ - public function __construct($store, $log, $scheduleFactory) + public function __construct($store, $log) { $this->setCommonDependencies($store, $log); + } + + /** + * @param ScheduleFactory $scheduleFactory + * @return $this + */ + public function setScheduleFactory($scheduleFactory) + { $this->scheduleFactory = $scheduleFactory; + return $this; } /** diff --git a/lib/Entity/DisplayGroup.php b/lib/Entity/DisplayGroup.php index a59c775a38..b17919488b 100644 --- a/lib/Entity/DisplayGroup.php +++ b/lib/Entity/DisplayGroup.php @@ -891,19 +891,40 @@ private function unlinkDisplayGroups() return $a->getId() - $b->getId(); }); - $this->getLog()->debug('Unlinking %d display groups to Display Group %s', count($links), $this->displayGroup); + $this->getLog()->debug('Unlinking ' . count($links) . ' display groups to Display Group ' . $this->displayGroup); foreach ($links as $displayGroup) { /* @var DisplayGroup $displayGroup */ - $this->getStore()->update(' - DELETE link - FROM `lkdgdg` p, `lkdgdg` link, `lkdgdg` c - WHERE p.parentId = link.parentId AND c.childId = link.childId - AND p.childId = :parentId AND c.parentId = :childId - ', [ + // Only ever delete 1 because if there are more than 1, we can assume that it is linked at that level from + // somewhere else + // https://github.com/xibosignage/xibo/issues/1417 + $linksToDelete = $this->getStore()->select(' + SELECT DISTINCT link.parentId, link.childId, link.depth + FROM `lkdgdg` p + INNER JOIN `lkdgdg` link + ON p.parentId = link.parentId + INNER JOIN `lkdgdg` c + ON c.childId = link.childId + WHERE p.childId = :parentId + AND c.parentId = :childId + ', [ 'parentId' => $this->displayGroupId, 'childId' => $displayGroup->displayGroupId ]); + + foreach ($linksToDelete as $linkToDelete) { + $this->getStore()->update(' + DELETE FROM `lkdgdg` + WHERE parentId = :parentId + AND childId = :childId + AND depth = :depth + LIMIT 1 + ', [ + 'parentId' => $linkToDelete['parentId'], + 'childId' => $linkToDelete['childId'], + 'depth' => $linkToDelete['depth'] + ]); + } } } diff --git a/lib/Entity/DisplayProfile.php b/lib/Entity/DisplayProfile.php index 5c94c0f332..b31d4cd21b 100644 --- a/lib/Entity/DisplayProfile.php +++ b/lib/Entity/DisplayProfile.php @@ -1152,6 +1152,8 @@ private function loadFromFile() 'tabs' => [ ['id' => 'general', 'name' => __('General')], ['id' => 'timers', 'name' => __('On/Off Time')], + ['id' => 'pictureOptions', 'name' => __('Picture')], + ['id' => 'lockOptions', 'name' => __('Lock')], ['id' => 'advanced', 'name' => __('Advanced')], ], 'settings' => [ @@ -1330,6 +1332,28 @@ private function loadFromFile() 'helpText' => __('A JSON object indicating the on/off timers to set'), 'enabled' => true, 'groupClass' => NULL + ], + [ + 'name' => 'pictureOptions', + 'tabId' => 'pictureOptions', + 'title' => __('Picture Options'), + 'type' => 'string', + 'fieldType' => 'text', + 'default' => '{}', + 'helpText' => __('A JSON object indicating the picture options to set'), + 'enabled' => true, + 'groupClass' => NULL + ], + [ + 'name' => 'lockOptions', + 'tabId' => 'lockOptions', + 'title' => __('Lock Options'), + 'type' => 'string', + 'fieldType' => 'text', + 'default' => '{}', + 'helpText' => __('A JSON object indicating the lock options to set'), + 'enabled' => true, + 'groupClass' => NULL ] ] ] diff --git a/lib/Entity/Schedule.php b/lib/Entity/Schedule.php index 6a40a81de3..2d1e59dd72 100644 --- a/lib/Entity/Schedule.php +++ b/lib/Entity/Schedule.php @@ -35,8 +35,6 @@ class Schedule implements \JsonSerializable public static $LAYOUT_EVENT = 1; public static $COMMAND_EVENT = 2; public static $OVERLAY_EVENT = 3; - public static $DAY_PART_CUSTOM = 0; - public static $DAY_PART_ALWAYS = 1; public static $DATE_MIN = 0; public static $DATE_MAX = 2147483647; @@ -179,6 +177,18 @@ class Schedule implements \JsonSerializable */ 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 @@ -226,14 +236,16 @@ class Schedule implements \JsonSerializable * @param PoolInterface $pool * @param DateServiceInterface $date * @param DisplayGroupFactory $displayGroupFactory + * @param DayPartFactory $dayPartFactory */ - public function __construct($store, $log, $config, $pool, $date, $displayGroupFactory) + public function __construct($store, $log, $config, $pool, $date, $displayGroupFactory, $dayPartFactory) { $this->setCommonDependencies($store, $log); $this->config = $config; $this->pool = $pool; $this->dateService = $date; $this->displayGroupFactory = $displayGroupFactory; + $this->dayPartFactory = $dayPartFactory; $this->excludeProperty('lastRecurrenceWatermark'); } @@ -253,16 +265,6 @@ public function setDisplayFactory($displayFactory) return $this; } - /** - * @param DayPartFactory $dayPartFactory - * @return $this - */ - public function setDayPartFactory($dayPartFactory) - { - $this->dayPartFactory = $dayPartFactory; - return $this; - } - /** * @param DateServiceInterface $dateService * @deprecated dateService is set by the factory @@ -314,10 +316,11 @@ public function setOwner($ownerId) /** * Are the provided dates within the schedule look ahead * @return bool + * @throws XiboException */ private function inScheduleLookAhead() { - if ($this->dayPartId == Schedule::$DAY_PART_ALWAYS) + if ($this->isAlwaysDayPart()) return true; // From Date and To Date are in UNIX format @@ -333,7 +336,7 @@ private function inScheduleLookAhead() // If we are a recurring schedule and our recurring date is out after the required files lookahead $this->getLog()->debug('Checking look ahead based on recurrence'); return ($this->fromDt <= $currentDate->format('U') && ($this->recurrenceRange == 0 || $this->recurrenceRange > $rfLookAhead->format('U'))); - } else if ($this->dayPartId != self::$DAY_PART_CUSTOM || $this->eventTypeId == self::$COMMAND_EVENT) { + } 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()); @@ -391,6 +394,7 @@ public function unassignDisplayGroup($displayGroup) /** * Validate + * @throws XiboException */ public function validate() { @@ -404,7 +408,7 @@ public function validate() if (!v::intType()->notEmpty()->validate($this->campaignId)) throw new InvalidArgumentException(__('Please select a Campaign/Layout for this event.'), 'campaignId'); - if ($this->dayPartId == Schedule::$DAY_PART_CUSTOM) { + 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'); @@ -426,7 +430,7 @@ public function validate() } // Make sure we have a sensible recurrence setting - if ($this->dayPartId !== self::$DAY_PART_CUSTOM && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) + 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 @@ -460,7 +464,7 @@ public function save($options = []) $this->validate(); // Handle "always" day parts - if ($this->dayPartId == self::$DAY_PART_ALWAYS) { + if ($this->isAlwaysDayPart()) { $this->fromDt = self::$DATE_MIN; $this->toDt = self::$DATE_MAX; } @@ -694,7 +698,7 @@ private function generateMonth($generateFromDt) ); // Events scheduled "always" will return one event - if ($this->dayPartId == Schedule::$DAY_PART_ALWAYS) { + if ($this->isAlwaysDayPart()) { // Create events with min/max dates $this->addDetail(Schedule::$DATE_MIN, Schedule::$DATE_MAX); return; @@ -720,7 +724,7 @@ private function generateMonth($generateFromDt) return; // Detect invalid recurrences and quit early - if ($this->dayPartId !== self::$DAY_PART_CUSTOM && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) + if (!$this->isCustomDayPart() && ($this->recurrenceType == 'Minute' || $this->recurrenceType == 'Hour')) return; // Check the cache @@ -948,12 +952,13 @@ private function dropEventCache($key = null) * 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->dayPartId != Schedule::$DAY_PART_ALWAYS && $this->dayPartId != Schedule::$DAY_PART_CUSTOM) { + if (!$this->isAlwaysDayPart() && !$this->isCustomDayPart()) { // End is always based on Start $end->setTimestamp($start->format('U')); @@ -1052,4 +1057,28 @@ private function unlinkDisplayGroups() $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; + } } \ No newline at end of file diff --git a/lib/Factory/DataSetFactory.php b/lib/Factory/DataSetFactory.php index 5ce573fab0..02ff01ff57 100644 --- a/lib/Factory/DataSetFactory.php +++ b/lib/Factory/DataSetFactory.php @@ -33,6 +33,7 @@ use Xibo\Exception\XiboException; use Xibo\Helper\Environment; use Xibo\Service\ConfigServiceInterface; +use Xibo\Service\DateServiceInterface; use Xibo\Service\LogServiceInterface; use Xibo\Service\SanitizerServiceInterface; use Xibo\Storage\StorageServiceInterface; @@ -58,6 +59,9 @@ class DataSetFactory extends BaseFactory /** @var DisplayFactory */ private $displayFactory; + /** @var DateServiceInterface */ + private $date; + /** * Construct a factory * @param StorageServiceInterface $store @@ -70,8 +74,9 @@ class DataSetFactory extends BaseFactory * @param DataSetColumnFactory $dataSetColumnFactory * @param PermissionFactory $permissionFactory * @param DisplayFactory $displayFactory + * @param DateServiceInterface $date */ - public function __construct($store, $log, $sanitizerService, $user, $userFactory, $config, $pool, $dataSetColumnFactory, $permissionFactory, $displayFactory) + public function __construct($store, $log, $sanitizerService, $user, $userFactory, $config, $pool, $dataSetColumnFactory, $permissionFactory, $displayFactory, $date) { $this->setCommonDependencies($store, $log, $sanitizerService); $this->setAclDependencies($user, $userFactory); @@ -80,6 +85,7 @@ public function __construct($store, $log, $sanitizerService, $user, $userFactory $this->dataSetColumnFactory = $dataSetColumnFactory; $this->permissionFactory = $permissionFactory; $this->displayFactory = $displayFactory; + $this->date = $date; } /** @@ -104,7 +110,8 @@ public function createEmpty() $this, $this->dataSetColumnFactory, $this->permissionFactory, - $this->displayFactory + $this->displayFactory, + $this->date ); } diff --git a/lib/Factory/DayPartFactory.php b/lib/Factory/DayPartFactory.php index ce87c5e47f..67524ca8ec 100644 --- a/lib/Factory/DayPartFactory.php +++ b/lib/Factory/DayPartFactory.php @@ -35,9 +35,6 @@ */ class DayPartFactory extends BaseFactory { - /** @var ScheduleFactory */ - private $scheduleFactory; - /** * Construct a factory * @param StorageServiceInterface $store @@ -45,13 +42,11 @@ class DayPartFactory extends BaseFactory * @param SanitizerServiceInterface $sanitizerService * @param User $user * @param UserFactory $userFactory - * @param ScheduleFactory $scheduleFactory */ - public function __construct($store, $log, $sanitizerService, $user, $userFactory, $scheduleFactory) + public function __construct($store, $log, $sanitizerService, $user, $userFactory) { $this->setCommonDependencies($store, $log, $sanitizerService); $this->setAclDependencies($user, $userFactory); - $this->scheduleFactory = $scheduleFactory; } /** @@ -62,8 +57,7 @@ public function createEmpty() { return new DayPart( $this->getStore(), - $this->getLog(), - $this->scheduleFactory + $this->getLog() ); } @@ -83,17 +77,43 @@ public function getById($dayPartId) return $dayParts[0]; } + /** + * Get the Always DayPart + * @return DayPart + * @throws NotFoundException + */ + public function getAlwaysDayPart() + { + $dayParts = $this->query(null, ['disableUserCheck' => 1, 'isAlways' => 1]); + + if (count($dayParts) <= 0) + throw new NotFoundException(); + + return $dayParts[0]; + } + + /** + * Get the Custom DayPart + * @return DayPart + * @throws NotFoundException + */ + public function getCustomDayPart() + { + $dayParts = $this->query(null, ['disableUserCheck' => 1, 'isCustom' => 1]); + + if (count($dayParts) <= 0) + throw new NotFoundException(); + + return $dayParts[0]; + } + /** * Get all dayparts with the system entries (always and custom) * @return DayPart[] */ public function allWithSystem() { - $dayParts = $this->query(); - - // Add system and custom - array_unshift($dayParts, ['dayPartId' => 1, 'name' => __('Always')]); - array_unshift($dayParts, ['dayPartId' => 0, 'name' => __('Custom')]); + $dayParts = $this->query(['isAlways DESC', 'isCustom DESC', 'name']); return $dayParts; } @@ -111,7 +131,7 @@ public function query($sortOrder = null, $filterBy = []) $sortOrder = ['name']; $params = array(); - $select = 'SELECT `daypart`.dayPartId, `name`, `description`, `isRetired`, `userId`, `startTime`, `endTime`, `exceptions`'; + $select = 'SELECT `daypart`.dayPartId, `name`, `description`, `isRetired`, `userId`, `startTime`, `endTime`, `exceptions`, `isCustom`, `isAlways` '; $body = ' FROM `daypart` '; @@ -125,6 +145,16 @@ public function query($sortOrder = null, $filterBy = []) $params['dayPartId'] = $this->getSanitizer()->getInt('dayPartId', $filterBy); } + if ($this->getSanitizer()->getInt('isAlways', $filterBy) !== null) { + $body .= ' AND `daypart`.isAlways = :isAlways '; + $params['isAlways'] = $this->getSanitizer()->getInt('isAlways', $filterBy); + } + + if ($this->getSanitizer()->getInt('isCustom', $filterBy) !== null) { + $body .= ' AND `daypart`.isCustom = :isCustom '; + $params['isCustom'] = $this->getSanitizer()->getInt('isCustom', $filterBy); + } + if ($this->getSanitizer()->getString('name', $filterBy) != null) { $body .= ' AND `daypart`.name = :name '; $params['name'] = $this->getSanitizer()->getString('name', $filterBy); @@ -144,7 +174,9 @@ public function query($sortOrder = null, $filterBy = []) $sql = $select . $body . $order . $limit; foreach ($this->getStore()->select($sql, $params) as $row) { - $dayPart = $this->createEmpty()->hydrate($row); + $dayPart = $this->createEmpty()->hydrate($row, [ + 'intProperties' => ['isAlways', 'isCustom'] + ]); $dayPart->exceptions = json_decode($dayPart->exceptions, true); $entries[] = $dayPart; diff --git a/lib/Factory/LayoutFactory.php b/lib/Factory/LayoutFactory.php index 4cbc698583..20d28ede4e 100644 --- a/lib/Factory/LayoutFactory.php +++ b/lib/Factory/LayoutFactory.php @@ -946,10 +946,9 @@ public function query($sortOrder = null, $filterBy = []) $select .= " layout.backgroundzIndex, "; $select .= " layout.schemaVersion, "; - if ($this->getSanitizer()->getInt('campaignId', 0, $filterBy) != 0) { + if ($this->getSanitizer()->getInt('campaignId', $filterBy) !== null) { $select .= ' lkcl.displayOrder, '; - } - else { + } else { $select .= ' NULL as displayOrder, '; } @@ -973,10 +972,14 @@ public function query($sortOrder = null, $filterBy = []) $body .= " AND campaign.IsLayoutSpecific = 1"; $body .= " INNER JOIN `user` ON `user`.userId = `campaign`.userId "; - if ($this->getSanitizer()->getInt('campaignId', 0, $filterBy) != 0) { + if ($this->getSanitizer()->getInt('campaignId', $filterBy) !== null) { // Join Campaign back onto it again - $body .= " INNER JOIN `lkcampaignlayout` lkcl ON lkcl.layoutid = layout.layoutid AND lkcl.CampaignID = :campaignId "; - $params['campaignId'] = $this->getSanitizer()->getInt('campaignId', 0, $filterBy); + $body .= " + INNER JOIN `lkcampaignlayout` lkcl + ON lkcl.layoutid = layout.layoutid + AND lkcl.CampaignID = :campaignId + "; + $params['campaignId'] = $this->getSanitizer()->getInt('campaignId', $filterBy); } if ($this->getSanitizer()->getInt('displayGroupId', $filterBy) !== null) { diff --git a/lib/Factory/MediaFactory.php b/lib/Factory/MediaFactory.php index 54e3e6b3e9..ad4c43cb7b 100644 --- a/lib/Factory/MediaFactory.php +++ b/lib/Factory/MediaFactory.php @@ -630,7 +630,11 @@ public function query($sortOrder = null, $filterBy = []) // Expired files? if ($this->getSanitizer()->getInt('expires', $filterBy) != 0) { - $body .= ' AND media.expires < :expires AND IFNULL(media.expires, 0) <> 0 '; + $body .= ' + AND media.expires < :expires + AND IFNULL(media.expires, 0) <> 0 + AND media.mediaId NOT IN (SELECT mediaId FROM `lkwidgetmedia`) + '; $params['expires'] = $this->getSanitizer()->getInt('expires', $filterBy); } diff --git a/lib/Factory/ScheduleFactory.php b/lib/Factory/ScheduleFactory.php index e28366328d..a3ff80e1d4 100644 --- a/lib/Factory/ScheduleFactory.php +++ b/lib/Factory/ScheduleFactory.php @@ -41,6 +41,9 @@ class ScheduleFactory extends BaseFactory */ private $displayGroupFactory; + /** @var DayPartFactory */ + private $dayPartFactory; + /** * Construct a factory * @param StorageServiceInterface $store @@ -50,14 +53,16 @@ class ScheduleFactory extends BaseFactory * @param PoolInterface $pool * @param DateServiceInterface $date * @param DisplayGroupFactory $displayGroupFactory + * @param DayPartFactory $dayPartFactory */ - public function __construct($store, $log, $sanitizerService, $config, $pool, $date, $displayGroupFactory) + public function __construct($store, $log, $sanitizerService, $config, $pool, $date, $displayGroupFactory, $dayPartFactory) { $this->setCommonDependencies($store, $log, $sanitizerService); $this->config = $config; $this->pool = $pool; $this->dateService = $date; $this->displayGroupFactory = $displayGroupFactory; + $this->dayPartFactory = $dayPartFactory; } /** @@ -72,7 +77,8 @@ public function createEmpty() $this->config, $this->pool, $this->dateService, - $this->displayGroupFactory + $this->displayGroupFactory, + $this->dayPartFactory ); } @@ -179,8 +185,12 @@ public function getForXmds($displayId, $fromDt, $toDt, $options = []) schedule.syncTimezone, `campaign`.campaign, `command`.command, - `lkscheduledisplaygroup`.displayGroupId + `lkscheduledisplaygroup`.displayGroupId, + `daypart`.isAlways, + `daypart`.isCustom FROM `schedule` + INNER JOIN `daypart` + ON `daypart`.dayPartId = `schedule`.dayPartId INNER JOIN `lkscheduledisplaygroup` ON `lkscheduledisplaygroup`.eventId = `schedule`.eventId INNER JOIN `lkdgdg` @@ -263,8 +273,12 @@ public function query($sortOrder = null, $filterBy = []) `command`.commandId, `command`.command, `schedule`.dayPartId, - `schedule`.syncTimezone + `schedule`.syncTimezone, + `daypart`.isAlways, + `daypart`.isCustom FROM `schedule` + INNER JOIN `daypart` + ON `daypart`.dayPartId = `schedule`.dayPartId LEFT OUTER JOIN `campaign` ON campaign.CampaignID = `schedule`.CampaignID LEFT OUTER JOIN `command` @@ -364,7 +378,7 @@ public function query($sortOrder = null, $filterBy = []) $sql .= 'ORDER BY ' . implode(',', $sortOrder); foreach ($this->getStore()->select($sql, $params) as $row) { - $entries[] = $this->createEmpty()->hydrate($row, ['intProperties' => ['isPriority', 'syncTimezone']]); + $entries[] = $this->createEmpty()->hydrate($row, ['intProperties' => ['isPriority', 'syncTimezone', 'isAlways', 'isCustom']]); } return $entries; diff --git a/lib/Helper/Environment.php b/lib/Helper/Environment.php index 608b4105c5..0f32a94a31 100644 --- a/lib/Helper/Environment.php +++ b/lib/Helper/Environment.php @@ -11,8 +11,8 @@ class Environment { - public static $WEBSITE_VERSION_NAME = '1.8.5'; - public static $WEBSITE_VERSION = 136; + public static $WEBSITE_VERSION_NAME = '1.8.6'; + public static $WEBSITE_VERSION = 137; public static $VERSION_REQUIRED = '5.5'; public static $VERSION_UNSUPPORTED = '8.0'; diff --git a/lib/Middleware/State.php b/lib/Middleware/State.php index 86b3105cd6..a03ed8b60d 100644 --- a/lib/Middleware/State.php +++ b/lib/Middleware/State.php @@ -75,7 +75,11 @@ public function call() $app->response()->header('strict-transport-security', 'max-age=' . $app->configService->GetSetting('STS_TTL', 600)); } else { - if ($app->configService->GetSetting('FORCE_HTTPS', 0) == 1) { + // Get the current route pattern + $resource = $app->router->getCurrentRoute()->getPattern(); + + // Allow non-https access to the clock page, otherwise force https + if ($resource !== '/clock' && $app->configService->GetSetting('FORCE_HTTPS', 0) == 1) { $redirect = "https://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']; header("Location: $redirect"); $app->halt(302); @@ -504,7 +508,9 @@ public static function registerControllersWithDi($app) $container->scheduleFactory, $container->displayEventFactory, $container->requiredFileFactory, - $container->tagFactory + $container->tagFactory, + $container->notificationFactory, + $container->userGroupFactory ); }); @@ -1114,7 +1120,8 @@ public static function registerFactoriesWithDi($container) $container->pool, $container->dataSetColumnFactory, $container->permissionFactory, - $container->displayFactory + $container->displayFactory, + $container->dateService ); }); @@ -1132,8 +1139,7 @@ public static function registerFactoriesWithDi($container) $container->logService, $container->sanitizerService, $container->user, - $container->userFactory, - $container->scheduleFactory + $container->userFactory ); }); @@ -1342,7 +1348,8 @@ public static function registerFactoriesWithDi($container) $container->configService, $container->pool, $container->dateService, - $container->displayGroupFactory + $container->displayGroupFactory, + $container->dayPartFactory ); }); diff --git a/lib/Service/DisplayNotifyService.php b/lib/Service/DisplayNotifyService.php index 33b2c49d15..24f8923e46 100644 --- a/lib/Service/DisplayNotifyService.php +++ b/lib/Service/DisplayNotifyService.php @@ -209,8 +209,20 @@ public function notifyByCampaignId($campaignId) ON lkdisplaydg.DisplayGroupID = `lkdgdg`.childId INNER JOIN `display` ON lkdisplaydg.DisplayID = display.displayID - WHERE `schedule`.campaignId = :activeCampaignId - AND ( + INNER JOIN ( + SELECT campaignId + FROM campaign + WHERE campaign.campaignId = :activeCampaignId + UNION + SELECT DISTINCT parent.campaignId + FROM `lkcampaignlayout` child + INNER JOIN `lkcampaignlayout` parent + ON parent.layoutId = child.layoutId + WHERE child.campaignId = :activeCampaignId + + ) campaigns + ON campaigns.campaignId = `schedule`.campaignId + WHERE ( (`schedule`.FromDT < :toDt AND IFNULL(`schedule`.toDt, `schedule`.fromDt) > :fromDt) OR `schedule`.recurrence_range >= :fromDt OR ( @@ -268,7 +280,6 @@ public function notifyByCampaignId($campaignId) if ($row['eventId'] != 0) { $scheduleEvents = $this->scheduleFactory ->createEmpty() - ->setDayPartFactory($this->dayPartFactory) ->hydrate($row) ->getEvents($currentDate, $rfLookAhead); @@ -405,7 +416,6 @@ public function notifyByDataSetId($dataSetId) if ($row['eventId'] != 0) { $scheduleEvents = $this->scheduleFactory ->createEmpty() - ->setDayPartFactory($this->dayPartFactory) ->hydrate($row) ->getEvents($currentDate, $rfLookAhead); @@ -520,7 +530,6 @@ public function notifyByPlaylistId($playlistId) if ($row['eventId'] != 0) { $scheduleEvents = $this->scheduleFactory ->createEmpty() - ->setDayPartFactory($this->dayPartFactory) ->hydrate($row) ->getEvents($currentDate, $rfLookAhead); diff --git a/lib/Widget/ModuleWidget.php b/lib/Widget/ModuleWidget.php index 5fd2ee26b4..16765758b5 100644 --- a/lib/Widget/ModuleWidget.php +++ b/lib/Widget/ModuleWidget.php @@ -32,6 +32,7 @@ use Xibo\Exception\ControllerNotImplemented; use Xibo\Exception\InvalidArgumentException; use Xibo\Exception\NotFoundException; +use Xibo\Exception\XiboException; use Xibo\Factory\CommandFactory; use Xibo\Factory\DataSetColumnFactory; use Xibo\Factory\DataSetFactory; @@ -371,22 +372,56 @@ public function dumpCacheForModule() /** * Hold a lock on concurrent requests * blocks if the request is locked - * @param $key - * @param $ttl - * @param $wait - * @param $tries + * @param string|null $key + * @param int $ttl seconds + * @param int $wait seconds + * @param int $tries + * @throws XiboException */ - public function concurrentRequestLock($key = null, $ttl = 30, $wait = 5000, $tries = 3) + public function concurrentRequestLock($key = null, $ttl = 900, $wait = 5, $tries = 100) { // If the key is null default to the widgetId if ($key === null) $key = $this->widget->widgetId; $this->lock = $this->getPool()->getItem('locks/widget/' . $key); - $this->lock->setInvalidationMethod(Invalidation::SLEEP, $wait, $tries); + // Set the invalidation method to simply return the value (not that we use it, but it gets us a miss on expiry) + $this->lock->setInvalidationMethod(Invalidation::VALUE); + + // Get the lock + // other requests will wait here until we're done, or we've timed out $this->lock->get(); - $this->lock->lock($ttl); + + // Did we get a lock? + // if we're a miss, then we're not already locked + if ($this->lock->isMiss()) { + // so lock now + $this->lock->set(true); + $this->lock->expiresAfter($ttl); + $this->lock->save(); + + //sleep(30); + } else { + // We are a hit - we must be locked + $this->getLog()->debug('LOCK hit for ' . $key); + + // Try again? + $tries--; + + if ($tries <= 0) { + // We've waited long enough + throw new XiboException('Concurrent record locked, time out.'); + } else { + $this->getLog()->debug('Unable to get a lock, trying again. Remaining retries: ' . $tries); + + // Hang about waiting for the lock to be released. + sleep($wait); + + // Recursive request (we've decremented the number of tries) + $this->concurrentRequestLock($key, $ttl, $wait, $tries); + } + } } /** @@ -395,10 +430,10 @@ public function concurrentRequestLock($key = null, $ttl = 30, $wait = 5000, $tri public function concurrentRequestRelease() { if ($this->lock !== null) { - $this->lock->set(time()); + // Release lock + $this->lock->set(false); $this->lock->expiresAfter(1); // Expire straight away - // Release lock $this->getPool()->saveDeferred($this->lock); } } @@ -1166,11 +1201,17 @@ public function templatesAvailable($loadImage = true) foreach (glob(PROJECT_ROOT . '/modules/' . $this->module->type . '/*.template.json') as $template) { // Read the contents, json_decode and add to the array $template = json_decode(file_get_contents($template), true); - $template['fileName'] = $template['image']; - if ($loadImage) { - // We ltrim this because the control is expecting a relative URL - $template['image'] = ltrim($this->getApp()->urlFor('module.getTemplateImage', ['type' => $this->module->type, 'templateId' => $template['id']]), '/'); + if (isset($template['image'])) { + $template['fileName'] = $template['image']; + + if ($loadImage) { + // We ltrim this because the control is expecting a relative URL + $template['image'] = ltrim($this->getApp()->urlFor('module.getTemplateImage', ['type' => $this->module->type, 'templateId' => $template['id']]), '/'); + } + } else { + $template['fileName'] = ''; + $template['image'] = ''; } $this->module->settings['templates'][] = $template; diff --git a/lib/Widget/Ticker.php b/lib/Widget/Ticker.php index 14229a92d7..6e6c384de6 100644 --- a/lib/Widget/Ticker.php +++ b/lib/Widget/Ticker.php @@ -683,7 +683,7 @@ public function getResource($displayId = 0) 'takeItemsFrom' => $takeItemsFrom, 'itemsPerPage' => $itemsPerPage, 'randomiseItems' => $this->getOption('randomiseItems', 0), - 'speed' => $this->getOption('speed'), + 'speed' => $this->getOption('speed', 1000), 'originalWidth' => $this->region->width, 'originalHeight' => $this->region->height, 'previewWidth' => $this->getSanitizer()->getDouble('width', 0), @@ -828,10 +828,28 @@ private function getRssItems($isPreview, $text) try { // Create a Guzzle Client to get the Feed XML $client = new Client(); - $response = $client->get($feedUrl, $this->getConfig()->getGuzzleProxy()); + $response = $client->get($feedUrl, $this->getConfig()->getGuzzleProxy([ + 'headers' => [ + 'Accept' => 'application/rss+xml, application/rdf+xml;q=0.8, application/atom+xml;q=0.6, application/xml;q=0.4, text/xml;q=0.4' + ], + 'stream' => true, + 'timeout' => 20 // wait no more than 20 seconds: https://github.com/xibosignage/xibo/issues/1401 + ])); + + // Pull out the content type + $contentType = $response->getHeaderLine('Content-Type'); + + $this->getLog()->debug('Feed returned content-type ' . $contentType); + + // https://github.com/xibosignage/xibo/issues/1401 + if (stripos($contentType, 'rss') === false && stripos($contentType, 'xml') === false) { + // The content type isn't compatible + $this->getLog()->error('Incompatible content type: ' . $contentType); + return false; + } - // Pull out the content type and body - $result = explode('charset=', $response->getHeaderLine('Content-Type')); + // Get the body, etc + $result = explode('charset=', $contentType); $document['encoding'] = isset($result[1]) ? $result[1] : ''; $document['xml'] = $response->getBody()->getContents(); @@ -849,8 +867,7 @@ private function getRssItems($isPreview, $text) $this->getLog()->error('Unable to get feed: ' . $requestException->getMessage()); $this->getLog()->debug($requestException->getTraceAsString()); - $document['xml'] = null; - $document['encoding'] = null; + return false; } } diff --git a/lib/XTR/EmailNotificationsTask.php b/lib/XTR/EmailNotificationsTask.php index d9405f2ad8..df302c6a4d 100644 --- a/lib/XTR/EmailNotificationsTask.php +++ b/lib/XTR/EmailNotificationsTask.php @@ -34,7 +34,8 @@ private function processQueue() // Handle queue of notifications to email. $this->runMessage .= '## ' . __('Email Notifications') . PHP_EOL; - $msgFrom = $this->config->GetSetting("mail_from"); + $msgFrom = $this->config->GetSetting('mail_from'); + $msgFromName = $this->config->GetSetting('mail_from_name'); $this->log->debug('Notification Queue sending from ' . $msgFrom); @@ -54,7 +55,10 @@ private function processQueue() // Send them an email $mail = new \PHPMailer\PHPMailer\PHPMailer(); $mail->From = $msgFrom; - $mail->FromName = $this->config->getThemeConfig('theme_name'); + + if ($msgFromName != null) + $mail->FromName = $msgFromName; + $mail->Subject = $notification->subject; $mail->addAddress($notification->email); diff --git a/lib/XTR/MaintenanceRegularTask.php b/lib/XTR/MaintenanceRegularTask.php index 212aa89641..d861dbcd88 100644 --- a/lib/XTR/MaintenanceRegularTask.php +++ b/lib/XTR/MaintenanceRegularTask.php @@ -40,54 +40,15 @@ public function run() /** * Display Down email alerts + * - just runs validate displays */ private function displayDownEmailAlerts() { $this->runMessage .= '## ' . __('Email Alerts') . PHP_EOL; - $emailAlerts = ($this->config->GetSetting("MAINTENANCE_EMAIL_ALERTS") == 'On'); - $alwaysAlert = ($this->config->GetSetting("MAINTENANCE_ALWAYS_ALERT") == 'On'); - - foreach ($this->app->container->get('\Xibo\Controller\Display')->setApp($this->app)->validateDisplays($this->displayFactory->query()) as $display) { - /* @var \Xibo\Entity\Display $display */ - // Is this the first time this display has gone "off-line" - $displayGoneOffline = ($display->loggedIn == 1); - - // Should we send an email? - if ($emailAlerts) { - // Alerts enabled for this display - if ($display->emailAlert == 1) { - // Display just gone offline, or always alert - if ($displayGoneOffline || $alwaysAlert) { - // Fields for email - $subject = sprintf(__("Email Alert for Display %s"), $display->display); - $body = sprintf(__("Display %s with ID %d was last seen at %s."), $display->display, $display->displayId, $this->date->getLocalDate($display->lastAccessed)); - - // Add to system - $notification = $this->notificationFactory->createSystemNotification($subject, $body, $this->date->parse()); - - // Add in any displayNotificationGroups, with permissions - foreach ($this->userGroupFactory->getDisplayNotificationGroups($display->displayGroupId) as $group) { - $notification->assignUserGroup($group); - } - - $notification->save(); + $this->app->container->get('\Xibo\Controller\Display')->setApp($this->app)->validateDisplays($this->displayFactory->query()); - $this->runMessage .= ' - A' . PHP_EOL; - } else { - $this->runMessage .= ' - U' . PHP_EOL; - } - } - else { - // Alert disabled for this display - $this->runMessage .= ' - D' . PHP_EOL; - } - } - else { - // Email alerts disabled globally - $this->runMessage .= ' - X' . PHP_EOL; - } - } + $this->appendRunMessage(__('Done')); } /** @@ -172,13 +133,13 @@ private function wakeOnLan() $this->runMessage .= ' - ' . $display->display . ' Error=' . $e->getMessage() . PHP_EOL; } } - else + else { $this->runMessage .= ' - ' . $display->display . ' Display already awake. Previous WOL send time: ' . $this->date->getLocalDate($display->lastWakeOnLanCommandSent) . PHP_EOL; + } } - else + else { $this->runMessage .= ' - ' . $display->display . ' Sleeping' . PHP_EOL; - - $this->runMessage .= ' - ' . $display->display . ' N/A' . PHP_EOL; + } } $this->runMessage .= ' - Done' . PHP_EOL . PHP_EOL; diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php index 761988814c..e7d2a8b5fa 100644 --- a/lib/Xmds/Soap.php +++ b/lib/Xmds/Soap.php @@ -401,7 +401,6 @@ protected function doRequiredFiles($serverKey, $hardwareKey, $httpDownloads) if ($row['scheduleId'] != 0) { $schedule = $this->scheduleFactory->createEmpty()->hydrate($row); - $schedule->setDayPartFactory($this->dayPartFactory); try { $scheduleEvents = $schedule->getEvents($fromFilter, $toFilter); @@ -862,7 +861,6 @@ protected function doSchedule($serverKey, $hardwareKey, $options = []) foreach ($events as $row) { $schedule = $this->scheduleFactory->createEmpty()->hydrate($row); - $schedule->setDayPartFactory($this->dayPartFactory); try { $scheduleEvents = $schedule->getEvents($fromFilter, $toFilter); diff --git a/locale/dbtranslate.php b/locale/dbtranslate.php index 42d809c520..d6b7dd5d20 100644 --- a/locale/dbtranslate.php +++ b/locale/dbtranslate.php @@ -165,6 +165,8 @@ echo __('Latest News URL'); echo __('RSS/Atom Feed to be displayed on the Status Dashboard'); echo __('Lock the Display Name to the device name provided by the Player?'); +echo __('Sending email name'); +echo __('Mail will be sent under this name'); // Transitions echo __('Fade In'); diff --git a/modules/clock-get-resource-flip.twig b/modules/clock-get-resource-flip.twig index bdfa0869f3..f48ea7f35f 100644 --- a/modules/clock-get-resource-flip.twig +++ b/modules/clock-get-resource-flip.twig @@ -22,11 +22,6 @@ .flip-clock-wrapper { text-align: center; position: relative; - {% if clockFace == "DailyCounter" %} - width: 620px; - {% else %} - width: 460px; - {% endif %} margin: 0; } @@ -42,37 +37,6 @@ {{ javaScript|raw }} diff --git a/modules/forecastio/weathericons-regular-webfont.svg b/modules/forecastio/player/weathericons-regular-webfont.svg similarity index 100% rename from modules/forecastio/weathericons-regular-webfont.svg rename to modules/forecastio/player/weathericons-regular-webfont.svg diff --git a/modules/forecastio/weathericons-regular-webfont.ttf b/modules/forecastio/player/weathericons-regular-webfont.ttf similarity index 100% rename from modules/forecastio/weathericons-regular-webfont.ttf rename to modules/forecastio/player/weathericons-regular-webfont.ttf diff --git a/modules/forecastio/weathericons-regular-webfont.woff b/modules/forecastio/player/weathericons-regular-webfont.woff similarity index 100% rename from modules/forecastio/weathericons-regular-webfont.woff rename to modules/forecastio/player/weathericons-regular-webfont.woff diff --git a/modules/forecastio/weathericons-regular-webfont.woff2 b/modules/forecastio/player/weathericons-regular-webfont.woff2 similarity index 100% rename from modules/forecastio/weathericons-regular-webfont.woff2 rename to modules/forecastio/player/weathericons-regular-webfont.woff2 diff --git a/modules/text-form-add.twig b/modules/text-form-add.twig index d23e4cced2..7e4e46cc2c 100644 --- a/modules/text-form-add.twig +++ b/modules/text-form-add.twig @@ -115,7 +115,7 @@
- {% for item in media %} {% if item.mediaType == "image" %} diff --git a/modules/text-form-edit.twig b/modules/text-form-edit.twig index 8b1057bd2c..1f00efc295 100644 --- a/modules/text-form-edit.twig +++ b/modules/text-form-edit.twig @@ -117,7 +117,7 @@
- {% for item in media %} {% if item.mediaType == "image" %} diff --git a/modules/ticker-form-edit.twig b/modules/ticker-form-edit.twig index 03522cc560..31933a20c8 100644 --- a/modules/ticker-form-edit.twig +++ b/modules/ticker-form-edit.twig @@ -292,7 +292,22 @@ {% set fieldJavaScript %}{{ forms.textarea("javaScript", "", module.getRawNode("javaScript"), title, "", "", 10) }}{% endset %} {% set helpText %}{% trans "A message to display when no data is returned from the source" %}{% endset %} - {% set fieldNoDataMessage %}{{ forms.textarea("noDataMessage", "", module.getRawNode("noDataMessage"), helpText, "", "", 5) }}{% endset %} + {% set fieldNoDataMessage %} +
+ +
+ +
+
+ {{ forms.textarea("noDataMessage", "", module.getRawNode("noDataMessage"), helpText, "", "", 5) }} + {% endset %} {# Different fields for each type of Ticker #} {% if module.getOption("sourceId") == 1 %} @@ -317,19 +332,38 @@ {% set helpText %}{% trans "Tick if you would like to override the template." %}{% endset %} {{ forms.checkbox("overrideTemplate", title, module.getOption("overrideTemplate", 0), helpText) }} -
-
{% trans "Available Substitutions" %}
-
    -
  • Name
  • -
  • Title
  • -
  • Description
  • -
  • Date
  • -
  • Content
  • -
  • Copyright
  • -
  • Link
  • -
  • PermaLink
  • -
  • Tag|Namespace
  • -
+
+
+
+
{% trans "Available Substitutions" %}
+
    +
  • Name
  • +
  • Title
  • +
  • Description
  • +
  • Date
  • +
  • Content
  • +
  • Copyright
  • +
  • Link
  • +
  • PermaLink
  • +
  • Tag|Namespace
  • +
+
+
+
+
+ +
+ +
+
+
{{ fieldTemplate }} @@ -412,13 +446,30 @@ {{ fieldSpeed }}
-
-
{% trans "Available Substitutions" %}
-
    - {% for column in module.dataSetColumns() %} -
  • {{ column.heading }}
  • - {% endfor %} -
+
+
+
{% trans "Available Substitutions" %}
+
    + {% for column in module.dataSetColumns() %} +
  • {{ column.heading }}
  • + {% endfor %} +
+
+
+
+
+ +
+ +
+
{{ fieldTemplate }} {{ fieldStyleSheet }} diff --git a/modules/xibo-text-render.js b/modules/xibo-text-render.js index 6109d8a9b5..c6939d24e9 100644 --- a/modules/xibo-text-render.js +++ b/modules/xibo-text-render.js @@ -204,11 +204,17 @@ jQuery.fn.extend({ height: height }); + var timeout = duration * 1000; + + if (options.fx !== "noTransition") { + timeout = timeout - (options.speed * 0.7); + } + // Cycle handles this for us $(this).cycle({ fx: (options.fx === "noTransition") ? "none" : options.fx, speed: (options.fx === "noTransition") ? 1 : options.speed, - timeout: (duration * 1000) - (options.speed * 0.7), + timeout: timeout, slides: "> " + slides }); } diff --git a/tests/integration/Cache/LayoutInCampaignStatusTest.php b/tests/integration/Cache/LayoutInCampaignStatusTest.php new file mode 100644 index 0000000000..fb66518755 --- /dev/null +++ b/tests/integration/Cache/LayoutInCampaignStatusTest.php @@ -0,0 +1,138 @@ + + public function setup() + { + parent::setup(); + + $this->getLogger()->debug('Setup test for Cache ' . get_class() .' Test'); + + // Create a Campaign + $this->campaign = (new XiboCampaign($this->getEntityProvider()))->create(Random::generateString()); + + // Create a Layout + $this->layout = $this->createLayout(); + + // Create a text widget on the Layout + $response = $this->getEntityProvider()->post('/playlist/widget/text/' . $this->layout->regions[0]->playlists[0]['playlistId'], [ + 'text' => 'Widget A', + 'duration' => 100, + 'useDuration' => 1 + ]); + + $this->widget = (new XiboText($this->getEntityProvider()))->hydrate($response); + + // Assign the layout to our campaign + $this->campaign->assignLayout($this->layout->layoutId); + + // Create a Display + $this->display = $this->createDisplay(); + + // Schedule the Campaign "always" onto our display + // deleting the layout will remove this at the end + $event = (new XiboSchedule($this->getEntityProvider()))->createEventLayout( + date('Y-m-d H:i:s', time()+3600), + date('Y-m-d H:i:s', time()+7200), + $this->campaign->campaignId, + [$this->display->displayGroupId], + 0, + NULL, + NULL, + NULL, + 0, + 0 + ); + + $this->displaySetStatus($this->display, Display::$STATUS_DONE); + $this->displaySetLicensed($this->display); + + $this->getLogger()->debug('Finished Setup'); + } + + public function tearDown() + { + $this->getLogger()->debug('Tear Down'); + + parent::tearDown(); + + // Delete the Layout we've been working with + $this->deleteLayout($this->layout); + + // Delete the Display + $this->deleteDisplay($this->display); + + // Delete the Campaign + $this->campaign->delete(); + } + // + + /** + * @group cacheInvalidateTests + */ + public function testInvalidateCache() + { + // Make sure our Layout is already status 1 + $this->assertTrue($this->layoutStatusEquals($this->layout, 3), 'Layout Status isnt as expected'); + + // Make sure our Display is already DONE + $this->assertTrue($this->displayStatusEquals($this->display, Display::$STATUS_DONE), 'Display Status isnt as expected'); + + // Build the Layout + $this->client->get('/layout/status/' . $this->layout->layoutId); + + // Check the Layout Status + // Validate the layout status afterwards + $this->assertTrue($this->layoutStatusEquals($this->layout, 1), 'Layout Status isnt as expected'); + + // Validate the display status afterwards + $this->assertTrue($this->displayStatusEquals($this->display, Display::$STATUS_PENDING), 'Display Status isnt as expected'); + + // Validate that XMR has been called. + $this->assertTrue(in_array($this->display->displayId, $this->getPlayerActionQueue()), 'Player action not present'); + } +} \ No newline at end of file diff --git a/views/campaign-form-layouts.twig b/views/campaign-form-layouts.twig index d482be9b99..8b38f40eaa 100644 --- a/views/campaign-form-layouts.twig +++ b/views/campaign-form-layouts.twig @@ -31,7 +31,7 @@
    {% for item in layouts %} -
  • {{ item.layout }}
  • +
  • {% if item.locked %} {% endif %}{{ item.layout }}
  • {% endfor %}
diff --git a/views/licence.twig b/views/licence.twig index 629fbf1014..e73a828ef1 100644 --- a/views/licence.twig +++ b/views/licence.twig @@ -1,7 +1,7 @@

License Digital Signage for Everyone

Xibo Digital Signage - www.xibo.org.uk. Version {{ version }}
- Copyright © 2006-2016 Daniel Garner, Alex Harrington, Spring Signage Ltd and - the Xibo Developers.

+ Copyright © 2006-2018 Spring Signage Ltd and the + Xibo Developers.

Xibo is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by diff --git a/views/schedule-form-add.twig b/views/schedule-form-add.twig index 64961770bb..07b0096ae9 100644 --- a/views/schedule-form-add.twig +++ b/views/schedule-form-add.twig @@ -63,7 +63,19 @@ {% set title %}{% trans "Dayparting" %}{% endset %} {% set helpText %}{% trans "Select the dayparting information for this event. To set your own times select custom and to have the event run constantly select Always." %}{% endset %} - {{ forms.dropdown("dayPartId", "single", title, "", dayParts, "dayPartId", "name", helpText, "day-part-control") }} +

+ +
+ +
+
{% set title %}{% trans "Start Time" %}{% endset %} {% set helpText %}{% trans "Select the start time for this event" %}{% endset %} diff --git a/views/schedule-form-edit.twig b/views/schedule-form-edit.twig index 7161fd6d35..4a9f70b2b9 100644 --- a/views/schedule-form-edit.twig +++ b/views/schedule-form-edit.twig @@ -66,7 +66,19 @@ {% set title %}{% trans "Dayparting" %}{% endset %} {% set helpText %}{% trans "Select the dayparting information for this event. To set your own times select custom and to have the event run constantly select Always." %}{% endset %} - {{ forms.dropdown("dayPartId", "single", title, event.dayPartId, dayParts, "dayPartId", "name", helpText, "day-part-control") }} +
+ +
+ +
+
{% set title %}{% trans "Start Time" %}{% endset %} {% set helpText %}{% trans "Select the start time for this event" %}{% endset %} diff --git a/views/schedule-page.twig b/views/schedule-page.twig index 518bbba97f..d1c16a55a0 100644 --- a/views/schedule-page.twig +++ b/views/schedule-page.twig @@ -200,7 +200,7 @@ eventClass = "event-success"; } - if (event.event.dayPartId == 1) { + if (event.event.isAlways == 1) { eventIcon = "fa-retweet"; } @@ -287,7 +287,7 @@ eventClass = "event-success"; } - if (event.event.dayPartId == 1) { + if (event.event.isAlways == 1) { eventIcon = "fa-retweet"; } diff --git a/web/theme/default/css/xibo.css b/web/theme/default/css/xibo.css index d28612f2ff..7bb1fdd174 100644 --- a/web/theme/default/css/xibo.css +++ b/web/theme/default/css/xibo.css @@ -936,4 +936,8 @@ ul li.active.tab-design:hover a { .permissionsDiv .badge { margin: 2px; +} + +.template-override-controls .well { + min-height: 100px; } \ No newline at end of file diff --git a/web/theme/default/js/xibo-calendar.js b/web/theme/default/js/xibo-calendar.js index c77a898430..7588503b9f 100644 --- a/web/theme/default/js/xibo-calendar.js +++ b/web/theme/default/js/xibo-calendar.js @@ -486,9 +486,11 @@ var processScheduleFormElements = function(el) { if (!el.is(":visible")) return; - - var endTimeControlDisplay = (fieldVal != 0) ? "none" : "block"; - var startTimeControlDisplay = (fieldVal == 1) ? "none" : "block"; + + var meta = el.find('option[value=' + fieldVal + ']').data(); + + var endTimeControlDisplay = (meta.isCustom === 0) ? "none" : "block"; + var startTimeControlDisplay = (meta.isAlways === 1) ? "none" : "block"; var $startTime = $(".starttime-control"); var $endTime = $(".endtime-control"); @@ -497,8 +499,9 @@ var processScheduleFormElements = function(el) { $startTime.css('display', startTimeControlDisplay); $endTime.css('display', endTimeControlDisplay); + // Dayparts only show the start control - if (fieldVal != 0 && fieldVal != 1) { + if (meta.isAlways === 0 && meta.isCustom === 0) { // We need to update the date/time controls to only accept the date element $startTime.find("input[name=fromDt_Link2]").hide(); $startTime.find(".help-block").html($startTime.closest("form").data().notDaypartMessage); diff --git a/web/theme/default/js/xibo-cms.js b/web/theme/default/js/xibo-cms.js index f7716c5e8e..c13bbf9f4d 100644 --- a/web/theme/default/js/xibo-cms.js +++ b/web/theme/default/js/xibo-cms.js @@ -250,23 +250,31 @@ function XiboInitialise(scope) { minView: 2, calendarType: calendarType }).change(function() { - var value = moment($(this).val(), jsDateFormat); - - // Get the current master data var preset = $("#" + $(this).data().linkCombined); - var updatedMaster = (preset.val() == "") ? moment() : moment(preset.val(), systemDateFormat); - - if (!updatedMaster.isValid()) - updatedMaster = moment(); + var value = $(this).val(); - updatedMaster.year(value.year()); - updatedMaster.month(value.month()); - updatedMaster.date(value.date()); - - preset.val(updatedMaster.format(systemDateFormat)); + // The user has wiped the value (it is empty, so we should set the hidden field to empty) + if (value === "") { + preset.val(""); + } else { + // Parse the value into a moment + value = moment($(this).val(), jsDateFormat); + + // Get the current master data (if empty, then assume now) + var updatedMaster = (preset.val() === "") ? moment() : moment(preset.val(), systemDateFormat); + + if (!updatedMaster.isValid()) + updatedMaster = moment(); + + updatedMaster.year(value.year()); + updatedMaster.month(value.month()); + updatedMaster.date(value.date()); + + preset.val(updatedMaster.format(systemDateFormat)); + } }); - if (preset != undefined && preset != "") + if (preset !== undefined && preset !== "") $(this).find(".dateTimePickerDate").datetimepicker('update', moment(preset, systemDateFormat).format(systemDateFormat)); // Time control @@ -274,23 +282,28 @@ function XiboInitialise(scope) { 'timeFormat': timeFormat, 'step': 15 }).change(function() { - var value = moment($(this).val(), jsTimeFormat); - - // Get the current master data + var value = $(this).val(); var preset = $("#" + $(this).data().linkCombined); - - var updatedMaster = (preset.val() == "") ? moment() : moment(preset.val(), systemDateFormat); - if (!updatedMaster.isValid()) - updatedMaster = moment(); - - updatedMaster.hour(value.hour()); - updatedMaster.minute(value.minute()); - updatedMaster.second(value.second()); - preset.val(updatedMaster.format(systemDateFormat)); + if (value === "") { + preset.val(""); + } else { + value = moment($(this).val(), jsTimeFormat); + + // Get the current master data + var updatedMaster = (preset.val() === "") ? moment() : moment(preset.val(), systemDateFormat); + if (!updatedMaster.isValid()) + updatedMaster = moment(); + + updatedMaster.hour(value.hour()); + updatedMaster.minute(value.minute()); + updatedMaster.second(value.second()); + + preset.val(updatedMaster.format(systemDateFormat)); + } }); - if (preset != undefined && preset != "") + if (preset !== undefined && preset !== "") $(this).find(".dateTimePickerTime").timepicker('setTime', moment(preset, systemDateFormat).toDate()); }); diff --git a/web/theme/default/js/xibo-forms.js b/web/theme/default/js/xibo-forms.js index 3316348b5a..10ae45cc0d 100644 --- a/web/theme/default/js/xibo-forms.js +++ b/web/theme/default/js/xibo-forms.js @@ -25,7 +25,7 @@ var text_callback = function(dialog, extraData) { "transform-origin: 0 0; }" + "h1, h2, h3, h4, p { margin-top: 0;}" + ""); - } + }; var applyTemplateContentIfNecessary = function(data, extra) { // Check to see if the override template check box is unchecked @@ -52,7 +52,20 @@ var text_callback = function(dialog, extraData) { } return data; - } + }; + + var convertLibraryReferences = function(data) { + // We need to convert any library references [123] to their full URL counterparts + // we leave well alone non-library references. + var regex = /\[[0-9]+]/gi; + + data = data.replace(regex, function (match) { + var inner = match.replace("]", "").replace("[", ""); + return CKEDITOR_DEFAULT_CONFIG.imageDownloadUrl.replace(":id", inner); + }); + + return data; + }; // Conjure up a text editor CKEDITOR.replace("ta_text", CKEDITOR_DEFAULT_CONFIG); @@ -75,17 +88,7 @@ var text_callback = function(dialog, extraData) { // Handle initial template set up data = applyTemplateContentIfNecessary(data, extra); - - // We need to convert any library references [123] to their full URL counterparts - // we leave well alone non-library references. - var regex = /\[[0-9]+]/gi; - - data = data.replace(regex, function (match) { - var inner = match.replace("]", "").replace("[", ""); - var replacement = CKEDITOR_DEFAULT_CONFIG.imageDownloadUrl.replace(":id", inner); - //console.log("match = " + match + ". replacement = " + replacement); - return replacement; - }); + data = convertLibraryReferences(data); CKEDITOR.instances["ta_text"].setData(data); }); @@ -104,6 +107,15 @@ var text_callback = function(dialog, extraData) { // Reapply the background style after switching to source view and back to the normal editing view CKEDITOR.instances["noDataMessage"].on('contentDom', applyContentsToIframe); + + // Get the template data + var data = CKEDITOR.instances["noDataMessage"].getData(); + + // Handle initial template set up + data = applyTemplateContentIfNecessary(data, extra); + data = convertLibraryReferences(data); + + CKEDITOR.instances["noDataMessage"].setData(data); }); } @@ -144,7 +156,7 @@ var text_callback = function(dialog, extraData) { }); // Do we have a media selector? - $("#ckeditor_library_select").selectpicker({ + $(".ckeditor_library_select").selectpicker({ liveSearch: true }).on('changed.bs.select', function (e) { var select = $(e.target);