diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index 00e71c7597..71a2f66d5e 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -32,11 +32,13 @@ jobs: # Step 4: The Rest - name: Check Comment id: check_comment + env: + COMMENT_BODY: ${{ github.event.comment.body }} run: | - comment_body="${{ github.event.comment.body }}" - if [[ "$comment_body" =~ TEST:\ (all|\*) ]]; then + # Sanitize and validate comment body for TEST command + if [[ "$COMMENT_BODY" =~ ^TEST:\ (all|\*)$ ]]; then test_all=true - elif [[ "$comment_body" =~ TEST:(.*?)END ]]; then + elif [[ "$COMMENT_BODY" =~ ^TEST:(.*?)END$ ]]; then specs="${BASH_REMATCH[1]}" # Split the content by commas and remove spaces IFS=',' read -ra content_array <<< "$specs" @@ -46,7 +48,7 @@ jobs: content_array=("${content_array[@]%/}") # Add "/app/cypress/e2e/" prefix to each element for ((i=0; i<${#content_array[@]}; i++)); do - content_array[$i]="/app/cypress/e2e/${content_array[$i]}" + content_array[$i]="/app/cypress/e2e/${content_array[$i]}" done # Join the content array elements with commas content_no_spaces="$(IFS=','; echo "${content_array[*]}")" diff --git a/lib/Connector/OpenWeatherMapConnector.php b/lib/Connector/OpenWeatherMapConnector.php index 9f35768f61..26ca577f18 100644 --- a/lib/Connector/OpenWeatherMapConnector.php +++ b/lib/Connector/OpenWeatherMapConnector.php @@ -44,7 +44,6 @@ class OpenWeatherMapConnector implements ConnectorInterface private $forecast3Hourly = '2.5/forecast'; private $forecastDaily = '2.5/forecast/daily'; private $forecastCombinedV3 = '3.0/onecall'; - private $forecastUv = 'uvi'; /** @var string */ protected $timezone; @@ -52,9 +51,6 @@ class OpenWeatherMapConnector implements ConnectorInterface /** @var \Xibo\Widget\DataType\Forecast */ protected $currentDay; - /** @var \Xibo\Widget\DataType\Forecast[] */ - protected $forecast; - public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface { $dispatcher->addListener(WidgetDataRequestEvent::$NAME, [$this, 'onDataRequest']); @@ -90,8 +86,7 @@ public function processSettingsForm(SanitizerInterface $params, array $settings) { if (!$this->isProviderSetting('owmApiKey')) { $settings['owmApiKey'] = $params->getString('owmApiKey'); - $settings['owmApiVersion'] = $params->getString('owmApiVersion'); - $settings['owmIsPaidPlan'] = $params->getString('owmIsPaidPlan'); + $settings['owmIsPaidPlan'] = $params->getCheckbox('owmIsPaidPlan'); $settings['cachePeriod'] = $params->getInt('cachePeriod'); } return $settings; diff --git a/lib/Controller/DataSet.php b/lib/Controller/DataSet.php index e83879a8c0..ad19aeaf95 100644 --- a/lib/Controller/DataSet.php +++ b/lib/Controller/DataSet.php @@ -1198,24 +1198,17 @@ public function import(Request $request, Response $response, $id) $sanitizer = $this->getSanitizer($request->getParams()); - $options = array( 'userId' => $this->getUser()->userId, 'dataSetId' => $id, 'controller' => $this, - 'upload_dir' => $libraryFolder . 'temp/', - 'download_via_php' => true, - 'script_url' => $this->urlFor($request,'dataSet.import', ['id' => $id]), - 'upload_url' => $this->urlFor($request,'dataSet.import', ['id' => $id]), - 'image_versions' => array(), 'accept_file_types' => '/\.csv/i', 'sanitizer' => $sanitizer ); try { // Hand off to the Upload Handler provided by jquery-file-upload - new DataSetUploadHandler($options); - + new DataSetUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options); } catch (\Exception $e) { // We must not issue an error, the file upload return should have the error object already $this->getState()->setCommitState(false); diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index d447a7863e..3ba8d1ec52 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -1867,7 +1867,6 @@ function edit(Request $request, Response $response, $id) // Tags are stored on the displaygroup, we're just passing through here if ($this->getUser()->featureEnabled('tag.tagging')) { - $display->setOriginalValue('tags', $display->tags); if (is_array($sanitizedParams->getParam('tags'))) { $tags = $this->tagFactory->tagsFromJson($sanitizedParams->getArray('tags')); } else { diff --git a/lib/Controller/Font.php b/lib/Controller/Font.php index 55e23c8ccc..35ab4e95d0 100644 --- a/lib/Controller/Font.php +++ b/lib/Controller/Font.php @@ -29,7 +29,6 @@ use Xibo\Factory\FontFactory; use Xibo\Helper\ByteFormatter; use Xibo\Helper\HttpCacheProvider; -use Xibo\Helper\UploadHandler; use Xibo\Service\DownloadService; use Xibo\Service\MediaService; use Xibo\Service\MediaServiceInterface; @@ -381,9 +380,6 @@ public function add(Request $request, Response $response) $libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024; $options = [ - 'upload_dir' => $libraryFolder . 'temp/', - 'script_url' => $this->urlFor($request, 'font.add'), - 'upload_url' => $this->urlFor($request, 'font.add'), 'accept_file_types' => '/\.' . implode('|', $validExt) . '$/i', 'libraryLimit' => $libraryLimit, 'libraryQuotaFull' => ($libraryLimit > 0 && $this->getMediaService()->libraryUsage() > $libraryLimit), @@ -395,10 +391,10 @@ public function add(Request $request, Response $response) $this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options)); // Hand off to the Upload Handler provided by jquery-file-upload - $uploadService = new UploadService($options, $this->getLog(), $this->getState()); + $uploadService = new UploadService($libraryFolder . 'temp/', $options, $this->getLog(), $this->getState()); $uploadHandler = $uploadService->createUploadHandler(); - $uploadHandler->setPostProcessor(function ($file, $uploadHandler) { + $uploadHandler->setPostProcessor(function ($file, $uploadHandler) use ($libraryFolder) { // Return right away if the file already has an error. if (!empty($file->error)) { return $file; @@ -406,9 +402,8 @@ public function add(Request $request, Response $response) $this->getUser()->isQuotaFullByUser(true); - /** @var UploadHandler $uploadHandler */ - $filePath = $uploadHandler->getUploadPath() . $file->fileName; - $libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION'); + // Get the uploaded file and move it to the right place + $filePath = $libraryFolder . 'temp/' . $file->fileName; // Add the Font $font = $this->getFontFactory() @@ -424,7 +419,7 @@ public function add(Request $request, Response $response) } // everything is fine, move the file from temp folder. - rename($filePath, $libraryLocation . 'fonts/' . $font->fileName); + rename($filePath, $libraryFolder . 'fonts/' . $font->fileName); // return $file->id = $font->id; @@ -434,6 +429,7 @@ public function add(Request $request, Response $response) return $file; }); + // Handle the post request $uploadHandler->post(); // all done, refresh fonts.css diff --git a/lib/Controller/Layout.php b/lib/Controller/Layout.php index 9b388c66b8..7a63d92ce4 100644 --- a/lib/Controller/Layout.php +++ b/lib/Controller/Layout.php @@ -2560,10 +2560,6 @@ public function import(Request $request, Response $response) 'userId' => $this->getUser()->userId, 'controller' => $this, 'dataSetFactory' => $this->getDataSetFactory(), - 'upload_dir' => $libraryFolder . 'temp/', - 'download_via_php' => true, - 'script_url' => $this->urlFor($request, 'layout.import'), - 'upload_url' => $this->urlFor($request, 'layout.import'), 'image_versions' => [], 'accept_file_types' => '/\.zip$/i', 'libraryLimit' => $libraryLimit, @@ -2576,7 +2572,7 @@ public function import(Request $request, Response $response) $this->setNoOutput(true); // Hand off to the Upload Handler provided by jquery-file-upload - new LayoutUploadHandler($options); + new LayoutUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options); // Explicitly set the Content-Type header to application/json $response = $response->withHeader('Content-Type', 'application/json'); diff --git a/lib/Controller/Library.php b/lib/Controller/Library.php index e1dffd0d14..0992a00555 100644 --- a/lib/Controller/Library.php +++ b/lib/Controller/Library.php @@ -1199,11 +1199,6 @@ public function add(Request $request, Response $response) 'allowMediaTypeChange' => $options['allowMediaTypeChange'], 'displayOrder' => $parsedBody->getInt('displayOrder'), 'playlistId' => $parsedBody->getInt('playlistId'), - 'upload_dir' => $libraryFolder . 'temp/', - 'download_via_php' => true, - 'script_url' => $this->urlFor($request,'library.add'), - 'upload_url' => $this->urlFor($request,'library.add'), - 'image_versions' => [], 'accept_file_types' => '/\.' . implode('|', $validExt) . '$/i', 'libraryLimit' => $libraryLimit, 'libraryQuotaFull' => ($libraryLimit > 0 && $this->getMediaService()->libraryUsage() > $libraryLimit), @@ -1220,7 +1215,7 @@ public function add(Request $request, Response $response) $this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options)); // Hand off to the Upload Handler provided by jquery-file-upload - new XiboUploadHandler($options); + new XiboUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options); // Explicitly set the Content-Type header to application/json $response = $response->withHeader('Content-Type', 'application/json'); diff --git a/lib/Controller/Notification.php b/lib/Controller/Notification.php index b33feb17be..185dc0f754 100644 --- a/lib/Controller/Notification.php +++ b/lib/Controller/Notification.php @@ -404,11 +404,6 @@ public function addAttachment(Request $request, Response $response) $options = array( 'userId' => $this->getUser()->userId, 'controller' => $this, - 'upload_dir' => $libraryFolder . 'temp/', - 'download_via_php' => true, - 'script_url' => $this->urlFor($request,'notification.add'), - 'upload_url' => $this->urlFor($request,'notification.add'), - 'image_versions' => array(), 'accept_file_types' => '/\.jpg|.jpeg|.png|.bmp|.gif|.zip|.pdf/i' ); @@ -418,7 +413,7 @@ public function addAttachment(Request $request, Response $response) $this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options)); // Hand off to the Upload Handler provided by jquery-file-upload - new AttachmentUploadHandler($options); + new AttachmentUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options); // Explicitly set the Content-Type header to application/json $response = $response->withHeader('Content-Type', 'application/json'); diff --git a/lib/Controller/PlayerSoftware.php b/lib/Controller/PlayerSoftware.php index a9a79d5e41..2e33712787 100644 --- a/lib/Controller/PlayerSoftware.php +++ b/lib/Controller/PlayerSoftware.php @@ -30,7 +30,6 @@ use Xibo\Factory\ModuleFactory; use Xibo\Factory\PlayerVersionFactory; use Xibo\Helper\ByteFormatter; -use Xibo\Helper\UploadHandler; use Xibo\Service\DownloadService; use Xibo\Service\MediaService; use Xibo\Service\MediaServiceInterface; @@ -576,9 +575,6 @@ public function add(Request $request, Response $response) $libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024; $options = [ - 'upload_dir' => $libraryFolder . 'temp/', - 'script_url' => $this->urlFor($request, 'playersoftware.add'), - 'upload_url' => $this->urlFor($request, 'playersoftware.add'), 'accept_file_types' => '/\.' . implode('|', $validExt) . '$/i', 'libraryLimit' => $libraryLimit, 'libraryQuotaFull' => ($libraryLimit > 0 && $this->getMediaService()->libraryUsage() > $libraryLimit), @@ -590,10 +586,10 @@ public function add(Request $request, Response $response) $this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options)); // Hand off to the Upload Handler provided by jquery-file-upload - $uploadService = new UploadService($options, $this->getLog(), $this->getState()); + $uploadService = new UploadService($libraryFolder . 'temp/', $options, $this->getLog(), $this->getState()); $uploadHandler = $uploadService->createUploadHandler(); - $uploadHandler->setPostProcessor(function ($file, $uploadHandler) { + $uploadHandler->setPostProcessor(function ($file, $uploadHandler) use ($libraryFolder) { // Return right away if the file already has an error. if (!empty($file->error)) { $this->getState()->setCommitState(false); @@ -602,9 +598,8 @@ public function add(Request $request, Response $response) $this->getUser()->isQuotaFullByUser(true); - /** @var UploadHandler $uploadHandler */ - $filePath = $uploadHandler->getUploadPath() . $file->fileName; - $libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION'); + // Get the uploaded file and move it to the right place + $filePath = $libraryFolder . 'temp/' . $file->fileName; // Add the Player Software record $playerSoftware = $this->getPlayerVersionFactory()->createEmpty(); @@ -633,7 +628,7 @@ public function add(Request $request, Response $response) } // everything is fine, move the file from temp folder. - rename($filePath, $libraryLocation . 'playersoftware/' . $playerSoftware->fileName); + rename($filePath, $libraryFolder . 'playersoftware/' . $playerSoftware->fileName); // return $file->id = $playerSoftware->versionId; diff --git a/lib/Controller/Schedule.php b/lib/Controller/Schedule.php index efdf7c0099..7988ec9b79 100644 --- a/lib/Controller/Schedule.php +++ b/lib/Controller/Schedule.php @@ -2284,8 +2284,9 @@ public function grid(Request $request, Response $response) ], $params) ); - // Setting for whether we show Layouts with out permissions + // Grab some settings which determine how events are displayed. $showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1); + $defaultTimezone = $this->getConfig()->getSetting('defaultTimezone'); foreach ($events as $event) { $event->load(); @@ -2348,15 +2349,25 @@ public function grid(Request $request, Response $response) $i++; } } else if ($event->recurrenceType === 'Month') { + // Force the timezone for this date (schedule from/to dates are timezone agnostic, but this + // date still has timezone information, which could lead to use formatting as the wrong day) + $date = Carbon::parse($event->fromDt)->tz($defaultTimezone); + $this->getLog()->debug('grid: setting description for monthly event with date: ' + . $date->toAtomString()); + if ($event->recurrenceMonthlyRepeatsOn === 0) { - $repeatsOn = 'the ' . Carbon::parse($event->fromDt)->format('jS') . ' day of the month'; + $repeatsOn = 'the ' . $date->format('jS') . ' day of the month'; } else { - $date = Carbon::parse($event->fromDt); + // Which day of the month is this? $firstDay = Carbon::parse('first ' . $date->format('l') . ' of ' . $date->format('F')); + + $this->getLog()->debug('grid: the first day of the month for this date is: ' + . $firstDay->toAtomString()); + $nth = $firstDay->diffInDays($date) / 7 + 1; $repeatWeekDayDate = $date->copy()->setDay($nth)->format('jS'); $repeatsOn = 'the ' . $repeatWeekDayDate . ' ' - . Carbon::parse($event->fromDt)->format('l') + . $date->format('l') . ' of the month'; } } @@ -2391,6 +2402,16 @@ public function grid(Request $request, Response $response) $event->toDt = $event->fromDt; } + // Set the row from/to date to be an ISO date for display (no timezone) + $event->setUnmatchedProperty( + 'displayFromDt', + Carbon::createFromTimestamp($event->fromDt)->format(DateFormatHelper::getSystemFormat()) + ); + $event->setUnmatchedProperty( + 'displayToDt', + Carbon::createFromTimestamp($event->toDt)->format(DateFormatHelper::getSystemFormat()) + ); + if ($this->isApi($request)) { continue; } diff --git a/lib/Controller/StatusDashboard.php b/lib/Controller/StatusDashboard.php index 45b2689adc..3a6f7c5a03 100644 --- a/lib/Controller/StatusDashboard.php +++ b/lib/Controller/StatusDashboard.php @@ -310,16 +310,9 @@ public function displayPage(Request $request, Response $response) $data['libraryWidgetData'] = json_encode($libraryUsage); // Get a count of users - $data['countUsers'] = count($this->userFactory->query()); + $data['countUsers'] = $this->userFactory->count(); // Get a count of active layouts, only for display groups we have permission for - $displayGroups = $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1]); - $displayGroupIds = array_map(function ($element) { - return $element->displayGroupId; - }, $displayGroups); - // Add an empty one - $displayGroupIds[] = -1; - $params = ['now' => Carbon::now()->format('U')]; $sql = ' @@ -422,9 +415,7 @@ public function displayPage(Request $request, Response $response) // Display Status and Media Inventory data - Level one $displays = $this->displayFactory->query(); - $displayIds = []; $displayLoggedIn = []; - $displayNames = []; $displayMediaStatus = []; $displaysOnline = 0; $displaysOffline = 0; @@ -432,8 +423,6 @@ public function displayPage(Request $request, Response $response) $displaysMediaNotUpToDate = 0; foreach ($displays as $display) { - $displayIds[] = $display->displayId; - $displayNames[] = $display->display; $displayLoggedIn[] = $display->loggedIn; $displayMediaStatus[] = $display->mediaInventoryStatus; } @@ -456,7 +445,6 @@ public function displayPage(Request $request, Response $response) $data['displayStatus'] = json_encode([$displaysOnline, $displaysOffline]); $data['displayMediaStatus'] = json_encode([$displaysMediaUpToDate, $displaysMediaNotUpToDate]); - $data['displayLabels'] = json_encode($displayNames); } catch (Exception $e) { $this->getLog()->error($e->getMessage()); $this->getLog()->debug($e->getTraceAsString()); diff --git a/lib/Controller/Tag.php b/lib/Controller/Tag.php index f4fb47a657..6332e27dd2 100644 --- a/lib/Controller/Tag.php +++ b/lib/Controller/Tag.php @@ -27,6 +27,7 @@ use Xibo\Event\DisplayGroupLoadEvent; use Xibo\Event\TagDeleteEvent; use Xibo\Event\TagEditEvent; +use Xibo\Event\TriggerTaskEvent; use Xibo\Factory\CampaignFactory; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; @@ -743,7 +744,10 @@ public function editMultiple(Request $request, Response $response) // Once we're done, and if we're a Display entity, we need to calculate the dynamic display groups if ($targetType === 'display') { // Background update. - $this->getConfig()->changeSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 1); + $this->getDispatcher()->dispatch( + new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESSED'), + TriggerTaskEvent::$NAME + ); } } else { $this->getLog()->debug('Tags were not changed'); diff --git a/lib/Controller/Widget.php b/lib/Controller/Widget.php index 3d1a5224e3..bd2afe7904 100644 --- a/lib/Controller/Widget.php +++ b/lib/Controller/Widget.php @@ -1692,7 +1692,18 @@ public function saveElements(Request $request, Response $response, $id) } if ($uniqueSlots > 0) { + $currentItemsPerPage = $widget->getOptionValue('itemsPerPage', null); + $widget->setOptionValue('itemsPerPage', 'attrib', $uniqueSlots); + + // We should calculate the widget duration as it might have changed + if ($currentItemsPerPage != $uniqueSlots) { + $this->getLog()->debug('saveElements: updating unique slots to ' . $uniqueSlots + . ', currentItemsPerPage: ' . $currentItemsPerPage); + + $module = $this->moduleFactory->getByType($widget->type); + $widget->calculateDuration($module); + } } // Save elements diff --git a/lib/Entity/Display.php b/lib/Entity/Display.php index d4a09ebd2e..8c04c7abc0 100644 --- a/lib/Entity/Display.php +++ b/lib/Entity/Display.php @@ -25,6 +25,7 @@ use Respect\Validation\Validator as v; use Stash\Interfaces\PoolInterface; use Xibo\Event\DisplayGroupLoadEvent; +use Xibo\Event\TriggerTaskEvent; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; use Xibo\Factory\DisplayProfileFactory; @@ -884,7 +885,10 @@ public function save($options = []) // Trigger an update of all dynamic DisplayGroups? if ($this->hasPropertyChanged('display') || $this->hasPropertyChanged('tags')) { // Background update. - $this->config->changeSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 1); + $this->getDispatcher()->dispatch( + new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESSED'), + TriggerTaskEvent::$NAME + ); } } diff --git a/lib/Entity/Media.php b/lib/Entity/Media.php index 47c22d3af4..5ac8e48b8c 100644 --- a/lib/Entity/Media.php +++ b/lib/Entity/Media.php @@ -836,19 +836,21 @@ private function assessDimensions(): void * Release an image from image processing * @param $md5 * @param $fileSize + * @param $height + * @param $width */ - public function release($md5, $fileSize) + public function release($md5, $fileSize, $height, $width) { - // Update the MD5 and fileSize - $this->getStore()->update('UPDATE `media` SET md5 = :md5, fileSize = :fileSize, released = :released, modifiedDt = :modifiedDt WHERE mediaId = :mediaId', [ + // Update the img record + $this->getStore()->update('UPDATE `media` SET md5 = :md5, fileSize = :fileSize, released = :released, height = :height, width = :width, modifiedDt = :modifiedDt WHERE mediaId = :mediaId', [ 'fileSize' => $fileSize, 'md5' => $md5, 'released' => 1, 'mediaId' => $this->mediaId, + 'height' => $height, + 'width' => $width, 'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()) ]); - $this->getLog()->debug('Updating image md5 and fileSize. MediaId '. $this->mediaId); - } /** diff --git a/lib/Entity/Notification.php b/lib/Entity/Notification.php index 4dd3961848..b142e36eef 100644 --- a/lib/Entity/Notification.php +++ b/lib/Entity/Notification.php @@ -257,16 +257,19 @@ public function load($options = []) * Save Notification * @throws InvalidArgumentException */ - public function save() + public function save(): void { $this->validate(); - if ($this->notificationId == null) + $isNewRecord = false; + if ($this->notificationId == null) { + $isNewRecord = true; $this->add(); - else + } else { $this->edit(); + } - $this->manageAssignments(); + $this->manageAssignments($isNewRecord); } /** @@ -346,13 +349,20 @@ private function edit() /** * Manage assignements in DB */ - private function manageAssignments() + private function manageAssignments(bool $isNewRecord): void { $this->linkUserGroups(); - $this->unlinkUserGroups(); + + // Only unlink if we're not new (otherwise there is no point as we can't have any links yet) + if (!$isNewRecord) { + $this->unlinkUserGroups(); + } $this->linkDisplayGroups(); - $this->unlinkDisplayGroups(); + + if (!$isNewRecord) { + $this->unlinkDisplayGroups(); + } $this->manageRealisedUserLinks(); } @@ -360,10 +370,11 @@ private function manageAssignments() /** * Manage the links in the User notification table */ - private function manageRealisedUserLinks() + private function manageRealisedUserLinks(bool $isNewRecord = false): void { - // Delete links that no longer exist - $this->getStore()->update(' + if (!$isNewRecord) { + // Delete links that no longer exist + $this->getStore()->update(' DELETE FROM `lknotificationuser` WHERE `notificationId` = :notificationId AND `userId` NOT IN ( SELECT `userId` @@ -373,9 +384,10 @@ private function manageRealisedUserLinks() WHERE `lknotificationgroup`.notificationId = :notificationId2 ) AND userId <> 0 ', [ - 'notificationId' => $this->notificationId, - 'notificationId2' => $this->notificationId - ]); + 'notificationId' => $this->notificationId, + 'notificationId2' => $this->notificationId + ]); + } // Pop in new links following from this adjustment $this->getStore()->update(' diff --git a/lib/Event/TriggerTaskEvent.php b/lib/Event/TriggerTaskEvent.php index 444c499ff9..18d0965b32 100644 --- a/lib/Event/TriggerTaskEvent.php +++ b/lib/Event/TriggerTaskEvent.php @@ -22,33 +22,26 @@ namespace Xibo\Event; +/** + * An event which triggers the provided task to Run Now (at the next XTR poll) + * optionally clears a cache key to provide further instructions to the task that's running + */ class TriggerTaskEvent extends Event { - public static $NAME = 'trigger.task.event'; - /** - * @var string - */ - private $className; - /** - * @var string - */ - private $setting; - /** - * @var mixed|null - */ - private $settingValue; + public static string $NAME = 'trigger.task.event'; /** - * @param string $className + * @param string $className Class name of the task to be run + * @param string $key Cache Key to be dropped */ - public function __construct(string $className, string $setting = '', $value = null) - { - $this->className = $className; - $this->setting = $setting; - $this->settingValue = $value; + public function __construct( + private readonly string $className, + private readonly string $key = '' + ) { } /** + * Returns the class name for the task to be run * @return string */ public function getClassName(): string @@ -56,13 +49,12 @@ public function getClassName(): string return $this->className; } - public function getSetting(): string - { - return $this->setting; - } - - public function getSettingValue() + /** + * Returns the cache key to be dropped + * @return string + */ + public function getKey(): string { - return $this->settingValue; + return $this->key; } } diff --git a/lib/Factory/ConnectorFactory.php b/lib/Factory/ConnectorFactory.php index f7c5b9932e..e00d2a0874 100644 --- a/lib/Factory/ConnectorFactory.php +++ b/lib/Factory/ConnectorFactory.php @@ -1,8 +1,8 @@ setFactories($this->container) ->useLogger($this->getLog()->getLoggerInterface()) - ->useSettings($this->config->getConnectorSettings($out->getSourceName()), true) ->useSettings($connector->settings) + ->useSettings($this->config->getConnectorSettings($out->getSourceName()), true) ->useHttpOptions($this->config->getGuzzleProxy()) ->useJwtService($this->jwtService) ->usePool($this->pool); diff --git a/lib/Factory/ModuleXmlTrait.php b/lib/Factory/ModuleXmlTrait.php index ec223f5f9d..d1b4743eb3 100644 --- a/lib/Factory/ModuleXmlTrait.php +++ b/lib/Factory/ModuleXmlTrait.php @@ -122,9 +122,9 @@ private function parseProperties($propertyNodes, ?Module $module = null): array $property->allowLibraryRefs = $node->getAttribute('allowLibraryRefs') === 'true'; $property->allowAssetRefs = $node->getAttribute('allowAssetRefs') === 'true'; $property->parseTranslations = $node->getAttribute('parseTranslations') === 'true'; + $property->saveDefault = $node->getAttribute('saveDefault') === 'true'; $property->title = __($this->getFirstValueOrDefaultFromXmlNode($node, 'title')); $property->helpText = __($this->getFirstValueOrDefaultFromXmlNode($node, 'helpText')); - $property->value = $this->getFirstValueOrDefaultFromXmlNode($node, 'value'); $property->dependsOn = $this->getFirstValueOrDefaultFromXmlNode($node, 'dependsOn'); // How should we default includeInXlf? diff --git a/lib/Factory/TagTrait.php b/lib/Factory/TagTrait.php index 6110886fdc..11a4aefd0c 100644 --- a/lib/Factory/TagTrait.php +++ b/lib/Factory/TagTrait.php @@ -58,13 +58,18 @@ public function loadTagsByEntityId(string $table, string $column, int $entityId) * @param string $table * @param string $column * @param array $entityIds - * @param array $entries + * @param \Xibo\Entity\EntityTrait[] $entries */ - public function decorateWithTagLinks(string $table, string $column, array $entityIds, array $entries) + public function decorateWithTagLinks(string $table, string $column, array $entityIds, array $entries): void { - $sql = 'SELECT tag.tagId, tag.tag, `'. $table .'`.value, `'.$table.'`.'.$column.' FROM `tag` INNER JOIN `'.$table.'` ON `'.$table.'`.tagId = tag.tagId WHERE `'.$table.'`.'.$column.' IN('. implode(',', $entityIds).')'; + // Query to get all tags from a tag link table for a set of entityIds + $sql = 'SELECT `tag`.`tagId`, `tag`.`tag`, `' . $table . '`.`value`, `' . $table . '`.`' . $column . '`' + . ' FROM `tag` ' + . ' INNER JOIN `' . $table . '` ON `' . $table . '`.`tagId` = `tag`.`tagId` ' + . ' WHERE `' . $table . '`.`' . $column . '` IN(' . implode(',', $entityIds) .')'; foreach ($this->getStore()->select($sql, []) as $row) { + // Add each tag returned above to its respective entity $sanitizedRow = $this->getSanitizer($row); $tagLink = new TagLink($this->getStore(), $this->getLog(), $this->getDispatcher()); @@ -78,6 +83,11 @@ public function decorateWithTagLinks(string $table, string $column, array $entit } } } + + // Set the original value on the entity. + foreach ($entries as $entry) { + $entry->setOriginalValue('tags', $entry->tags); + } } public function getTagUsageByEntity(string $tagLinkTable, string $idColumn, string $nameColumn, string $entity, int $tagId, &$entries) diff --git a/lib/Factory/UserFactory.php b/lib/Factory/UserFactory.php index 89c1c49915..83da9d197d 100644 --- a/lib/Factory/UserFactory.php +++ b/lib/Factory/UserFactory.php @@ -1,6 +1,6 @@ getUser()->isSuperAdmin()) { + // Non-super admins should only get a count of users in their group + $sql .= ' + WHERE `user`.userId IN ( + SELECT `otherUserLinks`.userId + FROM `lkusergroup` + INNER JOIN `group` + ON `group`.groupId = `lkusergroup`.groupId + AND `group`.isUserSpecific = 0 + INNER JOIN `lkusergroup` `otherUserLinks` + ON `otherUserLinks`.groupId = `group`.groupId + WHERE `lkusergroup`.userId = :currentUserId + ) + '; + $params['currentUserId'] = $this->getUser()->userId; + } + + // Run the query + $results = $this->getStore()->select($sql, $params); + return intval($results[0]['countOf'] ?? 0); + } } \ No newline at end of file diff --git a/lib/Helper/AttachmentUploadHandler.php b/lib/Helper/AttachmentUploadHandler.php index abd7fe509e..e6df75a908 100644 --- a/lib/Helper/AttachmentUploadHandler.php +++ b/lib/Helper/AttachmentUploadHandler.php @@ -1,4 +1,24 @@ . + */ namespace Xibo\Helper; @@ -12,7 +32,7 @@ class AttachmentUploadHandler extends BlueImpUploadHandler * @param $file * @param $index */ - protected function handle_form_data($file, $index) + protected function handleFormData($file, $index) { $controller = $this->options['controller']; /* @var \Xibo\Controller\Notification $controller */ diff --git a/lib/Helper/BlueImpUploadHandler.php b/lib/Helper/BlueImpUploadHandler.php index 56546e4ce2..54f71da9a1 100644 --- a/lib/Helper/BlueImpUploadHandler.php +++ b/lib/Helper/BlueImpUploadHandler.php @@ -1,5 +1,31 @@ . + */ + +namespace Xibo\Helper; + +use Psr\Log\LoggerInterface; + +/** + * Heavily modified BlueImp Upload handler, stripped out image processing, downloads, etc. * jQuery File Upload Plugin PHP Class 6.4.2 * https://github.com/blueimp/jQuery-File-Upload * @@ -9,18 +35,13 @@ * Licensed under the MIT license: * http://www.opensource.org/licenses/MIT */ - -namespace Xibo\Helper; -use stdClass; -use Intervention\Image\ImageManagerStatic as Img; -use Intervention\Image\Exception\NotReadableException; - class BlueImpUploadHandler { - protected $options; + protected array $options; + // PHP File Upload error message codes: // http://php.net/manual/en/features.file-upload.errors.php - protected $error_messages = array( + private array $errorMessages = [ 1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form', 3 => 'The uploaded file was only partially uploaded', @@ -29,30 +50,24 @@ class BlueImpUploadHandler 7 => 'Failed to write file to disk', 8 => 'A PHP extension stopped the file upload', 'post_max_size' => 'The uploaded file exceeds the post_max_size directive in php.ini', - 'max_file_size' => 'File is too big', - 'min_file_size' => 'File is too small', 'accept_file_types' => 'Filetype not allowed', - 'max_number_of_files' => 'Maximum number of files exceeded', - 'max_width' => 'Image exceeds maximum width', - 'min_width' => 'Image requires a minimum width', - 'max_height' => 'Image exceeds maximum height', - 'min_height' => 'Image requires a minimum height' - ); - - function __construct($options = null, $initialize = true, $error_messages = null) - { - $this->options = array( - 'script_url' => $this->get_full_url() . '/', - 'upload_dir' => dirname($this->get_server_var('SCRIPT_FILENAME')) . '/files/', - 'upload_url' => $this->get_full_url() . '/files/', - 'user_dirs' => false, - 'mkdir_mode' => 0755, - 'param_name' => 'files', - // Set the following option to 'POST', if your server does not support - // DELETE requests. This is a parameter sent to the client: - 'delete_type' => 'DELETE', + ]; + + /** + * @param string $uploadDir + * @param \Psr\Log\LoggerInterface $logger + * @param array $options + * @param bool $initialize + */ + public function __construct( + string $uploadDir, + private readonly LoggerInterface $logger, + array $options = [], + bool $initialize = true, + ) { + $this->options = array_merge([ + 'upload_dir' => $uploadDir, 'access_control_allow_origin' => '*', - 'access_control_allow_credentials' => false, 'access_control_allow_methods' => array( 'OPTIONS', 'HEAD', @@ -67,160 +82,71 @@ function __construct($options = null, $initialize = true, $error_messages = null 'Content-Range', 'Content-Disposition' ), - // Enable to provide file downloads via GET requests to the PHP script: - 'download_via_php' => false, // Defines which files can be displayed inline when downloaded: 'inline_file_types' => '/\.(gif|jpe?g|png)$/i', // Defines which files (based on their names) are accepted for upload: 'accept_file_types' => '/.+$/i', - // The php.ini settings upload_max_filesize and post_max_size - // take precedence over the following max_file_size setting: - 'max_file_size' => null, - 'min_file_size' => 1, - // The maximum number of files for the upload directory: - 'max_number_of_files' => null, - // Image resolution restrictions: - 'max_width' => null, - 'max_height' => null, - 'min_width' => 1, - 'min_height' => 1, // Set the following option to false to enable resumable uploads: 'discard_aborted_uploads' => true, - // Set to true to rotate images based on EXIF meta data, if available: - 'orient_image' => false, - 'image_versions' => array( - // Uncomment the following version to restrict the size of - // uploaded images: - /* - '' => array( - 'max_width' => 1920, - 'max_height' => 1200, - 'jpeg_quality' => 95 - ), - */ - // Uncomment the following to create medium sized images: - /* - 'medium' => array( - 'max_width' => 800, - 'max_height' => 600, - 'jpeg_quality' => 80 - ), - */ - 'thumbnail' => array( - // Uncomment the following to force the max - // dimensions and e.g. create square thumbnails: - //'crop' => true, - 'max_width' => 80, - 'max_height' => 80 - ) - ) - ); - if ($options) { - $this->options = array_merge($this->options, $options); - } - if ($error_messages) { - $this->error_messages = array_merge($this->error_messages, $error_messages); - } + ], $options); + if ($initialize) { $this->initialize(); } } - protected function initialize() + protected function getLogger(): LoggerInterface + { + return $this->logger; + } + + private function initialize(): void { - switch ($this->get_server_var('REQUEST_METHOD')) { + switch ($this->getServerVar('REQUEST_METHOD')) { case 'OPTIONS': case 'HEAD': $this->head(); break; - case 'GET': - $this->get(); - break; case 'PATCH': case 'PUT': case 'POST': $this->post(); break; - case 'DELETE': - $this->delete(); - break; default: $this->header('HTTP/1.1 405 Method Not Allowed'); } } - protected function get_full_url() + /** + * Get the upload directory + * @return string + */ + protected function getUploadDir(): string { - $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; - return - ($https ? 'https://' : 'http://') . - (!empty($_SERVER['REMOTE_USER']) ? $_SERVER['REMOTE_USER'] . '@' : '') . - (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : ($_SERVER['SERVER_NAME'] . - ($https && $_SERVER['SERVER_PORT'] === 443 || - $_SERVER['SERVER_PORT'] === 80 ? '' : ':' . $_SERVER['SERVER_PORT']))) . - substr($_SERVER['SCRIPT_NAME'], 0, strrpos($_SERVER['SCRIPT_NAME'], '/')); + return $this->options['upload_dir']; } - protected function get_user_id() + /** + * @param $fileName + * @param $version + * @return string + */ + private function getUploadPath($fileName = null, $version = null): string { - @session_start(); - return session_id(); - } - - protected function get_user_path() - { - if ($this->options['user_dirs']) { - return $this->get_user_id() . '/'; - } - return ''; - } - - protected function get_upload_path($file_name = null, $version = null) - { - $file_name = $file_name ? $file_name : ''; - $version_path = empty($version) ? '' : $version . '/'; - return $this->options['upload_dir'] . $this->get_user_path() - . $version_path . $file_name; - } - - protected function get_query_separator($url) - { - return strpos($url, '?') === false ? '?' : '&'; - } - - protected function get_download_url($file_name, $version = null) - { - if ($this->options['download_via_php']) { - $url = $this->options['script_url'] - . $this->get_query_separator($this->options['script_url']) - . 'file=' . rawurlencode($file_name); - if ($version) { - $url .= '&version=' . rawurlencode($version); - } - return $url . '&download=1'; - } - $version_path = empty($version) ? '' : rawurlencode($version) . '/'; - return $this->options['upload_url'] . $this->get_user_path() - . $version_path . rawurlencode($file_name); - } + $this->getLogger()->debug('getUploadPath: ' . $fileName); - protected function set_file_delete_properties($file) - { - $file->delete_url = $this->options['script_url'] - . $this->get_query_separator($this->options['script_url']) - . 'file=' . rawurlencode($file->name); - $file->delete_type = $this->options['delete_type']; - if ($file->delete_type !== 'DELETE') { - $file->delete_url .= '&_method=DELETE'; - } - if ($this->options['access_control_allow_credentials']) { - $file->delete_with_credentials = true; - } + $fileName = $fileName ?: ''; + $versionPath = empty($version) ? '' : $version . '/'; + return $this->options['upload_dir'] . $versionPath . $fileName; } - // Fix for overflowing signed 32 bit integers, - // works for sizes up to 2^32-1 bytes (4 GiB - 1): - protected function fix_integer_overflow($size) + /** + * Fix for overflowing signed 32-bit integers, + * works for sizes up to 2^32-1 bytes (4 GiB - 1): + * @param $size + * @return int + */ + private function fixIntegerOverflow($size): int { if ($size < 0) { $size += 2.0 * (PHP_INT_MAX + 1); @@ -228,291 +154,120 @@ protected function fix_integer_overflow($size) return $size; } - protected function get_file_size($file_path, $clear_stat_cache = false) - { - if ($clear_stat_cache) { - clearstatcache(true, $file_path); - } - return $this->fix_integer_overflow(filesize($file_path)); - - } - - protected function is_valid_file_object($file_name) + /** + * @param string $filePath + * @param bool $clearStatCache + * @return int + */ + private function getFileSize(string $filePath, bool $clearStatCache = false): int { - $file_path = $this->get_upload_path($file_name); - if (is_file($file_path) && $file_name[0] !== '.') { - return true; + if ($clearStatCache) { + clearstatcache(true, $filePath); } - return false; + return $this->fixIntegerOverflow(filesize($filePath)); } - protected function get_file_object($file_name) + /** + * @param $error + * @return string + */ + private function getErrorMessage($error): string { - if ($this->is_valid_file_object($file_name)) { - $file = new stdClass(); - $file->name = $file_name; - $file->size = $this->get_file_size( - $this->get_upload_path($file_name) - ); - $file->url = $this->get_download_url($file->name); - foreach ($this->options['image_versions'] as $version => $options) { - if (!empty($version)) { - if (is_file($this->get_upload_path($file_name, $version))) { - $file->{$version . '_url'} = $this->get_download_url( - $file->name, - $version - ); - } - } - } - $this->set_file_delete_properties($file); - return $file; - } - return null; - } - - protected function get_file_objects($iteration_method = 'get_file_object') - { - $upload_dir = $this->get_upload_path(); - if (!is_dir($upload_dir)) { - return array(); - } - return array_values(array_filter(array_map( - array($this, $iteration_method), - scandir($upload_dir) - ))); - } - - protected function count_file_objects() - { - return count($this->get_file_objects('is_valid_file_object')); - } - - protected function create_scaled_image($file_name, $version, $options) - { - $file_path = $this->get_upload_path($file_name); - if (!empty($version)) { - $version_dir = $this->get_upload_path(null, $version); - if (!is_dir($version_dir)) { - mkdir($version_dir, $this->options['mkdir_mode'], true); - } - $new_file_path = $version_dir . '/' . $file_name; - } else { - $new_file_path = $file_path; - } - if (!function_exists('getimagesize')) { - error_log('Function not found: getimagesize'); - return false; - } - list($img_width, $img_height) = @getimagesize($file_path); - if (!$img_width || !$img_height) { - return false; - } - $max_width = $options['max_width']; - $max_height = $options['max_height']; - $scale = min( - $max_width / $img_width, - $max_height / $img_height - ); - if ($scale >= 1) { - if ($file_path !== $new_file_path) { - return copy($file_path, $new_file_path); - } - return true; - } - if (!function_exists('imagecreatetruecolor')) { - error_log('Function not found: imagecreatetruecolor'); - return false; - } - if (empty($options['crop'])) { - $new_width = $img_width * $scale; - $new_height = $img_height * $scale; - $dst_x = 0; - $dst_y = 0; - $new_img = @imagecreatetruecolor($new_width, $new_height); - } else { - if (($img_width / $img_height) >= ($max_width / $max_height)) { - $new_width = $img_width / ($img_height / $max_height); - $new_height = $max_height; - } else { - $new_width = $max_width; - $new_height = $img_height / ($img_width / $max_width); - } - $dst_x = 0 - ($new_width - $max_width) / 2; - $dst_y = 0 - ($new_height - $max_height) / 2; - $new_img = @imagecreatetruecolor($max_width, $max_height); - } - switch (strtolower(substr(strrchr($file_name, '.'), 1))) { - case 'jpg': - case 'jpeg': - $src_img = @imagecreatefromjpeg($file_path); - $write_image = 'imagejpeg'; - $image_quality = isset($options['jpeg_quality']) ? - $options['jpeg_quality'] : 75; - break; - case 'gif': - @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0)); - $src_img = @imagecreatefromgif($file_path); - $write_image = 'imagegif'; - $image_quality = null; - break; - case 'png': - @imagecolortransparent($new_img, @imagecolorallocate($new_img, 0, 0, 0)); - @imagealphablending($new_img, false); - @imagesavealpha($new_img, true); - $src_img = @imagecreatefrompng($file_path); - $write_image = 'imagepng'; - $image_quality = isset($options['png_quality']) ? - $options['png_quality'] : 9; - break; - default: - $src_img = null; - } - $success = $src_img && @imagecopyresampled( - $new_img, - $src_img, - $dst_x, - $dst_y, - 0, - 0, - $new_width, - $new_height, - $img_width, - $img_height - ) && $write_image($new_img, $new_file_path, $image_quality); - // Free up memory (imagedestroy does not delete files): - @imagedestroy($src_img); - @imagedestroy($new_img); - return $success; - } - - protected function get_error_message($error) - { - return array_key_exists($error, $this->error_messages) ? - $this->error_messages[$error] : $error; + return $this->errorMessages[$error] ?? $error; } - function get_config_bytes($val) + /** + * @param $val + * @return float|int + */ + private function getConfigBytes($val): float|int { - $val = trim($val); - $last = strtolower($val[strlen($val) - 1]); - $val = substr($val, 0, -1); - switch ($last) { - case 'g': - $val *= 1024; - case 'm': - $val *= 1024; - case 'k': - $val *= 1024; - } - return $this->fix_integer_overflow($val); + return $this->fixIntegerOverflow(ByteFormatter::toBytes($val)); } - protected function validate($uploaded_file, $file, $error, $index) + /** + * @param $file + * @param $error + * @return bool + */ + private function validate($file, $error): bool { if ($error) { - $file->error = $this->get_error_message($error); + $file->error = $this->getErrorMessage($error); return false; } - $content_length = $this->fix_integer_overflow(intval( - $this->get_server_var('CONTENT_LENGTH') - )); - $post_max_size = $this->get_config_bytes(ini_get('post_max_size')); - if ($post_max_size && ($content_length > $post_max_size)) { - $file->error = $this->get_error_message('post_max_size'); + + // Make sure the content length isn't greater than the max size + $contentLength = $this->fixIntegerOverflow(intval($this->getServerVar('CONTENT_LENGTH'))); + $postMaxSize = $this->getConfigBytes(ini_get('post_max_size')); + if ($postMaxSize && ($contentLength > $postMaxSize)) { + $file->error = $this->getErrorMessage('post_max_size'); return false; } + + // Max sure the we are an accepted file type if (!preg_match($this->options['accept_file_types'], $file->name)) { - $file->error = $this->get_error_message('accept_file_types'); - return false; - } - if ($uploaded_file && is_uploaded_file($uploaded_file)) { - $file_size = $this->get_file_size($uploaded_file); - } else { - $file_size = $content_length; - } - if ($this->options['max_file_size'] && ( - $file_size > $this->options['max_file_size'] || - $file->size > $this->options['max_file_size']) - ) { - $file->error = $this->get_error_message('max_file_size'); - return false; - } - if ($this->options['min_file_size'] && - $file_size < $this->options['min_file_size'] - ) { - $file->error = $this->get_error_message('min_file_size'); - return false; - } - if (is_int($this->options['max_number_of_files']) && ( - $this->count_file_objects() >= $this->options['max_number_of_files']) - ) { - $file->error = $this->get_error_message('max_number_of_files'); + $file->error = $this->getErrorMessage('accept_file_types'); return false; } - if ($this->is_valid_image_file($uploaded_file)) { - list($img_width, $img_height) = @getimagesize($uploaded_file); - if (is_int($img_width)) { - if ($this->options['max_width'] && $img_width > $this->options['max_width']) { - $file->error = $this->get_error_message('max_width'); - return false; - } - if ($this->options['max_height'] && $img_height > $this->options['max_height']) { - $file->error = $this->get_error_message('max_height'); - return false; - } - if ($this->options['min_width'] && $img_width < $this->options['min_width']) { - $file->error = $this->get_error_message('min_width'); - return false; - } - if ($this->options['min_height'] && $img_height < $this->options['min_height']) { - $file->error = $this->get_error_message('min_height'); - return false; - } - } - } return true; } - protected function upcount_name_callback($matches) - { - $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1; - $ext = isset($matches[2]) ? $matches[2] : ''; - return ' (' . $index . ')' . $ext; - } - - protected function upcount_name($name) + private function upcountName(string $name): string { + $this->getLogger()->debug('upcountName: ' . $name); return preg_replace_callback( '/(?:(?: \(([\d]+)\))?(\.[^.]+))?$/', - array($this, 'upcount_name_callback'), + function ($matches): string { + $this->getLogger()->debug('upcountName: callback, matches: ' . var_export($matches, true)); + $index = isset($matches[1]) ? intval($matches[1]) + 1 : 1; + $ext = $matches[2] ?? ''; + return ' (' . $index . ')' . $ext; + }, $name, 1 ); } - protected function get_unique_filename($name, $type, $index, $content_range) + /** + * @param $name + * @param $contentRange + * @return string + */ + private function getUniqueFilename($name, $contentRange): string { - while (is_dir($this->get_upload_path($name))) { - $name = $this->upcount_name($name); + $uploadPath = $this->getUploadPath($name); + + $this->getLogger()->debug('getUniqueFilename: ' . $name . ', uploadPath: ' . $uploadPath + . ', contentRange: ' . $contentRange); + + $attempts = 0; + while (is_dir($uploadPath) && $attempts < 100) { + $name = $this->upcountName($name); + $attempts++; } - $contentRange = $content_range === null ? 0 : $content_range[1]; + $this->getLogger()->debug('getUniqueFilename: resolved file path: ' . $name); + + $contentRange = $contentRange === null ? 0 : $contentRange[1]; // Keep an existing filename if this is part of a chunked upload: - $uploaded_bytes = $this->fix_integer_overflow($contentRange); - while (is_file($this->get_upload_path($name))) { - if ($uploaded_bytes === $this->get_file_size( - $this->get_upload_path($name)) - ) { + $uploaded_bytes = $this->fixIntegerOverflow($contentRange); + while (is_file($this->getUploadPath($name))) { + if ($uploaded_bytes === $this->getFileSize($this->getUploadPath($name))) { break; } - $name = $this->upcount_name($name); + $name = $this->upcountName($name); } return $name; } - protected function trim_file_name($name, $type, $index, $content_range) + /** + * @param $name + * @param $type + * @return string + */ + private function trimFileName($name, $type): string { // Remove path information and dots around the filename, to prevent uploading // into different directories or replacing hidden system files. @@ -523,268 +278,148 @@ protected function trim_file_name($name, $type, $index, $content_range) $name = str_replace('.', '-', microtime(true)); } // Add missing file extension for known image types: - if (strpos($name, '.') === false && - preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches) + if (!str_contains($name, '.') + && preg_match('/^image\/(gif|jpe?g|png)/', $type, $matches) ) { $name .= '.' . $matches[1]; } return $name; } - protected function get_file_name($name, $type, $index, $content_range) + /** + * @param string $name + * @param string $type + * @param int|null $contentRange + * @return string + */ + private function getFileName(string $name, string $type, ?int $contentRange): string { - return $this->get_unique_filename( - $this->trim_file_name($name, $type, $index, $content_range), - $type, - $index, - $content_range + $this->getLogger()->debug('getFileName: ' . $name . ', type: ' . $type); + + return $this->getUniqueFilename( + $this->trimFileName($name, $type), + $contentRange ); } - protected function orient_image($file_path) - { - if (!function_exists('exif_read_data')) { - return false; - } - $exif = @exif_read_data($file_path); - if ($exif === false) { - return false; - } - $orientation = intval(@$exif['Orientation']); - if (!in_array($orientation, array(3, 6, 8))) { - return false; - } - $image = @imagecreatefromjpeg($file_path); - switch ($orientation) { - case 3: - $image = @imagerotate($image, 180, 0); - break; - case 6: - $image = @imagerotate($image, 270, 0); - break; - case 8: - $image = @imagerotate($image, 90, 0); - break; - default: - return false; - } - $success = imagejpeg($image, $file_path); - // Free up memory (imagedestroy does not delete files): - @imagedestroy($image); - return $success; - } + /** + * @param $uploadedFile + * @param $name + * @param $size + * @param $type + * @param $error + * @param $index + * @param $contentRange + * @return \stdClass + */ + private function handleFileUpload( + $uploadedFile, + $name, + $size, + $type, + $error, + $index = null, + $contentRange = null + ) { + $this->getLogger()->debug('handleFileUpload: ' . $uploadedFile); + + // Build a file object to return. + $file = new \stdClass(); + $file->name = $this->getFileName($name, $type, $contentRange); + $file->size = $this->fixIntegerOverflow(intval($size)); + $file->type = $type; - protected function handle_image_file($file_path, $file) - { - if ($this->options['orient_image']) { - $this->orient_image($file_path); - } - $failed_versions = array(); - foreach ($this->options['image_versions'] as $version => $options) { - if ($this->create_scaled_image($file->name, $version, $options)) { - if (!empty($version)) { - $file->{$version . '_url'} = $this->get_download_url( - $file->name, - $version - ); - } else { - $file->size = $this->get_file_size($file_path, true); - } - } else { - $failed_versions[] = $version; + if ($this->validate($file, $error)) { + $uploadPath = $this->getUploadPath(); + if (!is_dir($uploadPath)) { + mkdir($uploadPath, 0755, true); } - } - switch (count($failed_versions)) { - case 0: - break; - case 1: - $file->error = 'Failed to create scaled version: ' - . $failed_versions[0]; - break; - default: - $file->error = 'Failed to create scaled versions: ' - . implode($failed_versions, ', '); - } - } + $filePath = $this->getUploadPath($file->name); - protected function handle_file_upload($uploaded_file, $name, $size, $type, $error, - $index = null, $content_range = null) - { - $file = new stdClass(); - $file->name = $this->get_file_name($name, $type, $index, $content_range); - $file->size = $this->fix_integer_overflow(intval($size)); - $file->type = $type; - if ($this->validate($uploaded_file, $file, $error, $index)) { - $upload_dir = $this->get_upload_path(); - if (!is_dir($upload_dir)) { - mkdir($upload_dir, $this->options['mkdir_mode'], true); - } - $file_path = $this->get_upload_path($file->name); - $append_file = $content_range && is_file($file_path) && - $file->size > $this->get_file_size($file_path); - if ($uploaded_file && is_uploaded_file($uploaded_file)) { + // Are we appending? + $appendFile = $contentRange && is_file($filePath) && $file->size > $this->getFileSize($filePath); + + if ($uploadedFile && is_uploaded_file($uploadedFile)) { // multipart/formdata uploads (POST method uploads) - if ($append_file) { + if ($appendFile) { file_put_contents( - $file_path, - fopen($uploaded_file, 'r'), + $filePath, + fopen($uploadedFile, 'r'), FILE_APPEND ); } else { - move_uploaded_file($uploaded_file, $file_path); + move_uploaded_file($uploadedFile, $filePath); } } else { // Non-multipart uploads (PUT method support) file_put_contents( - $file_path, + $filePath, fopen('php://input', 'r'), - $append_file ? FILE_APPEND : 0 + $appendFile ? FILE_APPEND : 0 ); } - $file_size = $this->get_file_size($file_path, $append_file); - if ($file_size === $file->size) { - $file->url = $this->get_download_url($file->name); - list($img_width, $img_height) = @getimagesize($file_path); - if (is_int($img_width)) { - $this->handle_image_file($file_path, $file); - } - $file->width = $img_width; - $file->height = $img_height; - $this->handle_form_data($file, $index); + $fileSize = $this->getFileSize($filePath, $appendFile); + + if ($fileSize === $file->size) { + $this->handleFormData($file, $index); } else { - $file->size = $file_size; - if (!$content_range && $this->options['discard_aborted_uploads']) { - unlink($file_path); + $file->size = $fileSize; + if (!$contentRange && $this->options['discard_aborted_uploads']) { + unlink($filePath); $file->error = 'abort'; } } - $this->set_file_delete_properties($file); } return $file; } - protected function readfile($file_path) - { - return readfile($file_path); - } - - protected function body($str) + /** + * @param $file + * @param $index + * @return void + */ + protected function handleFormData($file, $index) { - echo $str; } - protected function header($str) + /** + * @param string $str + * @return void + */ + private function header(string $str): void { header($str); } - protected function get_server_var($id) - { - return isset($_SERVER[$id]) ? $_SERVER[$id] : ''; - } - - protected function generate_response($content, $print_response = true) - { - if ($print_response) { - $json = json_encode($content); - $redirect = isset($_REQUEST['redirect']) ? - stripslashes($_REQUEST['redirect']) : null; - if ($redirect) { - $this->header('Location: ' . sprintf($redirect, rawurlencode($json))); - return; - } - $this->head(); - if ($this->get_server_var('HTTP_CONTENT_RANGE')) { - $files = isset($content[$this->options['param_name']]) ? - $content[$this->options['param_name']] : null; - if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { - $this->header('Range: 0-' . ( - $this->fix_integer_overflow(intval($files[0]->size)) - 1 - )); - } - } - $this->body($json); - } - return $content; - } - - protected function get_version_param() - { - return isset($_GET['version']) ? basename(stripslashes($_GET['version'])) : null; - } - - protected function get_file_name_param() - { - return isset($_GET['file']) ? basename(stripslashes($_GET['file'])) : null; - } - - protected function get_file_type($file_path) - { - switch (strtolower(pathinfo($file_path, PATHINFO_EXTENSION))) { - case 'jpeg': - case 'jpg': - return 'image/jpeg'; - case 'png': - return 'image/png'; - case 'gif': - return 'image/gif'; - default: - return ''; - } - } - - protected function download() + /** + * @param $id + * @return mixed|string + */ + private function getServerVar($id): mixed { - if (!$this->options['download_via_php']) { - $this->header('HTTP/1.1 403 Forbidden'); - return; - } - $file_name = $this->get_file_name_param(); - if ($this->is_valid_file_object($file_name)) { - $file_path = $this->get_upload_path($file_name, $this->get_version_param()); - if (is_file($file_path)) { - if (!preg_match($this->options['inline_file_types'], $file_name)) { - $this->header('Content-Description: File Transfer'); - $this->header('Content-Type: application/octet-stream'); - $this->header('Content-Disposition: attachment; filename="' . $file_name . '"'); - $this->header('Content-Transfer-Encoding: binary'); - } else { - // Prevent Internet Explorer from MIME-sniffing the content-type: - $this->header('X-Content-Type-Options: nosniff'); - $this->header('Content-Type: ' . $this->get_file_type($file_path)); - $this->header('Content-Disposition: inline; filename="' . $file_name . '"'); - } - $this->header('Content-Length: ' . $this->get_file_size($file_path)); - $this->header('Last-Modified: ' . gmdate('D, d M Y H:i:s T', filemtime($file_path))); - ob_end_flush(); - $this->readfile($file_path); - exit; - } - } + return $_SERVER[$id] ?? ''; } - protected function send_content_type_header() + private function sendContentTypeHeader(): void { $this->header('Vary: Accept'); - if (strpos($this->get_server_var('HTTP_ACCEPT'), 'application/json') !== false) { + if (str_contains($this->getServerVar('HTTP_ACCEPT'), 'application/json')) { $this->header('Content-type: application/json'); } else { $this->header('Content-type: text/plain'); } } - protected function send_access_control_headers() + private function sendAccessControlHeaders(): void { $this->header('Access-Control-Allow-Origin: ' . $this->options['access_control_allow_origin']); - $this->header('Access-Control-Allow-Credentials: ' - . ($this->options['access_control_allow_credentials'] ? 'true' : 'false')); $this->header('Access-Control-Allow-Methods: ' . implode(', ', $this->options['access_control_allow_methods'])); $this->header('Access-Control-Allow-Headers: ' . implode(', ', $this->options['access_control_allow_headers'])); } - public function head() + private function head(): void { $this->header('Pragma: no-cache'); $this->header('Cache-Control: no-store, no-cache, must-revalidate'); @@ -792,105 +427,74 @@ public function head() // Prevent Internet Explorer from MIME-sniffing the content-type: $this->header('X-Content-Type-Options: nosniff'); if ($this->options['access_control_allow_origin']) { - $this->send_access_control_headers(); + $this->sendAccessControlHeaders(); } - $this->send_content_type_header(); + $this->sendContentTypeHeader(); } - public function get($print_response = true) + /** + * @return void + */ + public function post(): void { - if ($print_response && isset($_GET['download'])) { - return $this->download(); - } - $file_name = $this->get_file_name_param(); - if ($file_name) { - $response = array( - substr($this->options['param_name'], 0, -1) => $this->get_file_object($file_name) - ); - } else { - $response = array( - $this->options['param_name'] => $this->get_file_objects() - ); - } - return $this->generate_response($response, $print_response); - } + $upload = $_FILES['files'] ?? null; - public function post($print_response = true) - { - if (isset($_REQUEST['_method']) && $_REQUEST['_method'] === 'DELETE') { - return $this->delete($print_response); - } - $upload = isset($_FILES[$this->options['param_name']]) ? - $_FILES[$this->options['param_name']] : null; // Parse the Content-Disposition header, if available: - $file_name = $this->get_server_var('HTTP_CONTENT_DISPOSITION') ? + $fileName = $this->getServerVar('HTTP_CONTENT_DISPOSITION') ? rawurldecode(preg_replace( '/(^[^"]+")|("$)/', '', - $this->get_server_var('HTTP_CONTENT_DISPOSITION') + $this->getServerVar('HTTP_CONTENT_DISPOSITION') )) : null; + // Parse the Content-Range header, which has the following form: // Content-Range: bytes 0-524287/2000000 - $content_range = $this->get_server_var('HTTP_CONTENT_RANGE') ? - preg_split('/[^0-9]+/', $this->get_server_var('HTTP_CONTENT_RANGE')) : null; - $size = $content_range ? $content_range[3] : null; - $files = array(); + $contentRange = $this->getServerVar('HTTP_CONTENT_RANGE') + ? preg_split('/[^0-9]+/', $this->getServerVar('HTTP_CONTENT_RANGE')) + : null; + $size = $contentRange ? $contentRange[3] : null; + + $this->getLogger()->debug('post: contentRange: ' . var_export($contentRange, true)); + + $files = []; if ($upload && is_array($upload['tmp_name'])) { // param_name is an array identifier like "files[]", // $_FILES is a multi-dimensional array: foreach ($upload['tmp_name'] as $index => $value) { - $files[] = $this->handle_file_upload( + $files[] = $this->handleFileUpload( $upload['tmp_name'][$index], - $file_name ? $file_name : $upload['name'][$index], - $size ? $size : $upload['size'][$index], + $fileName ?: $upload['name'][$index], + $size ?: $upload['size'][$index], $upload['type'][$index], $upload['error'][$index], $index, - $content_range + $contentRange ); } } else { // param_name is a single object identifier like "file", // $_FILES is a one-dimensional array: - $files[] = $this->handle_file_upload( - isset($upload['tmp_name']) ? $upload['tmp_name'] : null, - $file_name ? $file_name : (isset($upload['name']) ? - $upload['name'] : null), - $size ? $size : (isset($upload['size']) ? - $upload['size'] : $this->get_server_var('CONTENT_LENGTH')), - isset($upload['type']) ? - $upload['type'] : $this->get_server_var('CONTENT_TYPE'), - isset($upload['error']) ? $upload['error'] : null, + $files[] = $this->handleFileUpload( + $upload['tmp_name'] ?? null, + $fileName ?: ($upload['name'] ?? null), + $size ?: ($upload['size'] ?? $this->getServerVar('CONTENT_LENGTH')), + $upload['type'] ?? $this->getServerVar('CONTENT_TYPE'), + $upload['error'] ?? null, null, - $content_range + $contentRange ); } - return $this->generate_response( - array($this->options['param_name'] => $files), - $print_response - ); - } - public function delete($print_response = true) - { - $file_name = $this->get_file_name_param(); - $file_path = $this->get_upload_path($file_name); - $success = is_file($file_path) && $file_name[0] !== '.' && unlink($file_path); - if ($success) { - foreach ($this->options['image_versions'] as $version => $options) { - if (!empty($version)) { - $file = $this->get_upload_path($file_name, $version); - if (is_file($file)) { - unlink($file); - } - } + // Output response + $json = json_encode(['files' => $files]); + $this->head(); + if ($this->getServerVar('HTTP_CONTENT_RANGE')) { + if ($files && is_array($files) && is_object($files[0]) && $files[0]->size) { + $this->header('Range: 0-' . ( + $this->fixIntegerOverflow(intval($files[0]->size)) - 1 + )); } } - return $this->generate_response(array('success' => $success), $print_response); + echo $json; } - - protected function is_valid_image_file($file_path) { - return (preg_match('/\.(gif|jpe?g|png)$/i', $file_path)); - } - } diff --git a/lib/Helper/DataSetUploadHandler.php b/lib/Helper/DataSetUploadHandler.php index a5be90fbc0..0b5a2c8dfc 100644 --- a/lib/Helper/DataSetUploadHandler.php +++ b/lib/Helper/DataSetUploadHandler.php @@ -1,8 +1,8 @@ options['controller']; diff --git a/lib/Helper/Environment.php b/lib/Helper/Environment.php index 679b127e3a..c55281818f 100644 --- a/lib/Helper/Environment.php +++ b/lib/Helper/Environment.php @@ -30,7 +30,7 @@ */ class Environment { - public static $WEBSITE_VERSION_NAME = '4.0.12'; + public static $WEBSITE_VERSION_NAME = '4.0.13'; public static $XMDS_VERSION = '7'; public static $XLF_VERSION = 4; public static $VERSION_REQUIRED = '8.1.0'; diff --git a/lib/Helper/HttpsDetect.php b/lib/Helper/HttpsDetect.php index 15a1078ac5..4004afa890 100644 --- a/lib/Helper/HttpsDetect.php +++ b/lib/Helper/HttpsDetect.php @@ -1,8 +1,23 @@ . */ @@ -39,16 +54,17 @@ public function getScheme() * Get Host * @return string */ - public function getHost() + public function getHost(): string { if (isset($_SERVER['HTTP_HOST'])) { - if (strpos($_SERVER['HTTP_HOST'], ':') !== false) { - $hostParts = explode(':', $_SERVER['HTTP_HOST']); + $httpHost = htmlentities($_SERVER['HTTP_HOST'], ENT_QUOTES, 'UTF-8'); + if (str_contains($httpHost, ':')) { + $hostParts = explode(':', $httpHost); return $hostParts[0]; } - return $_SERVER['HTTP_HOST']; + return $httpHost; } return $_SERVER['SERVER_NAME']; @@ -60,8 +76,8 @@ public function getHost() */ public function getPort() { - if (isset($_SERVER['HTTP_HOST']) && strpos($_SERVER['HTTP_HOST'], ':') !== false) { - $hostParts = explode(':', $_SERVER['HTTP_HOST']); + if (isset($_SERVER['HTTP_HOST']) && str_contains($_SERVER['HTTP_HOST'], ':')) { + $hostParts = explode(':', htmlentities($_SERVER['HTTP_HOST'], ENT_QUOTES, 'UTF-8')); return $hostParts[1]; } diff --git a/lib/Helper/LayoutUploadHandler.php b/lib/Helper/LayoutUploadHandler.php index b86ffc2b33..8222cf634c 100644 --- a/lib/Helper/LayoutUploadHandler.php +++ b/lib/Helper/LayoutUploadHandler.php @@ -36,7 +36,7 @@ class LayoutUploadHandler extends BlueImpUploadHandler * @param $file * @param $index */ - protected function handle_form_data($file, $index) + protected function handleFormData($file, $index) { /* @var \Xibo\Controller\Layout $controller */ $controller = $this->options['controller']; diff --git a/lib/Helper/UploadHandler.php b/lib/Helper/UploadHandler.php index 2026349d49..e8096ec0a1 100644 --- a/lib/Helper/UploadHandler.php +++ b/lib/Helper/UploadHandler.php @@ -1,8 +1,27 @@ . + */ namespace Xibo\Helper; -use Xibo\Service\LogServiceInterface; use Xibo\Support\Exception\LibraryFullException; /** @@ -14,8 +33,7 @@ class UploadHandler extends BlueImpUploadHandler * @var callable */ private $postProcess; - /** @var LogServiceInterface */ - private $logger; + /** @var ApplicationState */ private $state; @@ -28,16 +46,6 @@ public function setPostProcessor(callable $function) $this->postProcess = $function; } - /** - * @param LogServiceInterface $logger - * @return $this - */ - public function setLogger(LogServiceInterface $logger) - { - $this->logger = $logger; - return $this; - } - /** * @param ApplicationState $state * @return $this @@ -48,27 +56,18 @@ public function setState(ApplicationState $state) return $this; } - /** - * Get Upload path - * @return string - */ - public function getUploadPath() - { - return $this->options['upload_dir']; - } - /** * Handle form data from BlueImp * @param $file * @param $index */ - protected function handle_form_data($file, $index) + protected function handleFormData($file, $index) { try { - $filePath = $this->getUploadPath() . $file->name; + $filePath = $this->getUploadDir() . $file->name; $file->fileName = $file->name; - $name = $this->getParam($index, 'name', $file->name); + $name = htmlspecialchars($this->getParam($index, 'name', $file->name)); $file->name = $name; // Check Library @@ -81,14 +80,14 @@ protected function handle_form_data($file, $index) ); } - $this->logger->debug('Upload complete for name: ' . $name . '. Index is ' . $index); + $this->getLogger()->debug('Upload complete for name: ' . $name . '. Index is ' . $index); if ($this->postProcess !== null) { $file = call_user_func($this->postProcess, $file, $this); } } catch (\Exception $exception) { - $this->logger->error('Error uploading file : ' . $exception->getMessage()); - $this->logger->debug($exception->getTraceAsString()); + $this->getLogger()->error('Error uploading file : ' . $exception->getMessage()); + $this->getLogger()->debug($exception->getTraceAsString()); // Unlink the temporary file @unlink($filePath); diff --git a/lib/Helper/XiboUploadHandler.php b/lib/Helper/XiboUploadHandler.php index aaa9bb2301..885dc515ed 100644 --- a/lib/Helper/XiboUploadHandler.php +++ b/lib/Helper/XiboUploadHandler.php @@ -45,7 +45,7 @@ class XiboUploadHandler extends BlueImpUploadHandler * @param $file * @param $index */ - protected function handle_form_data($file, $index) + protected function handleFormData($file, $index) { $controller = $this->options['controller']; /* @var \Xibo\Controller\Library $controller */ @@ -55,7 +55,7 @@ protected function handle_form_data($file, $index) $fileName = $file->name; $filePath = $controller->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $fileName; - $controller->getLog()->debug('Upload complete for name: ' . $fileName . '. Index is ' . $index); + $this->getLogger()->debug('Upload complete for name: ' . $fileName . '. Index is ' . $index); // Upload and Save try { @@ -83,7 +83,7 @@ protected function handle_form_data($file, $index) $module = $controller->getModuleFactory() ->getByExtension(strtolower(substr(strrchr($fileName, '.'), 1))); - $controller->getLog()->debug(sprintf( + $this->getLogger()->debug(sprintf( 'Module Type = %s, Name = %s', $module->type, $module->name @@ -94,7 +94,7 @@ protected function handle_form_data($file, $index) $updateInLayouts = ($this->options['updateInLayouts'] == 1); $deleteOldRevisions = ($this->options['deleteOldRevisions'] == 1); - $controller->getLog()->debug(sprintf( + $this->getLogger()->debug(sprintf( 'Replacing old with new - updateInLayouts = %d, deleteOldRevisions = %d', $updateInLayouts, $deleteOldRevisions @@ -166,7 +166,7 @@ protected function handle_form_data($file, $index) LibraryUploadCompleteEvent::$NAME ); - $controller->getLog()->debug('Copying permissions to new media'); + $this->getLogger()->debug('Copying permissions to new media'); foreach ($controller->getPermissionFactory()->getAllByObjectId( $controller->getUser(), @@ -181,10 +181,10 @@ protected function handle_form_data($file, $index) // Do we want to replace this in all layouts? if ($updateInLayouts) { - $controller->getLog()->debug('Replace in all Layouts selected. Getting associated widgets'); + $this->getLogger()->debug('Replace in all Layouts selected. Getting associated widgets'); foreach ($controller->getWidgetFactory()->getByMediaId($oldMedia->mediaId, 0) as $widget) { - $controller->getLog()->debug('Found widgetId ' . $widget->widgetId + $this->getLogger()->debug('Found widgetId ' . $widget->widgetId . ' to assess, type is ' . $widget->type); if (!$controller->getUser()->checkEditable($widget)) { @@ -202,7 +202,7 @@ protected function handle_form_data($file, $index) if ($module->type == 'audio' && in_array($oldMedia->mediaId, $widget->getAudioIds()) ) { - $controller->getLog()->debug('Found audio on widget that needs updating. widgetId = ' . + $this->getLogger()->debug('Found audio on widget that needs updating. widgetId = ' . $widget->getId() . '. Linking ' . $media->mediaId); $widget->unassignAudioById($oldMedia->mediaId); @@ -210,7 +210,7 @@ protected function handle_form_data($file, $index) $widget->save(); } else if ($widget->type == 'global') { // This is a global widget and will have elements which refer to this media id. - $controller->getLog()->debug('This is a global widget, checking for elements.'); + $this->getLogger()->debug('This is a global widget, checking for elements.'); // We need to load options as that is where we store elements $widget->load(false); @@ -268,7 +268,7 @@ protected function handle_form_data($file, $index) } } - $controller->getLog()->debug(sprintf( + $this->getLogger()->debug(sprintf( 'Found widget that needs updating. ID = %d. Linking %d', $widget->getId(), $media->mediaId @@ -299,7 +299,7 @@ protected function handle_form_data($file, $index) // Update any background images if ($media->mediaType == 'image') { - $controller->getLog()->debug(sprintf( + $this->getLogger()->debug(sprintf( 'Updating layouts with the old media %d as the background image.', $oldMedia->mediaId )); @@ -316,12 +316,12 @@ protected function handle_form_data($file, $index) // this means we can't delete the original mediaId when it comes time to do so. $deleteOldRevisions = false; - $controller->getLog()->info( + $this->getLogger()->info( 'Media used on Widget that we cannot edit. Delete Old Revisions has been disabled.' ); } - $controller->getLog()->debug(sprintf( + $this->getLogger()->debug(sprintf( 'Found layout that needs updating. ID = %d. Setting background image id to %d', $layout->layoutId, $media->mediaId @@ -331,7 +331,7 @@ protected function handle_form_data($file, $index) } } } elseif ($this->options['widgetId'] != 0) { - $controller->getLog()->debug('Swapping a specific widget only.'); + $this->getLogger()->debug('Swapping a specific widget only.'); // swap this one $widget = $controller->getWidgetFactory()->getById($this->options['widgetId']); @@ -346,7 +346,7 @@ protected function handle_form_data($file, $index) // We either want to Link the old record to this one, or delete it if ($updateInLayouts && $deleteOldRevisions) { - $controller->getLog()->debug('Delete old revisions of ' . $oldMedia->mediaId); + $this->getLogger()->debug('Delete old revisions of ' . $oldMedia->mediaId); // Check we have permission to delete this media if (!$controller->getUser()->checkDeleteable($oldMedia)) { @@ -357,7 +357,7 @@ protected function handle_form_data($file, $index) // Join the prior revision up with the new media. $priorMedia = $controller->getMediaFactory()->getParentById($oldMedia->mediaId); - $controller->getLog()->debug( + $this->getLogger()->debug( 'Prior media found, joining ' . $priorMedia->mediaId . ' with ' . $media->mediaId ); @@ -366,7 +366,7 @@ protected function handle_form_data($file, $index) $priorMedia->save(['validate' => false]); } catch (NotFoundException $e) { // Nothing to do then - $controller->getLog()->debug('No prior media found'); + $this->getLogger()->debug('No prior media found'); } $controller->getDispatcher()->dispatch( @@ -444,7 +444,7 @@ protected function handle_form_data($file, $index) // Are we assigning to a Playlist? if ($this->options['playlistId'] != 0 && $this->options['widgetId'] == 0) { - $controller->getLog()->debug('Assigning uploaded media to playlistId ' + $this->getLogger()->debug('Assigning uploaded media to playlistId ' . $this->options['playlistId']); // Get the Playlist @@ -494,8 +494,8 @@ protected function handle_form_data($file, $index) $file->widgetId = $widget->widgetId; } } catch (Exception $e) { - $controller->getLog()->error('Error uploading media: ' . $e->getMessage()); - $controller->getLog()->debug($e->getTraceAsString()); + $this->getLogger()->error('Error uploading media: ' . $e->getMessage()); + $this->getLogger()->debug($e->getTraceAsString()); // Unlink the temporary file @unlink($filePath); diff --git a/lib/Listener/DisplayGroupListener.php b/lib/Listener/DisplayGroupListener.php index 6e6af43c52..5eacb3d96f 100644 --- a/lib/Listener/DisplayGroupListener.php +++ b/lib/Listener/DisplayGroupListener.php @@ -197,12 +197,14 @@ public function onFolderMoving(FolderMovingEvent $event) * @param EventDispatcherInterface $dispatcher * @return void */ - public function onTagDelete(TagDeleteEvent $event, $eventName, EventDispatcherInterface $dispatcher) + public function onTagDelete(TagDeleteEvent $event, $eventName, EventDispatcherInterface $dispatcher): void { $displays = $this->storageService->select(' SELECT lktagdisplaygroup.displayGroupId - FROM `lktagdisplaygroup` INNER JOIN `displaygroup` - ON `lktagdisplaygroup`.displayGroupId = `displaygroup`.displayGroupId AND `displaygroup`.isDisplaySpecific = 1 + FROM `lktagdisplaygroup` + INNER JOIN `displaygroup` + ON `lktagdisplaygroup`.displayGroupId = `displaygroup`.displayGroupId + AND `displaygroup`.isDisplaySpecific = 1 WHERE `lktagdisplaygroup`.tagId = :tagId', [ 'tagId' => $event->getTagId() ]); @@ -214,7 +216,7 @@ public function onTagDelete(TagDeleteEvent $event, $eventName, EventDispatcherIn if (count($displays) > 0) { $dispatcher->dispatch( - new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESS', 1), + new TriggerTaskEvent('\Xibo\XTR\MaintenanceRegularTask', 'DYNAMIC_DISPLAY_GROUP_ASSESSED'), TriggerTaskEvent::$NAME ); } diff --git a/lib/Listener/TaskListener.php b/lib/Listener/TaskListener.php index 5e901dcf85..b4eefadda0 100644 --- a/lib/Listener/TaskListener.php +++ b/lib/Listener/TaskListener.php @@ -22,33 +22,24 @@ namespace Xibo\Listener; +use Stash\Interfaces\PoolInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Xibo\Event\TriggerTaskEvent; use Xibo\Factory\TaskFactory; use Xibo\Service\ConfigServiceInterface; /** - * Task events + * A listener for events related to tasks */ class TaskListener { use ListenerLoggerTrait; - /** - * @var TaskFactory - */ - private $taskFactory; - /** - * @var ConfigServiceInterface - */ - private $configService; - public function __construct( - TaskFactory $taskFactory, - ConfigServiceInterface $configService + private readonly TaskFactory $taskFactory, + private readonly ConfigServiceInterface $configService, + private readonly PoolInterface $pool ) { - $this->taskFactory = $taskFactory; - $this->configService = $configService; } /** @@ -68,12 +59,14 @@ public function registerWithDispatcher(EventDispatcherInterface $dispatcher) : T * @throws \Xibo\Support\Exception\InvalidArgumentException * @throws \Xibo\Support\Exception\NotFoundException */ - public function onTriggerTask(TriggerTaskEvent $event) + public function onTriggerTask(TriggerTaskEvent $event): void { - if (!empty($event->getSetting()) && !empty($event->getSettingValue())) { - $this->configService->changeSetting($event->getSetting(), $event->getSettingValue()); + if (!empty($event->getKey())) { + // Drop this setting from the cache + $this->pool->deleteItem($event->getKey()); } + // Mark the task to run now $task = $this->taskFactory->getByClass($event->getClassName()); $task->runNow = 1; $task->save(); diff --git a/lib/Middleware/ListenersMiddleware.php b/lib/Middleware/ListenersMiddleware.php index a33ac95d8a..8aaf677d5a 100644 --- a/lib/Middleware/ListenersMiddleware.php +++ b/lib/Middleware/ListenersMiddleware.php @@ -146,7 +146,8 @@ public static function setListeners(App $app) // Listen for event that affect Task (new TaskListener( $c->get('taskFactory'), - $c->get('configService') + $c->get('configService'), + $c->get('pool') )) ->useLogger($c->get('logger')) ->registerWithDispatcher($dispatcher); diff --git a/lib/Middleware/State.php b/lib/Middleware/State.php index 0a4b241000..4e856b157c 100644 --- a/lib/Middleware/State.php +++ b/lib/Middleware/State.php @@ -205,12 +205,13 @@ public static function setState(App $app, Request $request): Request $mode = $container->get('configService')->getSetting('SERVER_MODE'); $container->get('logService')->setMode($mode); - - if ($container->get('name') == 'web' || $container->get('name') == 'xtr') { + // Inject some additional changes on a per-container basis + $containerName = $container->get('name'); + if ($containerName == 'web' || $containerName == 'xtr' || $containerName == 'xmds') { /** @var Twig $view */ $view = $container->get('view'); - if ($container->get('name') == 'web') { + if ($containerName == 'web') { $container->set('flash', function () { return new \Slim\Flash\Messages(); }); @@ -223,8 +224,11 @@ public static function setState(App $app, Request $request): Request $filter = new \Twig\TwigFilter('url_decode', 'urldecode'); $twigEnvironment->addFilter($filter); - // set Twig auto reload - $twigEnvironment->enableAutoReload(); + // Set Twig auto reload if needed + // XMDS only renders widget html cache, and shouldn't need auto reload. + if ($containerName !== 'xmds') { + $twigEnvironment->enableAutoReload(); + } } // Configure logging diff --git a/lib/Report/Bandwidth.php b/lib/Report/Bandwidth.php index 5d106a6de2..3fb2bc333f 100644 --- a/lib/Report/Bandwidth.php +++ b/lib/Report/Bandwidth.php @@ -1,4 +1,24 @@ . + */ namespace Xibo\Report; @@ -256,7 +276,8 @@ public function getResults(SanitizerInterface $sanitizedParams) } // Decide what our units are going to be, based on the size - $base = floor(log($maxSize) / log(1024)); + // We need to put a fallback value in case it returns an infinite value + $base = !is_infinite(floor(log($maxSize) / log(1024))) ? floor(log($maxSize) / log(1024)) : 0; $labels = []; $data = []; diff --git a/lib/Service/ConfigService.php b/lib/Service/ConfigService.php index 5704ac9a2b..1cd41e7137 100644 --- a/lib/Service/ConfigService.php +++ b/lib/Service/ConfigService.php @@ -1,6 +1,6 @@ getSetting('GLOBAL_THEME_NAME', 'default') : $themeName; + $globalTheme = ($themeName == null) + ? basename($this->getSetting('GLOBAL_THEME_NAME', 'default')) + : $themeName; // Is this theme valid? - $systemTheme = (is_dir(PROJECT_ROOT . '/web/theme/' . $globalTheme) && file_exists(PROJECT_ROOT . '/web/theme/' . $globalTheme . '/config.php')); - $customTheme = (is_dir(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme) && file_exists(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme . '/config.php')); + $systemTheme = (is_dir(PROJECT_ROOT . '/web/theme/' . $globalTheme) + && file_exists(PROJECT_ROOT . '/web/theme/' . $globalTheme . '/config.php')); + $customTheme = (is_dir(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme) + && file_exists(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme . '/config.php')); if ($systemTheme) { require(PROJECT_ROOT . '/web/theme/' . $globalTheme . '/config.php'); @@ -285,8 +291,9 @@ public function loadTheme($themeName = null) } elseif ($customTheme) { require(PROJECT_ROOT . '/web/theme/custom/' . $globalTheme . '/config.php'); $themeFolder = 'theme/custom/' . $globalTheme . '/'; - } else + } else { throw new ConfigurationException(__('The theme "%s" does not exist', $globalTheme)); + } $this->themeLoaded = true; $this->themeConfig = $config; diff --git a/lib/Service/ImageProcessingService.php b/lib/Service/ImageProcessingService.php index daea0041b0..834ff59210 100644 --- a/lib/Service/ImageProcessingService.php +++ b/lib/Service/ImageProcessingService.php @@ -1,8 +1,8 @@ resize($width, $height, function ($constraint) { $constraint->aspectRatio(); }); + + // Get the updated height and width + $updatedHeight = $img->height(); + $updatedWidth = $img->width(); + $img->save($filePath); $img->destroy(); - } catch (NotReadableException $notReadableException) { $this->log->error('Image not readable: ' . $notReadableException->getMessage()); } - return $filePath; + return [ + 'filePath' => $filePath, + 'height' => $updatedHeight ?? $height, + 'width' => $updatedWidth ?? $width + ]; } } \ No newline at end of file diff --git a/lib/Service/UploadService.php b/lib/Service/UploadService.php index 40d51f2917..b4e88d70af 100644 --- a/lib/Service/UploadService.php +++ b/lib/Service/UploadService.php @@ -1,51 +1,59 @@ . + */ namespace Xibo\Service; use Xibo\Helper\ApplicationState; use Xibo\Helper\UploadHandler; +/** + * Upload Service to scaffold an upload handler + */ class UploadService { - /** @var array */ - private $settings; - - /** @var LogServiceInterface */ - private $logger; - /** @var ApplicationState */ - private $state; /** - * AnalyticsService constructor. + * UploadService constructor. + * @param string $uploadDir * @param array $settings * @param LogServiceInterface $logger * @param ApplicationState $state */ public function __construct( - array $settings, - LogServiceInterface $logger, - ApplicationState $state + private readonly string $uploadDir, + private readonly array $settings, + private readonly LogServiceInterface $logger, + private readonly ApplicationState $state ) { - $this->settings = $settings; - $this->logger = $logger; - $this->state = $state; } /** * Create a new upload handler - * @param array $errors * @return UploadHandler */ - public function createUploadHandler($errors = []) + public function createUploadHandler(): UploadHandler { - $options = array_merge([ - 'download_via_php' => true, - ], $this->settings); - // Blue imp requires an extra / - $handler = new UploadHandler($options, false, $errors); + $handler = new UploadHandler($this->uploadDir, $this->logger->getLoggerInterface(), $this->settings, false); - return $handler - ->setLogger($this->logger) - ->setState($this->state); + return $handler->setState($this->state); } } diff --git a/lib/Widget/CurrenciesAndStocksProvider.php b/lib/Widget/CurrenciesAndStocksProvider.php index 706458e8d4..b20e5f5257 100644 --- a/lib/Widget/CurrenciesAndStocksProvider.php +++ b/lib/Widget/CurrenciesAndStocksProvider.php @@ -74,7 +74,7 @@ public function fetchDuration(DurationProviderInterface $durationProvider): Widg if ($numItems > 1) { // If we have paging involved then work out the page count. - $itemsPerPage = $durationProvider->getWidget()->getOptionValue('maxItemsPerPage', 0); + $itemsPerPage = $durationProvider->getWidget()->getOptionValue('itemsPerPage', 0); if ($itemsPerPage > 0) { $numItems = ceil($numItems / $itemsPerPage); } diff --git a/lib/Widget/DataSetProvider.php b/lib/Widget/DataSetProvider.php index 60070843de..b85ea01c93 100644 --- a/lib/Widget/DataSetProvider.php +++ b/lib/Widget/DataSetProvider.php @@ -50,8 +50,6 @@ public function fetchData(DataProviderInterface $dataProvider): WidgetProviderIn public function fetchDuration(DurationProviderInterface $durationProvider): WidgetProviderInterface { if ($durationProvider->getWidget()->getOptionValue('durationIsPerItem', 0) == 1) { - $this->getLog()->debug('fetchDuration: duration is per item'); - // Count of rows $numItems = $durationProvider->getWidget()->getOptionValue('numItems', 0); @@ -66,6 +64,13 @@ public function fetchDuration(DurationProviderInterface $durationProvider): Widg } $durationProvider->setDuration($durationProvider->getWidget()->calculatedDuration * $numItems); + + $this->getLog()->debug(sprintf( + 'fetchDuration: duration is per item, numItems: %s, rowsPerPage: %s, itemsPerPage: %s', + $numItems, + $rowsPerPage, + $itemsPerPage + )); } return $this; } diff --git a/lib/Widget/Definition/Property.php b/lib/Widget/Definition/Property.php index ab6fe02cd8..40ff8c7ca0 100644 --- a/lib/Widget/Definition/Property.php +++ b/lib/Widget/Definition/Property.php @@ -90,9 +90,12 @@ class Property implements \JsonSerializable /** @var bool Should translations be parsed in the value? */ public $parseTranslations = false; - /** @var bool Should the prooperty be included in the XLF? */ + /** @var bool Should the property be included in the XLF? */ public $includeInXlf = false; + /** @var bool Should the default value be written out to widget options */ + public $saveDefault = false; + /** @var \Xibo\Widget\Definition\PlayerCompatibility */ public $playerCompatibility; @@ -111,6 +114,7 @@ class Property implements \JsonSerializable /** @var string The group ID of the property */ public $propertyGroupId; + /** @var mixed The value assigned to this property. This is set from widget options, or settings, never via XML */ public $value; /** @inheritDoc */ @@ -136,6 +140,7 @@ public function jsonSerialize(): array 'allowLibraryRefs' => $this->allowLibraryRefs, 'allowAssetRefs' => $this->allowAssetRefs, 'parseTranslations' => $this->parseTranslations, + 'saveDefault' => $this->saveDefault, 'dependsOn' => $this->dependsOn, ]; } @@ -197,7 +202,7 @@ public function setValueByType( bool $ignoreDefault = false ): Property { $value = $this->getByType($params, $key); - if ($value !== $this->default || $ignoreDefault) { + if ($value !== $this->default || $ignoreDefault || $this->saveDefault) { $this->value = $value; } return $this; diff --git a/lib/Widget/Render/WidgetDataProviderCache.php b/lib/Widget/Render/WidgetDataProviderCache.php index c8f0235cdb..7986202677 100644 --- a/lib/Widget/Render/WidgetDataProviderCache.php +++ b/lib/Widget/Render/WidgetDataProviderCache.php @@ -115,6 +115,9 @@ public function decorateWithCache( // Get the cache $this->cache = $this->pool->getItem($this->key); + + // Invalidation method old means that if this cache key is being regenerated concurrently to this request + // we return the old data we have stored already. $this->cache->setInvalidationMethod(Invalidation::OLD); // Get the data (this might be OLD data) @@ -122,32 +125,44 @@ public function decorateWithCache( $cacheCreationDt = $this->cache->getCreation(); // Does the cache have data? + // we keep data 50% longer than we need to, so that it has a chance to be regenerated out of band if ($data === null) { $this->getLog()->debug('decorateWithCache: miss, no data'); $hasData = false; } else { - // Determine if the cache returned is a miss or older than the modified date - $this->isMissOrOld = $this->cache->isMiss() || ( - $dataModifiedDt !== null && $cacheCreationDt !== false && $dataModifiedDt->isAfter($cacheCreationDt) - ); - - $this->getLog()->debug('decorateWithCache: cache has data, is miss or old: ' - . var_export($this->isMissOrOld, true)); + $hasData = true; // Clear the data provider and add the cached items back to it. $dataProvider->clearData(); $dataProvider->clearMeta(); $dataProvider->addItems($data->data ?? []); + // Record any cached mediaIds + $this->cachedMediaIds = $data->media ?? []; + + // Update any meta foreach (($data->meta ?? []) as $key => $item) { $dataProvider->addOrUpdateMeta($key, $item); } - $dataProvider->addOrUpdateMeta('cacheDt', $this->cache->getCreation()->format('c')); - $dataProvider->addOrUpdateMeta('expireDt', $this->cache->getExpiration()->format('c')); - // Record any cached mediaIds - $this->cachedMediaIds = $data->media ?? []; - $hasData = true; + // Determine whether this cache is a miss (i.e. expired and being regenerated, expired, out of date) + // We use our own expireDt here because Stash will only return expired data with invalidation method OLD + // if the data is currently being regenerated and another process has called lock() on it + $expireDt = $dataProvider->getMeta()['expireDt'] ?? null; + if ($expireDt !== null) { + $expireDt = Carbon::createFromFormat('c', $expireDt); + } else { + $expireDt = $this->cache->getExpiration(); + } + + // Determine if the cache returned is a miss or older than the modified/expired dates + $this->isMissOrOld = $this->cache->isMiss() + || ($dataModifiedDt !== null && $cacheCreationDt !== false && $dataModifiedDt->isAfter($cacheCreationDt) + || ($expireDt->isBefore(Carbon::now())) + ); + + $this->getLog()->debug('decorateWithCache: cache has data, is miss or old: ' + . var_export($this->isMissOrOld, true)); } // If we do not have data/we're old/missed cache, and we have requested a lock, then we will be refreshing @@ -200,13 +215,14 @@ public function saveToCache(DataProviderInterface $dataProvider): void throw new GeneralException('No cache to save'); } - // Set our cache from the data provider. + // Set some cache dates so that we can track when this data provider was cached and when it should expire. $dataProvider->addOrUpdateMeta('cacheDt', Carbon::now()->format('c')); $dataProvider->addOrUpdateMeta( 'expireDt', Carbon::now()->addSeconds($dataProvider->getCacheTtl())->format('c') ); + // Set our cache from the data provider. $object = new \stdClass(); $object->data = $dataProvider->getData(); $object->meta = $dataProvider->getMeta(); @@ -216,7 +232,10 @@ public function saveToCache(DataProviderInterface $dataProvider): void if (!$cached) { throw new GeneralException('Cache failure'); } - $this->cache->expiresAfter($dataProvider->getCacheTtl()); + + // Keep the cache 50% longer than necessary + // The expireDt must always be 15 minutes to allow plenty of time for the WidgetSyncTask to regenerate. + $this->cache->expiresAfter(ceil(max($dataProvider->getCacheTtl() * 1.5, 900))); // Save to the pool $this->pool->save($this->cache); diff --git a/lib/XTR/ImageProcessingTask.php b/lib/XTR/ImageProcessingTask.php index 2f53fa9a59..a2559af3ad 100644 --- a/lib/XTR/ImageProcessingTask.php +++ b/lib/XTR/ImageProcessingTask.php @@ -1,8 +1,8 @@ $imgHeight) { // 'landscape'; - $this->imageProcessingService->resizeImage($filePath, $resizeThreshold, null); + $updatedImg = $this->imageProcessingService->resizeImage($filePath, $resizeThreshold, null); } else { // 'portrait'; - $this->imageProcessingService->resizeImage($filePath, null, $resizeThreshold); + $updatedImg = $this->imageProcessingService->resizeImage($filePath, null, $resizeThreshold); } // Clears file status cache - clearstatcache(true, $filePath); + clearstatcache(true, $updatedImg['filePath']); $count++; // Release image and save - $media->release(md5_file($filePath), filesize($filePath)); + $media->release( + md5_file($updatedImg['filePath']), + filesize($updatedImg['filePath']), + $updatedImg['height'], + $updatedImg['width'] + ); $this->store->commitIfNecessary(); $mediaDisplays= []; diff --git a/lib/XTR/MaintenanceRegularTask.php b/lib/XTR/MaintenanceRegularTask.php index 66a19f5660..511e5e1823 100644 --- a/lib/XTR/MaintenanceRegularTask.php +++ b/lib/XTR/MaintenanceRegularTask.php @@ -493,15 +493,23 @@ private function publishLayouts() /** * Assess any eligible dynamic display groups if necessary * @return void + * @throws \Xibo\Support\Exception\NotFoundException */ - private function assessDynamicDisplayGroups() + private function assessDynamicDisplayGroups(): void { $this->runMessage .= '## ' . __('Assess Dynamic Display Groups') . PHP_EOL; - if ($this->config->getSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 0) == 1) { + // Do we have a cache key set to say that dynamic display group assessment has been completed? + $cache = $this->pool->getItem('DYNAMIC_DISPLAY_GROUP_ASSESSED'); + if ($cache->isMiss()) { Profiler::start('RegularMaintenance::assessDynamicDisplayGroups', $this->log); - $this->config->changeSetting('DYNAMIC_DISPLAY_GROUP_ASSESS', 0); + // Set the cache key with a long expiry and save. + $cache->set(true); + $cache->expiresAt(Carbon::now()->addYear()); + $this->pool->save($cache); + + // Process each dynamic display group $count = 0; foreach ($this->displayGroupFactory->getByIsDynamic(1) as $group) { diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php index 489e4c1cfa..410d7c8711 100644 --- a/lib/Xmds/Soap.php +++ b/lib/Xmds/Soap.php @@ -2551,94 +2551,124 @@ protected function authDisplay($hardwareKey) /** * Alert Display Up - * @throws \phpmailerException + * assesses whether a notification is required to be sent for this display, and only does something if the + * display is currently marked as offline (i.e. it is coming back online again) + * this is only called in Register * @throws NotFoundException */ - protected function alertDisplayUp() + protected function alertDisplayUp(): void { $maintenanceEnabled = $this->getConfig()->getSetting('MAINTENANCE_ENABLED'); if ($this->display->loggedIn == 0) { - $this->getLog()->info(sprintf('Display %s was down, now its up.', $this->display->display)); // Log display up $this->displayEventFactory->createEmpty()->displayUp($this->display->displayId); - $dayPartId = $this->display->getSetting('dayPartId', null,['displayOverride' => true]); - - $operatingHours = true; - - if ($dayPartId !== null) { - try { - $dayPart = $this->dayPartFactory->getById($dayPartId); - - $startTimeArray = explode(':', $dayPart->startTime); - $startTime = Carbon::now()->setTime(intval($startTimeArray[0]), intval($startTimeArray[1])); - - $endTimeArray = explode(':', $dayPart->endTime); - $endTime = Carbon::now()->setTime(intval($endTimeArray[0]), intval($endTimeArray[1])); - - $now = Carbon::now(); - - // exceptions - foreach ($dayPart->exceptions as $exception) { + // Do we need to email? + if ($this->display->emailAlert == 1 + && ($maintenanceEnabled == 'On' || $maintenanceEnabled == 'Protected') + && $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') == 1 + ) { + // Only send alerts during operating hours. + if ($this->isInsideOperatingHours()) { + $subject = sprintf(__('Recovery for Display %s'), $this->display->display); + $body = sprintf( + __('Display ID %d is now back online %s'), + $this->display->displayId, + Carbon::now()->format(DateFormatHelper::getSystemFormat()) + ); - // check if we are on exception day and if so override the startTime and endTime accordingly - if ($exception['day'] == Carbon::now()->format('D')) { - $exceptionsStartTime = explode(':', $exception['start']); - $startTime = Carbon::now()->setTime(intval($exceptionsStartTime[0]), intval($exceptionsStartTime[1])); + // Create a notification assigned to system-wide user groups + try { + $notification = $this->notificationFactory->createSystemNotification( + $subject, + $body, + Carbon::now() + ); - $exceptionsEndTime = explode(':', $exception['end']); - $endTime = Carbon::now()->setTime(intval($exceptionsEndTime[0]), intval($exceptionsEndTime[1])); + // Get groups which have been configured to receive notifications + foreach ($this->userGroupFactory + ->getDisplayNotificationGroups($this->display->displayGroupId) as $group) { + $notification->assignUserGroup($group); } - } - // check if we are inside the operating hours for this display - we use that flag to decide if we need to create a notification and send an email. - if (($now >= $startTime && $now <= $endTime)) { - $operatingHours = true; - } else { - $operatingHours = false; + // Save the notification and insert the links, etc. + $notification->save(); + } catch (\Exception) { + $this->getLog()->error(sprintf( + 'Unable to send email alert for display %s with subject %s and body %s', + $this->display->display, + $subject, + $body + )); } - - } catch (NotFoundException $e) { - $this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' . $this->display->displayId); + } else { + $this->getLog()->info('Not sending recovery email for Display - ' + . $this->display->display . ' we are outside of its operating hours'); } + } else { + $this->getLog()->debug(sprintf( + 'No email required. Email Alert: %d, Enabled: %s, Email Enabled: %s.', + $this->display->emailAlert, + $maintenanceEnabled, + $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') + )); } + } + } - // Do we need to email? - if ($this->display->emailAlert == 1 && ($maintenanceEnabled == 'On' || $maintenanceEnabled == 'Protected') - && $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS') == 1) { + /** + * Is the display currently inside operating hours? + * @return bool + * @throws \Xibo\Support\Exception\NotFoundException + */ + private function isInsideOperatingHours(): bool + { + $dayPartId = $this->display->getSetting('dayPartId', null, ['displayOverride' => true]); + if ($dayPartId !== null) { + try { + $dayPart = $this->dayPartFactory->getById($dayPartId); - // for displays without dayPartId set, this is always true, otherwise we check if we are inside the operating hours set for this display - if ($operatingHours) { - $subject = sprintf(__("Recovery for Display %s"), $this->display->display); - $body = sprintf(__("Display ID %d is now back online %s"), $this->display->displayId, - Carbon::now()->format(DateFormatHelper::getSystemFormat())); + $startTimeArray = explode(':', $dayPart->startTime); + $startTime = Carbon::now()->setTime(intval($startTimeArray[0]), intval($startTimeArray[1])); - // Create a notification assigned to system wide user groups - try { - $notification = $this->notificationFactory->createSystemNotification($subject, $body, - Carbon::now()); + $endTimeArray = explode(':', $dayPart->endTime); + $endTime = Carbon::now()->setTime(intval($endTimeArray[0]), intval($endTimeArray[1])); - // Add in any displayNotificationGroups, with permissions - foreach ($this->userGroupFactory->getDisplayNotificationGroups($this->display->displayGroupId) as $group) { - $notification->assignUserGroup($group); - } + $now = Carbon::now(); - $notification->save(); + // handle exceptions + foreach ($dayPart->exceptions as $exception) { + // check if we are on exception day and if so override the startTime and endTime accordingly + if ($exception['day'] == Carbon::now()->format('D')) { + // Parse the start/end times into the current day. + $exceptionsStartTime = explode(':', $exception['start']); + $startTime = Carbon::now()->setTime( + intval($exceptionsStartTime[0]), + intval($exceptionsStartTime[1]) + ); - } catch (\Exception $e) { - $this->getLog()->error(sprintf('Unable to send email alert for display %s with subject %s and body %s', - $this->display->display, $subject, $body)); + $exceptionsEndTime = explode(':', $exception['end']); + $endTime = Carbon::now()->setTime( + intval($exceptionsEndTime[0]), + intval($exceptionsEndTime[1]) + ); } - } else { - $this->getLog()->info('Not sending recovery email for Display - ' . $this->display->display . ' we are outside of its operating hours'); } - } else { - $this->getLog()->debug(sprintf('No email required. Email Alert: %d, Enabled: %s, Email Enabled: %s.', $this->display->emailAlert, $maintenanceEnabled, $this->getConfig()->getSetting('MAINTENANCE_EMAIL_ALERTS'))); + + // check if we are inside the operating hours for this display - we use that flag to decide + // if we need to create a notification and send an email. + if (($now >= $startTime && $now <= $endTime)) { + return true; + } + } catch (NotFoundException) { + $this->getLog()->debug('Unknown dayPartId set on Display Profile for displayId ' + . $this->display->displayId); } } + return false; } /** diff --git a/lib/Xmds/Wsdl.php b/lib/Xmds/Wsdl.php index c236283634..6db2e6e160 100644 --- a/lib/Xmds/Wsdl.php +++ b/lib/Xmds/Wsdl.php @@ -1,8 +1,23 @@ . */ @@ -51,25 +66,23 @@ public function output() * get Root url * @return string */ - public static function getRoot() + public static function getRoot(): string { - # Check REQUEST_URI is set. IIS doesn't set it so we need to build it + # Check REQUEST_URI is set. IIS doesn't set it, so we need to build it # Attribution: # Code snippet from http://support.ecenica.com/web-hosting/scripting/troubleshooting-scripting-errors/how-to-fix-server-request_uri-php-error-on-windows-iis/ # Released under BSD License # Copyright (c) 2009, Ecenica Limited All rights reserved. - if (!isset($_SERVER['REQUEST_URI'])) - { + if (!isset($_SERVER['REQUEST_URI'])) { $_SERVER['REQUEST_URI'] = $_SERVER['PHP_SELF']; - if (isset($_SERVER['QUERY_STRING'])) - { - $_SERVER['REQUEST_URI'].='?'.$_SERVER['QUERY_STRING']; + if (isset($_SERVER['QUERY_STRING'])) { + $_SERVER['REQUEST_URI'] .= '?' . $_SERVER['QUERY_STRING']; } } ## End Code Snippet - $request = explode('?', $_SERVER['REQUEST_URI']); + $request = explode('?', htmlentities($_SERVER['REQUEST_URI'], ENT_QUOTES, 'UTF-8')); return ((new HttpsDetect())->getUrl()) . '/' . ltrim($request[0], '/'); } -} \ No newline at end of file +} diff --git a/locale/af.mo b/locale/af.mo index a638a5c0ba..30f6b3e8cf 100755 Binary files a/locale/af.mo and b/locale/af.mo differ diff --git a/locale/ar.mo b/locale/ar.mo index fc02ffb2ff..fbb6777c73 100755 Binary files a/locale/ar.mo and b/locale/ar.mo differ diff --git a/locale/bg.mo b/locale/bg.mo index 60c388fdb6..85045c245e 100755 Binary files a/locale/bg.mo and b/locale/bg.mo differ diff --git a/locale/ca.mo b/locale/ca.mo index d0653921c0..d4ac42a844 100755 Binary files a/locale/ca.mo and b/locale/ca.mo differ diff --git a/locale/cs.mo b/locale/cs.mo index d39d0f190b..81915859c1 100755 Binary files a/locale/cs.mo and b/locale/cs.mo differ diff --git a/locale/da.mo b/locale/da.mo index 3cc5f69ca0..cf38ce73c6 100755 Binary files a/locale/da.mo and b/locale/da.mo differ diff --git a/locale/de.mo b/locale/de.mo index 8af5d52b05..dd38edb581 100755 Binary files a/locale/de.mo and b/locale/de.mo differ diff --git a/locale/default.pot b/locale/default.pot index 96430ab31a..3349a8e93c 100755 --- a/locale/default.pot +++ b/locale/default.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-06-05 17:18+0100\n" +"POT-Creation-Date: 2024-07-05 08:10+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -7076,13 +7076,13 @@ msgstr "" #: lib/Controller/MenuBoard.php:162 lib/Controller/Playlist.php:393 #: lib/Controller/Transition.php:94 lib/Controller/Library.php:650 #: lib/Controller/SyncGroup.php:170 lib/Controller/DataSetColumn.php:173 -#: lib/Controller/Tag.php:230 lib/Controller/Layout.php:1777 +#: lib/Controller/Tag.php:231 lib/Controller/Layout.php:1777 #: lib/Controller/MenuBoardCategory.php:191 #: lib/Controller/DisplayProfile.php:200 lib/Controller/User.php:335 #: lib/Controller/Applications.php:178 lib/Controller/Resolution.php:161 #: lib/Controller/Display.php:838 lib/Controller/MenuBoardProduct.php:191 -#: lib/Controller/PlayerSoftware.php:158 lib/Controller/Command.php:178 -#: lib/Controller/DataSetRss.php:163 lib/Controller/Schedule.php:2415 +#: lib/Controller/PlayerSoftware.php:157 lib/Controller/Command.php:178 +#: lib/Controller/DataSetRss.php:163 lib/Controller/Schedule.php:2436 #: lib/Controller/UserGroup.php:157 lib/Controller/DayPart.php:154 msgid "Edit" msgstr "" @@ -7114,19 +7114,19 @@ msgstr "" #: lib/Controller/MenuBoard.php:223 lib/Controller/Playlist.php:458 #: lib/Controller/Playlist.php:469 lib/Controller/Library.php:690 #: lib/Controller/Library.php:696 lib/Controller/SyncGroup.php:184 -#: lib/Controller/DataSetColumn.php:181 lib/Controller/Tag.php:237 -#: lib/Controller/Tag.php:243 lib/Controller/Layout.php:1835 +#: lib/Controller/DataSetColumn.php:181 lib/Controller/Tag.php:238 +#: lib/Controller/Tag.php:244 lib/Controller/Layout.php:1835 #: lib/Controller/Layout.php:1841 lib/Controller/MenuBoardCategory.php:201 -#: lib/Controller/Font.php:173 lib/Controller/Font.php:182 +#: lib/Controller/Font.php:172 lib/Controller/Font.php:181 #: lib/Controller/DisplayProfile.php:221 lib/Controller/User.php:349 #: lib/Controller/Applications.php:185 lib/Controller/Resolution.php:172 #: lib/Controller/Display.php:849 lib/Controller/Display.php:868 #: lib/Controller/MenuBoardProduct.php:201 #: lib/Controller/MenuBoardProduct.php:207 -#: lib/Controller/PlayerSoftware.php:165 lib/Controller/PlayerSoftware.php:174 +#: lib/Controller/PlayerSoftware.php:164 lib/Controller/PlayerSoftware.php:173 #: lib/Controller/Command.php:187 lib/Controller/Command.php:196 -#: lib/Controller/DataSetRss.php:171 lib/Controller/Schedule.php:2421 -#: lib/Controller/Schedule.php:2430 lib/Controller/UserGroup.php:165 +#: lib/Controller/DataSetRss.php:171 lib/Controller/Schedule.php:2442 +#: lib/Controller/Schedule.php:2451 lib/Controller/UserGroup.php:165 #: lib/Controller/DayPart.php:161 lib/Controller/DayPart.php:167 msgid "Delete" msgstr "" @@ -11470,7 +11470,7 @@ msgstr "" #: cache/9f/9f9a142004ca4a1c914a4395fe586017.php:125 #: cache/a4/a4b5f52b1992bb349e8a30b19ff24645.php:80 -#: lib/Controller/Playlist.php:1703 lib/Controller/Library.php:2128 +#: lib/Controller/Playlist.php:1703 lib/Controller/Library.php:2123 #: lib/Controller/Layout.php:1657 msgid "Design" msgstr "" @@ -12886,7 +12886,7 @@ msgstr "" #: cache/0a/0ae358955c92f467744fc6cf0f8f5624.php:92 #: cache/cd/cdf12d9a1fc3b5db7965cfa8f9bb8d13.php:92 #: cache/70/70af2bbd82243291558c9b83caec5b48.php:104 -#: lib/Controller/Font.php:163 +#: lib/Controller/Font.php:162 msgid "Details" msgstr "" @@ -14061,7 +14061,7 @@ msgstr "" #: cache/66/661d79c5da6d14a0db85cd0980868ff7.php:626 #: cache/66/661d79c5da6d14a0db85cd0980868ff7.php:738 -#: lib/Report/Bandwidth.php:301 +#: lib/Report/Bandwidth.php:322 msgid "Bandwidth" msgstr "" @@ -16166,7 +16166,7 @@ msgid "Are you sure you want to delete a non-empty Playlist?" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:2940 -#: lib/Connector/OpenWeatherMapConnector.php:500 +#: lib/Connector/OpenWeatherMapConnector.php:495 msgid "Afrikaans" msgstr "" @@ -16195,12 +16195,12 @@ msgid "Arabic (Tunisia)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:2968 -#: lib/Connector/OpenWeatherMapConnector.php:501 +#: lib/Connector/OpenWeatherMapConnector.php:496 msgid "Arabic" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:2972 -#: lib/Connector/OpenWeatherMapConnector.php:502 +#: lib/Connector/OpenWeatherMapConnector.php:497 msgid "Azerbaijani" msgstr "" @@ -16209,7 +16209,7 @@ msgid "Belarusian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:2980 -#: lib/Connector/OpenWeatherMapConnector.php:503 +#: lib/Connector/OpenWeatherMapConnector.php:498 msgid "Bulgarian" msgstr "" @@ -16238,12 +16238,12 @@ msgid "Bosnian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3008 -#: lib/Connector/OpenWeatherMapConnector.php:504 +#: lib/Connector/OpenWeatherMapConnector.php:499 msgid "Catalan" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3012 -#: lib/Connector/OpenWeatherMapConnector.php:507 +#: lib/Connector/OpenWeatherMapConnector.php:502 msgid "Czech" msgstr "" @@ -16256,7 +16256,7 @@ msgid "Welsh" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3024 -#: lib/Connector/OpenWeatherMapConnector.php:508 +#: lib/Connector/OpenWeatherMapConnector.php:503 msgid "Danish" msgstr "" @@ -16269,7 +16269,7 @@ msgid "German (Switzerland)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3036 -#: lib/Connector/OpenWeatherMapConnector.php:509 +#: lib/Connector/OpenWeatherMapConnector.php:504 msgid "German" msgstr "" @@ -16278,12 +16278,12 @@ msgid "Divehi" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3044 -#: lib/Connector/OpenWeatherMapConnector.php:510 +#: lib/Connector/OpenWeatherMapConnector.php:505 msgid "Greek" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3048 -#: lib/Connector/OpenWeatherMapConnector.php:511 +#: lib/Connector/OpenWeatherMapConnector.php:506 msgid "English" msgstr "" @@ -16337,7 +16337,7 @@ msgid "Spanish (United States)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3104 -#: lib/Connector/OpenWeatherMapConnector.php:538 +#: lib/Connector/OpenWeatherMapConnector.php:533 msgid "Spanish" msgstr "" @@ -16346,7 +16346,7 @@ msgid "Estonian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3112 -#: lib/Connector/OpenWeatherMapConnector.php:512 +#: lib/Connector/OpenWeatherMapConnector.php:507 msgid "Basque" msgstr "" @@ -16355,7 +16355,7 @@ msgid "Persian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3120 -#: lib/Connector/OpenWeatherMapConnector.php:514 +#: lib/Connector/OpenWeatherMapConnector.php:509 msgid "Finnish" msgstr "" @@ -16376,7 +16376,7 @@ msgid "French (Switzerland)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3140 -#: lib/Connector/OpenWeatherMapConnector.php:515 +#: lib/Connector/OpenWeatherMapConnector.php:510 msgid "French" msgstr "" @@ -16389,7 +16389,7 @@ msgid "Scottish Gaelic" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3152 -#: lib/Connector/OpenWeatherMapConnector.php:516 +#: lib/Connector/OpenWeatherMapConnector.php:511 msgid "Galician" msgstr "" @@ -16402,22 +16402,22 @@ msgid "Gujarati" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3164 -#: lib/Connector/OpenWeatherMapConnector.php:517 +#: lib/Connector/OpenWeatherMapConnector.php:512 msgid "Hebrew" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3168 -#: lib/Connector/OpenWeatherMapConnector.php:518 +#: lib/Connector/OpenWeatherMapConnector.php:513 msgid "Hindi" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3172 -#: lib/Connector/OpenWeatherMapConnector.php:519 +#: lib/Connector/OpenWeatherMapConnector.php:514 msgid "Croatian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3176 -#: lib/Connector/OpenWeatherMapConnector.php:520 +#: lib/Connector/OpenWeatherMapConnector.php:515 msgid "Hungarian" msgstr "" @@ -16426,7 +16426,7 @@ msgid "Armenian (Armenia)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3184 -#: lib/Connector/OpenWeatherMapConnector.php:521 +#: lib/Connector/OpenWeatherMapConnector.php:516 msgid "Indonesian" msgstr "" @@ -16439,12 +16439,12 @@ msgid "Italian (Switzerland)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3196 -#: lib/Connector/OpenWeatherMapConnector.php:522 +#: lib/Connector/OpenWeatherMapConnector.php:517 msgid "Italian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3200 -#: lib/Connector/OpenWeatherMapConnector.php:523 +#: lib/Connector/OpenWeatherMapConnector.php:518 msgid "Japanese" msgstr "" @@ -16469,7 +16469,7 @@ msgid "Kannada" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3224 -#: lib/Connector/OpenWeatherMapConnector.php:524 +#: lib/Connector/OpenWeatherMapConnector.php:519 msgid "Korean" msgstr "" @@ -16490,12 +16490,12 @@ msgid "Lao" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3244 -#: lib/Connector/OpenWeatherMapConnector.php:526 +#: lib/Connector/OpenWeatherMapConnector.php:521 msgid "Lithuanian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3248 -#: lib/Connector/OpenWeatherMapConnector.php:525 +#: lib/Connector/OpenWeatherMapConnector.php:520 msgid "Latvian" msgstr "" @@ -16508,7 +16508,7 @@ msgid "Maori" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3260 -#: lib/Connector/OpenWeatherMapConnector.php:527 +#: lib/Connector/OpenWeatherMapConnector.php:522 msgid "Macedonian" msgstr "" @@ -16553,7 +16553,7 @@ msgid "Dutch (Belgium)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3304 -#: lib/Connector/OpenWeatherMapConnector.php:529 +#: lib/Connector/OpenWeatherMapConnector.php:524 msgid "Dutch" msgstr "" @@ -16566,7 +16566,7 @@ msgid "Punjabi (India)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3316 -#: lib/Connector/OpenWeatherMapConnector.php:530 +#: lib/Connector/OpenWeatherMapConnector.php:525 msgid "Polish" msgstr "" @@ -16575,17 +16575,17 @@ msgid "Portuguese (Brazil)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3324 -#: lib/Connector/OpenWeatherMapConnector.php:531 +#: lib/Connector/OpenWeatherMapConnector.php:526 msgid "Portuguese" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3328 -#: lib/Connector/OpenWeatherMapConnector.php:533 +#: lib/Connector/OpenWeatherMapConnector.php:528 msgid "Romanian" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3332 -#: lib/Connector/OpenWeatherMapConnector.php:534 +#: lib/Connector/OpenWeatherMapConnector.php:529 msgid "Russian" msgstr "" @@ -16602,12 +16602,12 @@ msgid "Sinhala" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3348 -#: lib/Connector/OpenWeatherMapConnector.php:536 +#: lib/Connector/OpenWeatherMapConnector.php:531 msgid "Slovak" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3352 -#: lib/Connector/OpenWeatherMapConnector.php:537 +#: lib/Connector/OpenWeatherMapConnector.php:532 msgid "Slovenian" msgstr "" @@ -16620,7 +16620,7 @@ msgid "Serbian (Cyrillic)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3364 -#: lib/Connector/OpenWeatherMapConnector.php:539 +#: lib/Connector/OpenWeatherMapConnector.php:534 msgid "Serbian" msgstr "" @@ -16629,7 +16629,7 @@ msgid "Swati" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3372 -#: lib/Connector/OpenWeatherMapConnector.php:535 +#: lib/Connector/OpenWeatherMapConnector.php:530 msgid "Swedish" msgstr "" @@ -16654,7 +16654,7 @@ msgid "Tajik" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3396 -#: lib/Connector/OpenWeatherMapConnector.php:540 +#: lib/Connector/OpenWeatherMapConnector.php:535 msgid "Thai" msgstr "" @@ -16671,7 +16671,7 @@ msgid "Klingon" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3412 -#: lib/Connector/OpenWeatherMapConnector.php:541 +#: lib/Connector/OpenWeatherMapConnector.php:536 msgid "Turkish" msgstr "" @@ -16692,7 +16692,7 @@ msgid "Uyghur (China)" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3432 -#: lib/Connector/OpenWeatherMapConnector.php:542 +#: lib/Connector/OpenWeatherMapConnector.php:537 msgid "Ukrainian" msgstr "" @@ -16709,7 +16709,7 @@ msgid "Uzbek" msgstr "" #: cache/74/744308e2456d1f83f205ef9ab9fe3022.php:3448 -#: lib/Connector/OpenWeatherMapConnector.php:543 +#: lib/Connector/OpenWeatherMapConnector.php:538 msgid "Vietnamese" msgstr "" @@ -18110,7 +18110,7 @@ msgid "" msgstr "" #: cache/c5/c52b8109558dd3acc604e183177113c8.php:342 -#: lib/Controller/Library.php:1356 +#: lib/Controller/Library.php:1351 msgid "Sorry, Fonts do not have any editable properties." msgstr "" @@ -20736,10 +20736,10 @@ msgstr "" #: lib/Controller/Task.php:215 lib/Controller/Widget.php:298 #: lib/Controller/Template.php:539 lib/Controller/DisplayGroup.php:796 #: lib/Controller/DisplayGroup.php:2664 lib/Controller/DataSet.php:618 -#: lib/Controller/Notification.php:579 lib/Controller/Campaign.php:649 +#: lib/Controller/Notification.php:574 lib/Controller/Campaign.php:649 #: lib/Controller/Campaign.php:1385 lib/Controller/Folder.php:255 #: lib/Controller/Playlist.php:788 lib/Controller/SyncGroup.php:291 -#: lib/Controller/DataSetColumn.php:389 lib/Controller/Tag.php:359 +#: lib/Controller/DataSetColumn.php:389 lib/Controller/Tag.php:360 #: lib/Controller/Layout.php:488 lib/Controller/CypressTest.php:284 #: lib/Controller/DisplayProfile.php:318 lib/Controller/DisplayProfile.php:659 #: lib/Controller/User.php:633 lib/Controller/Applications.php:454 @@ -20752,20 +20752,20 @@ msgstr "" #: lib/Controller/Task.php:284 lib/Controller/Widget.php:573 #: lib/Controller/DisplayGroup.php:1025 lib/Controller/DataSet.php:912 -#: lib/Controller/ScheduleReport.php:426 lib/Controller/Notification.php:709 +#: lib/Controller/ScheduleReport.php:426 lib/Controller/Notification.php:704 #: lib/Controller/Campaign.php:959 lib/Controller/MenuBoard.php:446 #: lib/Controller/Folder.php:323 lib/Controller/Playlist.php:979 -#: lib/Controller/Transition.php:158 lib/Controller/Library.php:1411 +#: lib/Controller/Transition.php:158 lib/Controller/Library.php:1406 #: lib/Controller/SyncGroup.php:557 lib/Controller/DataSetColumn.php:596 -#: lib/Controller/Tag.php:544 lib/Controller/Connector.php:205 +#: lib/Controller/Tag.php:545 lib/Controller/Connector.php:205 #: lib/Controller/Layout.php:667 lib/Controller/Layout.php:789 #: lib/Controller/Layout.php:913 lib/Controller/MenuBoardCategory.php:437 #: lib/Controller/DisplayProfile.php:475 lib/Controller/User.php:905 #: lib/Controller/User.php:2532 lib/Controller/Applications.php:560 #: lib/Controller/Resolution.php:393 lib/Controller/Region.php:412 -#: lib/Controller/Region.php:582 lib/Controller/Display.php:1910 +#: lib/Controller/Region.php:582 lib/Controller/Display.php:1909 #: lib/Controller/MenuBoardProduct.php:640 -#: lib/Controller/PlayerSoftware.php:373 lib/Controller/Command.php:510 +#: lib/Controller/PlayerSoftware.php:372 lib/Controller/Command.php:510 #: lib/Controller/DataSetRss.php:561 lib/Controller/UserGroup.php:517 #: lib/Controller/DayPart.php:500 #, php-format @@ -20774,18 +20774,18 @@ msgstr "" #: lib/Controller/Task.php:331 lib/Controller/Widget.php:692 #: lib/Controller/DisplayGroup.php:1081 lib/Controller/DataSet.php:1002 -#: lib/Controller/ScheduleReport.php:464 lib/Controller/Notification.php:768 +#: lib/Controller/ScheduleReport.php:464 lib/Controller/Notification.php:763 #: lib/Controller/Campaign.php:1038 lib/Controller/SavedReport.php:283 #: lib/Controller/MenuBoard.php:523 lib/Controller/Folder.php:405 #: lib/Controller/Playlist.php:1060 lib/Controller/Library.php:1016 #: lib/Controller/SyncGroup.php:635 lib/Controller/DataSetColumn.php:685 -#: lib/Controller/Tag.php:629 lib/Controller/Layout.php:1027 +#: lib/Controller/Tag.php:630 lib/Controller/Layout.php:1027 #: lib/Controller/MenuBoardCategory.php:517 #: lib/Controller/DisplayProfile.php:554 lib/Controller/User.php:1022 #: lib/Controller/Applications.php:593 lib/Controller/Resolution.php:442 -#: lib/Controller/Region.php:469 lib/Controller/Display.php:1966 +#: lib/Controller/Region.php:469 lib/Controller/Display.php:1965 #: lib/Controller/MenuBoardProduct.php:722 -#: lib/Controller/PlayerSoftware.php:279 lib/Controller/Command.php:562 +#: lib/Controller/PlayerSoftware.php:278 lib/Controller/Command.php:562 #: lib/Controller/DataSetRss.php:656 lib/Controller/UserGroup.php:571 #: lib/Controller/DayPart.php:615 #, php-format @@ -20814,8 +20814,8 @@ msgstr "" #: lib/Controller/Widget.php:191 lib/Controller/Widget.php:456 #: lib/Controller/Widget.php:665 lib/Controller/Widget.php:823 #: lib/Controller/Widget.php:988 lib/Controller/Widget.php:1077 -#: lib/Controller/Widget.php:1447 lib/Controller/Widget.php:1556 -#: lib/Controller/Widget.php:1638 lib/Controller/Playlist.php:1311 +#: lib/Controller/Widget.php:1444 lib/Controller/Widget.php:1553 +#: lib/Controller/Widget.php:1635 lib/Controller/Playlist.php:1311 #: lib/Controller/Playlist.php:1476 lib/Controller/Layout.php:741 #: lib/Controller/Layout.php:844 lib/Controller/Region.php:229 #: lib/Controller/Region.php:376 lib/Controller/Region.php:462 @@ -20851,9 +20851,9 @@ msgstr "" #: lib/Controller/Widget.php:321 lib/Controller/Widget.php:448 #: lib/Controller/Widget.php:720 lib/Controller/Widget.php:815 #: lib/Controller/Widget.php:884 lib/Controller/Widget.php:980 -#: lib/Controller/Widget.php:1069 lib/Controller/Widget.php:1353 -#: lib/Controller/Widget.php:1439 lib/Controller/Widget.php:1548 -#: lib/Controller/Widget.php:1630 +#: lib/Controller/Widget.php:1069 lib/Controller/Widget.php:1350 +#: lib/Controller/Widget.php:1436 lib/Controller/Widget.php:1545 +#: lib/Controller/Widget.php:1627 msgid "This Widget is not shared with you with edit permission" msgstr "" @@ -20956,41 +20956,41 @@ msgstr "" msgid "Problem rendering widget" msgstr "" -#: lib/Controller/Widget.php:1499 +#: lib/Controller/Widget.php:1496 msgid "Edited Expiry" msgstr "" -#: lib/Controller/Widget.php:1563 +#: lib/Controller/Widget.php:1560 msgid "You can only set a target region on a Widget in the drawer." msgstr "" -#: lib/Controller/Widget.php:1587 +#: lib/Controller/Widget.php:1584 msgid "Target region set" msgstr "" -#: lib/Controller/Widget.php:1661 +#: lib/Controller/Widget.php:1658 msgid "Invalid element JSON" msgstr "" -#: lib/Controller/Widget.php:1667 +#: lib/Controller/Widget.php:1664 msgid "" "At least one element is required for this Widget. Please delete it if you no " "longer need it." msgstr "" -#: lib/Controller/Widget.php:1725 +#: lib/Controller/Widget.php:1733 msgid "Saved elements" msgstr "" -#: lib/Controller/Widget.php:1750 +#: lib/Controller/Widget.php:1758 msgid "Please supply a propertyId" msgstr "" -#: lib/Controller/Widget.php:1804 +#: lib/Controller/Widget.php:1812 msgid "Please provide a widgetId" msgstr "" -#: lib/Controller/Widget.php:1823 +#: lib/Controller/Widget.php:1831 msgid "Widget does not have a data type" msgstr "" @@ -21066,7 +21066,7 @@ msgid "DisplayGroups cannot be manually assigned to a Dynamic Group" msgstr "" #: lib/Controller/DisplayGroup.php:1403 lib/Controller/DisplayGroup.php:1416 -#: lib/Controller/Display.php:2135 lib/Controller/Display.php:2149 +#: lib/Controller/Display.php:2134 lib/Controller/Display.php:2148 msgid "Access Denied to DisplayGroup" msgstr "" @@ -21179,34 +21179,34 @@ msgstr "" msgid "Copied %s as %s" msgstr "" -#: lib/Controller/DataSet.php:1291 +#: lib/Controller/DataSet.php:1284 msgid "Missing JSON Body" msgstr "" -#: lib/Controller/DataSet.php:1298 +#: lib/Controller/DataSet.php:1291 msgid "Malformed JSON body, rows and uniqueKeys are required" msgstr "" -#: lib/Controller/DataSet.php:1338 +#: lib/Controller/DataSet.php:1331 #, php-format msgid "Incorrect date provided %s, expected date format Y-m-d H:i:s " msgstr "" -#: lib/Controller/DataSet.php:1387 +#: lib/Controller/DataSet.php:1380 msgid "No data found in request body" msgstr "" -#: lib/Controller/DataSet.php:1391 +#: lib/Controller/DataSet.php:1384 #, php-format msgid "Imported JSON into %s" msgstr "" -#: lib/Controller/DataSet.php:1455 +#: lib/Controller/DataSet.php:1448 #, php-format msgid "Run Test-Request for %s" msgstr "" -#: lib/Controller/DataSet.php:1570 +#: lib/Controller/DataSet.php:1563 #, php-format msgid "Cache cleared for %s" msgstr "" @@ -21328,7 +21328,7 @@ msgstr "" msgid "Delete?" msgstr "" -#: lib/Controller/Notification.php:570 +#: lib/Controller/Notification.php:565 msgid "Problem moving uploaded file into the Attachment Folder" msgstr "" @@ -21348,7 +21348,7 @@ msgstr "" msgid "No displays with View permissions" msgstr "" -#: lib/Controller/Stats.php:553 lib/Report/Bandwidth.php:276 +#: lib/Controller/Stats.php:553 lib/Report/Bandwidth.php:297 msgid "Deleted Displays" msgstr "" @@ -21454,7 +21454,7 @@ msgid "Please provide LayoutId" msgstr "" #: lib/Controller/Action.php:348 lib/Controller/Action.php:498 -#: lib/Controller/Action.php:562 lib/Controller/Layout.php:2963 +#: lib/Controller/Action.php:562 lib/Controller/Layout.php:2959 msgid "Layout is not checked out" msgstr "" @@ -21623,7 +21623,7 @@ msgid "" "tab to define" msgstr "" -#: lib/Controller/Playlist.php:1201 lib/Controller/Library.php:2279 +#: lib/Controller/Playlist.php:1201 lib/Controller/Library.php:2274 #: lib/Controller/Layout.php:2182 #, php-format msgid "Copied as %s" @@ -21663,7 +21663,7 @@ msgstr "" msgid "Specified Playlist item is not in use." msgstr "" -#: lib/Controller/Playlist.php:1713 lib/Controller/Library.php:2138 +#: lib/Controller/Playlist.php:1713 lib/Controller/Library.php:2133 #: lib/Controller/Layout.php:1703 msgid "Preview Layout" msgstr "" @@ -21718,8 +21718,8 @@ msgstr "" msgid "This Media has enable stat collection set to INHERIT" msgstr "" -#: lib/Controller/Library.php:734 lib/Controller/Font.php:156 -#: lib/Controller/PlayerSoftware.php:187 +#: lib/Controller/Library.php:734 lib/Controller/Font.php:155 +#: lib/Controller/PlayerSoftware.php:186 msgid "Download" msgstr "" @@ -21727,97 +21727,97 @@ msgstr "" msgid "This library item is in use." msgstr "" -#: lib/Controller/Library.php:1174 lib/Controller/Library.php:1391 -#: lib/Controller/Library.php:2468 +#: lib/Controller/Library.php:1174 lib/Controller/Library.php:1386 +#: lib/Controller/Library.php:2463 msgid "Cannot set Expiry date in the past" msgstr "" -#: lib/Controller/Library.php:1432 lib/Controller/Library.php:1497 +#: lib/Controller/Library.php:1427 lib/Controller/Library.php:1492 #: lib/Controller/Maintenance.php:97 msgid "Sorry this function is disabled." msgstr "" -#: lib/Controller/Library.php:1529 lib/Controller/Maintenance.php:269 +#: lib/Controller/Library.php:1524 lib/Controller/Maintenance.php:269 msgid "Library Tidy Complete" msgstr "" -#: lib/Controller/Library.php:1612 +#: lib/Controller/Library.php:1607 msgid "Cannot download region specific module" msgstr "" -#: lib/Controller/Library.php:1760 +#: lib/Controller/Library.php:1755 msgid "Route is available through the API" msgstr "" -#: lib/Controller/Library.php:1829 lib/Controller/Layout.php:2247 +#: lib/Controller/Library.php:1824 lib/Controller/Layout.php:2247 msgid "No tags to assign" msgstr "" -#: lib/Controller/Library.php:1840 lib/Controller/Layout.php:2258 +#: lib/Controller/Library.php:1835 lib/Controller/Layout.php:2258 #, php-format msgid "Tagged %s" msgstr "" -#: lib/Controller/Library.php:1903 lib/Controller/Layout.php:2323 +#: lib/Controller/Library.php:1898 lib/Controller/Layout.php:2323 msgid "No tags to unassign" msgstr "" -#: lib/Controller/Library.php:1914 lib/Controller/Layout.php:2333 +#: lib/Controller/Library.php:1909 lib/Controller/Layout.php:2333 #, php-format msgid "Untagged %s" msgstr "" -#: lib/Controller/Library.php:2061 lib/Controller/Library.php:2145 +#: lib/Controller/Library.php:2056 lib/Controller/Library.php:2140 msgid "Specified Media item is not in use." msgstr "" -#: lib/Controller/Library.php:2352 lib/Middleware/Theme.php:143 +#: lib/Controller/Library.php:2347 lib/Middleware/Theme.php:143 #, php-format msgid "This form accepts files up to a maximum size of %s" msgstr "" -#: lib/Controller/Library.php:2476 +#: lib/Controller/Library.php:2471 msgid "Provided URL is invalid" msgstr "" -#: lib/Controller/Library.php:2512 +#: lib/Controller/Library.php:2507 #, php-format msgid "" "Invalid Module type or extension. Module type %s does not allow for %s " "extension" msgstr "" -#: lib/Controller/Library.php:2545 +#: lib/Controller/Library.php:2540 msgid "Download rejected for an unknown reason." msgstr "" -#: lib/Controller/Library.php:2549 +#: lib/Controller/Library.php:2544 #, php-format msgid "Download rejected due to %s" msgstr "" -#: lib/Controller/Library.php:2556 +#: lib/Controller/Library.php:2551 msgid "Media upload from URL was successful" msgstr "" -#: lib/Controller/Library.php:2601 +#: lib/Controller/Library.php:2596 msgid "Invalid image data" msgstr "" -#: lib/Controller/Library.php:2707 +#: lib/Controller/Library.php:2702 #, php-format msgid "Media %s moved to Folder %s" msgstr "" -#: lib/Controller/Library.php:2794 +#: lib/Controller/Library.php:2789 msgid "Not configured by any active connector." msgstr "" -#: lib/Controller/Library.php:2844 +#: lib/Controller/Library.php:2839 msgid "Download failed" msgstr "" -#: lib/Controller/Library.php:2853 +#: lib/Controller/Library.php:2848 msgid "Imported" msgstr "" @@ -21830,31 +21830,31 @@ msgstr "" msgid "Free" msgstr "" -#: lib/Controller/StatusDashboard.php:417 +#: lib/Controller/StatusDashboard.php:410 msgid "Latest news not available." msgstr "" -#: lib/Controller/StatusDashboard.php:420 +#: lib/Controller/StatusDashboard.php:413 msgid "Latest news not enabled." msgstr "" -#: lib/Controller/Tag.php:253 +#: lib/Controller/Tag.php:254 msgid "Usage" msgstr "" -#: lib/Controller/Tag.php:491 +#: lib/Controller/Tag.php:492 msgid "Access denied System tags cannot be edited" msgstr "" -#: lib/Controller/Tag.php:617 +#: lib/Controller/Tag.php:618 msgid "Access denied System tags cannot be deleted" msgstr "" -#: lib/Controller/Tag.php:715 +#: lib/Controller/Tag.php:716 msgid "Edit multiple tags is not supported on this item" msgstr "" -#: lib/Controller/Tag.php:755 +#: lib/Controller/Tag.php:759 msgid "Tags Edited" msgstr "" @@ -21885,9 +21885,9 @@ msgstr "" #: lib/Controller/Layout.php:966 lib/Controller/Layout.php:1068 #: lib/Controller/Layout.php:1116 lib/Controller/Layout.php:1165 #: lib/Controller/Layout.php:1234 lib/Controller/Layout.php:1273 -#: lib/Controller/Layout.php:2689 lib/Controller/Layout.php:2740 -#: lib/Controller/Layout.php:2779 lib/Controller/Layout.php:2848 -#: lib/Controller/Layout.php:2908 lib/Controller/Layout.php:2958 +#: lib/Controller/Layout.php:2685 lib/Controller/Layout.php:2736 +#: lib/Controller/Layout.php:2775 lib/Controller/Layout.php:2844 +#: lib/Controller/Layout.php:2904 lib/Controller/Layout.php:2954 msgid "You do not have permissions to edit this layout" msgstr "" @@ -21983,62 +21983,62 @@ msgstr "" msgid "Cannot export Draft Layout" msgstr "" -#: lib/Controller/Layout.php:2618 +#: lib/Controller/Layout.php:2614 msgid "Layout background must be an image" msgstr "" -#: lib/Controller/Layout.php:2745 +#: lib/Controller/Layout.php:2741 msgid "Layout is already checked out" msgstr "" -#: lib/Controller/Layout.php:2754 +#: lib/Controller/Layout.php:2750 #, php-format msgid "Checked out %s" msgstr "" -#: lib/Controller/Layout.php:2870 +#: lib/Controller/Layout.php:2866 #, php-format msgid "Published %s" msgstr "" -#: lib/Controller/Layout.php:2877 +#: lib/Controller/Layout.php:2873 #, php-format msgid "Layout will be published on %s" msgstr "" -#: lib/Controller/Layout.php:2975 +#: lib/Controller/Layout.php:2971 #, php-format msgid "Discarded %s" msgstr "" -#: lib/Controller/Layout.php:3026 +#: lib/Controller/Layout.php:3022 msgid "" "This function is available only to User who originally locked this Layout." msgstr "" -#: lib/Controller/Layout.php:3115 lib/Entity/Layout.php:2257 +#: lib/Controller/Layout.php:3111 lib/Entity/Layout.php:2257 msgid "Empty Region" msgstr "" -#: lib/Controller/Layout.php:3219 +#: lib/Controller/Layout.php:3215 msgid "Incorrect image data" msgstr "" -#: lib/Controller/Layout.php:3244 +#: lib/Controller/Layout.php:3240 msgid "Thumbnail not found for Layout" msgstr "" -#: lib/Controller/Layout.php:3336 +#: lib/Controller/Layout.php:3332 #, php-format msgid "Please select %s" msgstr "" -#: lib/Controller/Layout.php:3354 +#: lib/Controller/Layout.php:3350 #, php-format msgid "Fetched %s" msgstr "" -#: lib/Controller/Layout.php:3477 +#: lib/Controller/Layout.php:3473 #, php-format msgid "Created %s" msgstr "" @@ -22163,14 +22163,14 @@ msgstr "" msgid "Authentication code incorrect" msgstr "" -#: lib/Controller/Font.php:421 lib/Controller/PlayerSoftware.php:630 +#: lib/Controller/Font.php:416 lib/Controller/PlayerSoftware.php:625 #: lib/Helper/XiboUploadHandler.php:440 msgid "" "Sorry this is a corrupted upload, the file size doesn't match what we're " "expecting." msgstr "" -#: lib/Controller/Font.php:582 +#: lib/Controller/Font.php:578 msgid "Unable to write to the library" msgstr "" @@ -22286,8 +22286,8 @@ msgid "You do not have permission to edit these permissions." msgstr "" #: lib/Controller/User.php:1663 lib/Controller/User.php:1792 -#: lib/Controller/User.php:2028 lib/Controller/Display.php:2024 -#: lib/Controller/Display.php:2059 lib/Controller/Display.php:2064 +#: lib/Controller/User.php:2028 lib/Controller/Display.php:2023 +#: lib/Controller/Display.php:2058 lib/Controller/Display.php:2063 msgid "The array of ids is empty!" msgstr "" @@ -22485,90 +22485,90 @@ msgstr "" msgid "Unknown" msgstr "" -#: lib/Controller/Display.php:1957 +#: lib/Controller/Display.php:1956 msgid "Cannot delete a Lead Display of a Sync Group" msgstr "" -#: lib/Controller/Display.php:2101 +#: lib/Controller/Display.php:2100 msgid "Displays Updated" msgstr "" -#: lib/Controller/Display.php:2159 +#: lib/Controller/Display.php:2158 #, php-format msgid "%s assigned to Display Groups" msgstr "" -#: lib/Controller/Display.php:2250 +#: lib/Controller/Display.php:2246 msgid "once it has connected for the first time" msgstr "" -#: lib/Controller/Display.php:2317 lib/Controller/Display.php:3032 -#: lib/Controller/Display.php:3156 +#: lib/Controller/Display.php:2313 lib/Controller/Display.php:3028 +#: lib/Controller/Display.php:3152 #, php-format msgid "Request sent for %s" msgstr "" -#: lib/Controller/Display.php:2345 lib/Controller/Display.php:2397 +#: lib/Controller/Display.php:2341 lib/Controller/Display.php:2393 msgid "" "This display has no mac address recorded against it yet. Make sure the " "display is running." msgstr "" -#: lib/Controller/Display.php:2420 +#: lib/Controller/Display.php:2416 #, php-format msgid "Wake on Lan sent for %s" msgstr "" -#: lib/Controller/Display.php:2529 +#: lib/Controller/Display.php:2525 #, php-format msgid "Alert for Display %s" msgstr "" -#: lib/Controller/Display.php:2531 +#: lib/Controller/Display.php:2527 #, php-format msgid "Display ID %d is offline since %s." msgstr "" -#: lib/Controller/Display.php:2630 +#: lib/Controller/Display.php:2626 #, php-format msgid "Authorised set to %d for %s" msgstr "" -#: lib/Controller/Display.php:2731 +#: lib/Controller/Display.php:2727 #, php-format msgid "Default Layout with name %s set for %s" msgstr "" -#: lib/Controller/Display.php:2817 +#: lib/Controller/Display.php:2813 msgid "Provided CMS URL is invalid" msgstr "" -#: lib/Controller/Display.php:2821 +#: lib/Controller/Display.php:2817 msgid "New CMS URL can have maximum of 1000 characters" msgstr "" -#: lib/Controller/Display.php:2825 +#: lib/Controller/Display.php:2821 msgid "Provided CMS Key is invalid" msgstr "" -#: lib/Controller/Display.php:2833 +#: lib/Controller/Display.php:2829 msgid "Invalid Two Factor Authentication Code" msgstr "" -#: lib/Controller/Display.php:2883 +#: lib/Controller/Display.php:2879 #, php-format msgid "Cancelled CMS Transfer for %s" msgstr "" -#: lib/Controller/Display.php:2921 +#: lib/Controller/Display.php:2917 msgid "Code cannot be empty" msgstr "" -#: lib/Controller/Display.php:2949 +#: lib/Controller/Display.php:2945 msgid "Provided user_code does not exist" msgstr "" -#: lib/Controller/Display.php:3025 lib/Controller/Display.php:3149 +#: lib/Controller/Display.php:3021 lib/Controller/Display.php:3145 msgid "XMR is not configured for this Display" msgstr "" @@ -22576,12 +22576,12 @@ msgstr "" msgid "The Library Location you have picked is not writeable" msgstr "" -#: lib/Controller/Settings.php:335 lib/Entity/Display.php:753 +#: lib/Controller/Settings.php:335 lib/Entity/Display.php:754 #: lib/Widget/Validator/DisplayOrGeoValidator.php:55 msgid "The latitude entered is not valid." msgstr "" -#: lib/Controller/Settings.php:345 lib/Entity/Display.php:749 +#: lib/Controller/Settings.php:345 lib/Entity/Display.php:750 #: lib/Widget/Validator/DisplayOrGeoValidator.php:60 msgid "The longitude entered is not valid." msgstr "" @@ -22598,7 +22598,7 @@ msgstr "" msgid "Added Menu Board Product" msgstr "" -#: lib/Controller/PlayerSoftware.php:470 +#: lib/Controller/PlayerSoftware.php:469 msgid "File available only for SSSP displays" msgstr "" @@ -22626,7 +22626,7 @@ msgid "%s scheduled on %s" msgstr "" #: lib/Controller/Schedule.php:378 lib/Controller/Schedule.php:636 -#: lib/Controller/Schedule.php:2328 lib/Entity/Schedule.php:1851 +#: lib/Controller/Schedule.php:2329 lib/Entity/Schedule.php:1851 msgid "Private Item" msgstr "" @@ -22662,7 +22662,7 @@ msgstr "" msgid "Edited Event" msgstr "" -#: lib/Controller/Schedule.php:2481 lib/Factory/RequiredFileFactory.php:414 +#: lib/Controller/Schedule.php:2502 lib/Factory/RequiredFileFactory.php:414 msgid "Unknown type" msgstr "" @@ -23331,44 +23331,44 @@ msgstr "" msgid "The Region dimensions cannot be empty or negative" msgstr "" -#: lib/Entity/Display.php:696 +#: lib/Entity/Display.php:697 msgid "Can not have a display without a name" msgstr "" -#: lib/Entity/Display.php:700 +#: lib/Entity/Display.php:701 msgid "Can not have a display without a hardware key" msgstr "" -#: lib/Entity/Display.php:705 +#: lib/Entity/Display.php:706 msgid "" "Wake on Lan is enabled, but you have not specified a time to wake the display" msgstr "" -#: lib/Entity/Display.php:712 +#: lib/Entity/Display.php:713 msgid "BroadCast Address is not a valid IP Address" msgstr "" -#: lib/Entity/Display.php:720 +#: lib/Entity/Display.php:721 msgid "CIDR subnet mask is not a number within the range of 0 to 32." msgstr "" -#: lib/Entity/Display.php:734 +#: lib/Entity/Display.php:735 msgid "" "Pattern of secureOn-password is not \"xx-xx-xx-xx-xx-xx\" (x = digit or " "CAPITAL letter)" msgstr "" -#: lib/Entity/Display.php:758 +#: lib/Entity/Display.php:759 msgid "Bandwidth limit must be a whole number greater than 0." msgstr "" -#: lib/Entity/Display.php:781 +#: lib/Entity/Display.php:782 msgid "" "Please set a Default Layout directly on this Display or in CMS Administrator " "Settings" msgstr "" -#: lib/Entity/Display.php:868 +#: lib/Entity/Display.php:869 #, php-format msgid "You have exceeded your maximum number of authorised displays. %d" msgstr "" @@ -23581,184 +23581,184 @@ msgstr "" msgid "%d of %d player actions failed" msgstr "" -#: lib/Service/ConfigService.php:289 +#: lib/Service/ConfigService.php:295 #, php-format msgid "The theme \"%s\" does not exist" msgstr "" -#: lib/Service/ConfigService.php:637 +#: lib/Service/ConfigService.php:644 msgid "PHP Version" msgstr "" -#: lib/Service/ConfigService.php:639 +#: lib/Service/ConfigService.php:646 #, php-format msgid "PHP version %s or later required." msgstr "" -#: lib/Service/ConfigService.php:642 +#: lib/Service/ConfigService.php:649 msgid "Cache File System Permissions" msgstr "" -#: lib/Service/ConfigService.php:644 +#: lib/Service/ConfigService.php:651 msgid "Write permissions are required for cache/" msgstr "" -#: lib/Service/ConfigService.php:647 +#: lib/Service/ConfigService.php:654 msgid "MySQL database (PDO MySql)" msgstr "" -#: lib/Service/ConfigService.php:649 +#: lib/Service/ConfigService.php:656 msgid "PDO support with MySQL drivers must be enabled in PHP." msgstr "" -#: lib/Service/ConfigService.php:652 +#: lib/Service/ConfigService.php:659 msgid "JSON Extension" msgstr "" -#: lib/Service/ConfigService.php:654 +#: lib/Service/ConfigService.php:661 msgid "PHP JSON extension required to function." msgstr "" -#: lib/Service/ConfigService.php:657 +#: lib/Service/ConfigService.php:664 msgid "SOAP Extension" msgstr "" -#: lib/Service/ConfigService.php:659 +#: lib/Service/ConfigService.php:666 msgid "PHP SOAP extension required to function." msgstr "" -#: lib/Service/ConfigService.php:662 +#: lib/Service/ConfigService.php:669 msgid "GD Extension" msgstr "" -#: lib/Service/ConfigService.php:664 +#: lib/Service/ConfigService.php:671 msgid "PHP GD extension required to function." msgstr "" -#: lib/Service/ConfigService.php:667 +#: lib/Service/ConfigService.php:674 msgid "Session" msgstr "" -#: lib/Service/ConfigService.php:669 +#: lib/Service/ConfigService.php:676 msgid "PHP session support required to function." msgstr "" -#: lib/Service/ConfigService.php:672 +#: lib/Service/ConfigService.php:679 msgid "FileInfo" msgstr "" -#: lib/Service/ConfigService.php:674 +#: lib/Service/ConfigService.php:681 msgid "" "Requires PHP FileInfo support to function. If you are on Windows you need to " "enable the php_fileinfo.dll in your php.ini file." msgstr "" -#: lib/Service/ConfigService.php:677 +#: lib/Service/ConfigService.php:684 msgid "PCRE" msgstr "" -#: lib/Service/ConfigService.php:679 +#: lib/Service/ConfigService.php:686 msgid "PHP PCRE support to function." msgstr "" -#: lib/Service/ConfigService.php:682 +#: lib/Service/ConfigService.php:689 msgid "Gettext" msgstr "" -#: lib/Service/ConfigService.php:684 +#: lib/Service/ConfigService.php:691 msgid "PHP Gettext support to function." msgstr "" -#: lib/Service/ConfigService.php:687 +#: lib/Service/ConfigService.php:694 msgid "DOM Extension" msgstr "" -#: lib/Service/ConfigService.php:689 +#: lib/Service/ConfigService.php:696 msgid "PHP DOM core functionality enabled." msgstr "" -#: lib/Service/ConfigService.php:692 +#: lib/Service/ConfigService.php:699 msgid "DOM XML Extension" msgstr "" -#: lib/Service/ConfigService.php:694 +#: lib/Service/ConfigService.php:701 msgid "PHP DOM XML extension to function." msgstr "" -#: lib/Service/ConfigService.php:697 +#: lib/Service/ConfigService.php:704 msgid "Allow PHP to open external URLs" msgstr "" -#: lib/Service/ConfigService.php:699 +#: lib/Service/ConfigService.php:706 msgid "" "You must have the curl extension enabled or PHP configured with " "\"allow_url_fopen = On\" for the CMS to access external resources. We " "strongly recommend curl." msgstr "" -#: lib/Service/ConfigService.php:703 +#: lib/Service/ConfigService.php:710 msgid "DateTimeZone" msgstr "" -#: lib/Service/ConfigService.php:705 +#: lib/Service/ConfigService.php:712 msgid "" "This enables us to get a list of time zones supported by the hosting server." msgstr "" -#: lib/Service/ConfigService.php:709 +#: lib/Service/ConfigService.php:716 msgid "ZIP" msgstr "" -#: lib/Service/ConfigService.php:711 +#: lib/Service/ConfigService.php:718 msgid "This enables import / export of layouts." msgstr "" -#: lib/Service/ConfigService.php:714 +#: lib/Service/ConfigService.php:721 msgid "Support for uploading large files is recommended." msgstr "" -#: lib/Service/ConfigService.php:715 +#: lib/Service/ConfigService.php:722 msgid "" "We suggest setting your PHP post_max_size and upload_max_filesize to at " "least 128M, and also increasing your max_execution_time to at least 120 " "seconds." msgstr "" -#: lib/Service/ConfigService.php:717 +#: lib/Service/ConfigService.php:724 msgid "Large File Uploads" msgstr "" -#: lib/Service/ConfigService.php:723 +#: lib/Service/ConfigService.php:730 msgid "cURL" msgstr "" -#: lib/Service/ConfigService.php:725 +#: lib/Service/ConfigService.php:732 msgid "cURL is used to fetch data from the Internet or Local Network" msgstr "" -#: lib/Service/ConfigService.php:728 +#: lib/Service/ConfigService.php:735 msgid "ZeroMQ" msgstr "" -#: lib/Service/ConfigService.php:730 +#: lib/Service/ConfigService.php:737 msgid "" "ZeroMQ is used to send messages to XMR which allows push communications with " "player" msgstr "" -#: lib/Service/ConfigService.php:734 +#: lib/Service/ConfigService.php:741 msgid "OpenSSL" msgstr "" -#: lib/Service/ConfigService.php:736 +#: lib/Service/ConfigService.php:743 msgid "OpenSSL is used to seal and verify messages sent to XMR" msgstr "" -#: lib/Service/ConfigService.php:740 +#: lib/Service/ConfigService.php:747 msgid "SimpleXML" msgstr "" -#: lib/Service/ConfigService.php:742 +#: lib/Service/ConfigService.php:749 msgid "SimpleXML is used to parse RSS feeds and other XML data sources" msgstr "" @@ -24629,68 +24629,68 @@ msgstr "" msgid "Asset file does not exist" msgstr "" -#: lib/Widget/Definition/Property.php:216 +#: lib/Widget/Definition/Property.php:221 #, php-format msgid "Value too large for %s" msgstr "" -#: lib/Widget/Definition/Property.php:256 +#: lib/Widget/Definition/Property.php:261 #, php-format msgid "Missing required property %s" msgstr "" -#: lib/Widget/Definition/Property.php:267 +#: lib/Widget/Definition/Property.php:272 #, php-format msgid "%s must be a valid URI" msgstr "" -#: lib/Widget/Definition/Property.php:280 -#: lib/Widget/Definition/Property.php:292 +#: lib/Widget/Definition/Property.php:285 +#: lib/Widget/Definition/Property.php:297 msgid "" "That is not a valid date interval, please use natural language such as 1 week" msgstr "" -#: lib/Widget/Definition/Property.php:302 +#: lib/Widget/Definition/Property.php:307 #, php-format msgid "%s must equal %s" msgstr "" -#: lib/Widget/Definition/Property.php:311 +#: lib/Widget/Definition/Property.php:316 #, php-format msgid "%s must not equal %s" msgstr "" -#: lib/Widget/Definition/Property.php:320 +#: lib/Widget/Definition/Property.php:325 #, php-format msgid "%s must contain %s" msgstr "" -#: lib/Widget/Definition/Property.php:330 +#: lib/Widget/Definition/Property.php:335 #, php-format msgid "%s must not contain %s" msgstr "" -#: lib/Widget/Definition/Property.php:341 +#: lib/Widget/Definition/Property.php:346 #, php-format msgid "%s must be less than %s" msgstr "" -#: lib/Widget/Definition/Property.php:352 +#: lib/Widget/Definition/Property.php:357 #, php-format msgid "%s must be less than or equal to %s" msgstr "" -#: lib/Widget/Definition/Property.php:363 +#: lib/Widget/Definition/Property.php:368 #, php-format msgid "%s must be greater than or equal to %s" msgstr "" -#: lib/Widget/Definition/Property.php:374 +#: lib/Widget/Definition/Property.php:379 #, php-format msgid "%s must be greater than %s" msgstr "" -#: lib/Widget/Definition/Property.php:446 +#: lib/Widget/Definition/Property.php:451 #, php-format msgid "%s is not a valid option" msgstr "" @@ -24775,17 +24775,17 @@ msgid "" "Layout code." msgstr "" -#: lib/Xmds/Soap.php:2615 +#: lib/Xmds/Soap.php:2576 #, php-format msgid "Recovery for Display %s" msgstr "" -#: lib/Xmds/Soap.php:2616 +#: lib/Xmds/Soap.php:2578 #, php-format msgid "Display ID %d is now back online %s" msgstr "" -#: lib/Xmds/Soap.php:2687 +#: lib/Xmds/Soap.php:2717 msgid "Bandwidth allowance exceeded" msgstr "" @@ -24847,7 +24847,7 @@ msgstr "" msgid "Unable to import row %d" msgstr "" -#: lib/Helper/UploadHandler.php:78 lib/Helper/XiboUploadHandler.php:66 +#: lib/Helper/UploadHandler.php:77 lib/Helper/XiboUploadHandler.php:66 #: lib/Helper/LayoutUploadHandler.php:55 #, php-format msgid "Your library is full. Library Limit: %s K" @@ -25163,31 +25163,31 @@ msgstr "" msgid "Currency data invalid" msgstr "" -#: lib/Connector/OpenWeatherMapConnector.php:128 +#: lib/Connector/OpenWeatherMapConnector.php:123 msgid "Unable to get weather results." msgstr "" -#: lib/Connector/OpenWeatherMapConnector.php:505 +#: lib/Connector/OpenWeatherMapConnector.php:500 msgid "Chinese Simplified" msgstr "" -#: lib/Connector/OpenWeatherMapConnector.php:506 +#: lib/Connector/OpenWeatherMapConnector.php:501 msgid "Chinese Traditional" msgstr "" -#: lib/Connector/OpenWeatherMapConnector.php:513 +#: lib/Connector/OpenWeatherMapConnector.php:508 msgid "Persian (Farsi)" msgstr "" -#: lib/Connector/OpenWeatherMapConnector.php:528 +#: lib/Connector/OpenWeatherMapConnector.php:523 msgid "Norwegian" msgstr "" -#: lib/Connector/OpenWeatherMapConnector.php:532 +#: lib/Connector/OpenWeatherMapConnector.php:527 msgid "Português Brasil" msgstr "" -#: lib/Connector/OpenWeatherMapConnector.php:544 +#: lib/Connector/OpenWeatherMapConnector.php:539 msgid "Zulu" msgstr "" @@ -25355,11 +25355,11 @@ msgstr "" msgid "Publishing layouts with set publish dates" msgstr "" -#: lib/XTR/MaintenanceRegularTask.php:499 +#: lib/XTR/MaintenanceRegularTask.php:500 msgid "Assess Dynamic Display Groups" msgstr "" -#: lib/XTR/MaintenanceRegularTask.php:538 +#: lib/XTR/MaintenanceRegularTask.php:546 msgid "Tidy Ad Campaign Schedules" msgstr "" @@ -25558,7 +25558,7 @@ msgstr "" msgid "Uptime" msgstr "" -#: lib/Report/Bandwidth.php:113 +#: lib/Report/Bandwidth.php:133 #, php-format msgid "%s bandwidth report" msgstr "" diff --git a/locale/el.mo b/locale/el.mo index e537158536..ce6bb8b6d6 100755 Binary files a/locale/el.mo and b/locale/el.mo differ diff --git a/locale/en_GB.mo b/locale/en_GB.mo index 1d122a14a5..9c1a748ab4 100755 Binary files a/locale/en_GB.mo and b/locale/en_GB.mo differ diff --git a/locale/es.mo b/locale/es.mo index aefc068e2d..a3af0c91af 100755 Binary files a/locale/es.mo and b/locale/es.mo differ diff --git a/locale/et.mo b/locale/et.mo index 5785ec7c2d..874ed5b112 100755 Binary files a/locale/et.mo and b/locale/et.mo differ diff --git a/locale/eu.mo b/locale/eu.mo index d9b26f76d4..b6cfd139bf 100755 Binary files a/locale/eu.mo and b/locale/eu.mo differ diff --git a/locale/fa.mo b/locale/fa.mo index eb342cebf4..daa7c3c72a 100755 Binary files a/locale/fa.mo and b/locale/fa.mo differ diff --git a/locale/fi.mo b/locale/fi.mo index 21f59c56c1..624d8cb7ad 100755 Binary files a/locale/fi.mo and b/locale/fi.mo differ diff --git a/locale/fr.mo b/locale/fr.mo index 43c6a08032..0a88214368 100755 Binary files a/locale/fr.mo and b/locale/fr.mo differ diff --git a/locale/fr_CA.mo b/locale/fr_CA.mo index 8cd9c6e7aa..d7f8e58351 100644 Binary files a/locale/fr_CA.mo and b/locale/fr_CA.mo differ diff --git a/locale/he.mo b/locale/he.mo index 7c6c4fa669..ed3e92bfe4 100755 Binary files a/locale/he.mo and b/locale/he.mo differ diff --git a/locale/hi.mo b/locale/hi.mo index 12e508ddb1..0c83504ab7 100755 Binary files a/locale/hi.mo and b/locale/hi.mo differ diff --git a/locale/hr.mo b/locale/hr.mo index 8f99b3eab8..71e33c9d63 100755 Binary files a/locale/hr.mo and b/locale/hr.mo differ diff --git a/locale/hu.mo b/locale/hu.mo index 009cd78681..e8b59c2241 100755 Binary files a/locale/hu.mo and b/locale/hu.mo differ diff --git a/locale/id.mo b/locale/id.mo index a4502a2472..7f9bb29ff7 100755 Binary files a/locale/id.mo and b/locale/id.mo differ diff --git a/locale/it.mo b/locale/it.mo index acfe0b18a3..bb235c078e 100755 Binary files a/locale/it.mo and b/locale/it.mo differ diff --git a/locale/ja.mo b/locale/ja.mo index 9ad04dc79a..dd2681175d 100755 Binary files a/locale/ja.mo and b/locale/ja.mo differ diff --git a/locale/ko.mo b/locale/ko.mo index dd7d3ee54a..2633f9aed6 100755 Binary files a/locale/ko.mo and b/locale/ko.mo differ diff --git a/locale/ku.mo b/locale/ku.mo index f17fd15081..0e4de8c11e 100755 Binary files a/locale/ku.mo and b/locale/ku.mo differ diff --git a/locale/lb.mo b/locale/lb.mo index 199a61035e..cddddc6a28 100755 Binary files a/locale/lb.mo and b/locale/lb.mo differ diff --git a/locale/lo.mo b/locale/lo.mo index 2549e95523..8f283ab1ab 100755 Binary files a/locale/lo.mo and b/locale/lo.mo differ diff --git a/locale/lt.mo b/locale/lt.mo index 9956a3b7e0..5b5784f8f6 100755 Binary files a/locale/lt.mo and b/locale/lt.mo differ diff --git a/locale/nb.mo b/locale/nb.mo index 938d74b91c..fbe39a34a7 100755 Binary files a/locale/nb.mo and b/locale/nb.mo differ diff --git a/locale/nl.mo b/locale/nl.mo index 33c474b083..36b38f5660 100755 Binary files a/locale/nl.mo and b/locale/nl.mo differ diff --git a/locale/nl_NL.mo b/locale/nl_NL.mo index 833bd408ab..7ce50272ac 100755 Binary files a/locale/nl_NL.mo and b/locale/nl_NL.mo differ diff --git a/locale/pl.mo b/locale/pl.mo index c46ba557e1..b7c83e28c9 100755 Binary files a/locale/pl.mo and b/locale/pl.mo differ diff --git a/locale/pt.mo b/locale/pt.mo index c3a46b74fe..5ed3001a0c 100755 Binary files a/locale/pt.mo and b/locale/pt.mo differ diff --git a/locale/pt_BR.mo b/locale/pt_BR.mo index 5b280d1f5f..25afaa5b02 100755 Binary files a/locale/pt_BR.mo and b/locale/pt_BR.mo differ diff --git a/locale/ro.mo b/locale/ro.mo index bc72b480de..1f93788524 100755 Binary files a/locale/ro.mo and b/locale/ro.mo differ diff --git a/locale/ru.mo b/locale/ru.mo index c13876d654..261186f399 100755 Binary files a/locale/ru.mo and b/locale/ru.mo differ diff --git a/locale/sk.mo b/locale/sk.mo index 83a600e3b8..79f3defa8b 100755 Binary files a/locale/sk.mo and b/locale/sk.mo differ diff --git a/locale/sl.mo b/locale/sl.mo index dc6c24c22d..09b842ba9f 100755 Binary files a/locale/sl.mo and b/locale/sl.mo differ diff --git a/locale/sr@latin.mo b/locale/sr@latin.mo index 3a599aa201..e890efada8 100755 Binary files a/locale/sr@latin.mo and b/locale/sr@latin.mo differ diff --git a/locale/sv.mo b/locale/sv.mo index eac274a1d7..6463ee6bcf 100755 Binary files a/locale/sv.mo and b/locale/sv.mo differ diff --git a/locale/th.mo b/locale/th.mo index 144f271bf7..12e1e40fa2 100755 Binary files a/locale/th.mo and b/locale/th.mo differ diff --git a/locale/tr.mo b/locale/tr.mo index a014c3099c..08160af788 100755 Binary files a/locale/tr.mo and b/locale/tr.mo differ diff --git a/locale/vi.mo b/locale/vi.mo index 2cfaf27bd7..e2d7a4f823 100755 Binary files a/locale/vi.mo and b/locale/vi.mo differ diff --git a/locale/zh_CN.mo b/locale/zh_CN.mo index d2706b08b8..5604b2e632 100755 Binary files a/locale/zh_CN.mo and b/locale/zh_CN.mo differ diff --git a/locale/zh_TW.mo b/locale/zh_TW.mo index 1e22ef383f..87e77cde60 100755 Binary files a/locale/zh_TW.mo and b/locale/zh_TW.mo differ diff --git a/modules/forecastio.xml b/modules/forecastio.xml index 0abde683a6..e6102edfbb 100755 --- a/modules/forecastio.xml +++ b/modules/forecastio.xml @@ -100,35 +100,7 @@ - - - {% trans "No results returned, please configure Open Weather Map connector!" %} - - ]]> - - 0) { moment.locale(properties.lang); diff --git a/modules/src/xibo-finance-render.js b/modules/src/xibo-finance-render.js index 5d190b0707..43247d1b45 100644 --- a/modules/src/xibo-finance-render.js +++ b/modules/src/xibo-finance-render.js @@ -1,7 +1,7 @@ /* - * Copyright (C) 2023 Xibo Signage Ltd + * Copyright (C) 2024 Xibo Signage Ltd * - * Xibo - Digital Signage - http://www.xibo.org.uk + * Xibo - Digital Signage - https://xibosignage.com * * This file is part of Xibo. * @@ -28,7 +28,7 @@ jQuery.fn.extend({ duration: '30', durationIsPerItem: false, numItems: items.length, - maxItemsPerPage: 5, + itemsPerPage: 5, previewWidth: 0, previewHeight: 0, scaleOverride: 0, @@ -36,6 +36,10 @@ jQuery.fn.extend({ options = $.extend({}, defaults, options); + if (!options.itemsPerPage) { + options.itemsPerPage = 1; + } + // Calculate the dimensions of this itemoptions.numItems // based on the preview/original dimensions let width = height = 0; @@ -65,8 +69,8 @@ jQuery.fn.extend({ this.each(function(_idx, _elem) { // How many pages to we need? const numberOfPages = - (options.numItems > options.maxItemsPerPage) ? - Math.ceil(options.numItems / options.maxItemsPerPage) : 1; + (options.numItems > options.itemsPerPage) ? + Math.ceil(options.numItems / options.itemsPerPage) : 1; const $mainContainer = $(_elem); // Destroy any existing cycle @@ -91,9 +95,9 @@ jQuery.fn.extend({ for (let i = 0; i < numberOfPages; i++) { // Create a page const $itemsHTML = $('
').addClass('page'); - for (let j = 0; j < options.maxItemsPerPage; j++) { - if (((i * options.maxItemsPerPage) + j) < options.numItems) { - const $item = $(items[(i * options.maxItemsPerPage) + j]); + for (let j = 0; j < options.itemsPerPage; j++) { + if (((i * options.itemsPerPage) + j) < options.numItems) { + const $item = $(items[(i * options.itemsPerPage) + j]); // Clone and append the item to the page // and remove template-item class when isEditor = true (isEditor ? $item.clone() : $item).appendTo($itemsHTML) diff --git a/modules/src/xibo-text-render.js b/modules/src/xibo-text-render.js index d302348298..14cead02c6 100644 --- a/modules/src/xibo-text-render.js +++ b/modules/src/xibo-text-render.js @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 Xibo Signage Ltd + * Copyright (C) 2024 Xibo Signage Ltd * * Xibo - Digital Signage - https://xibosignage.com * @@ -326,6 +326,11 @@ jQuery.fn.extend({ // Make sure the speed is something sensible options.speed = (options.speed === 0) ? 1 : options.speed; + + // Set the content div height, if we don't do this when the marquee + // plugin floats the content inside, this goes to 0 and up/down + // marquees don't work + $contentDiv.css('height', height); } if (marquee) { @@ -364,8 +369,10 @@ jQuery.fn.extend({ options.effect === 'marqueeUp' || options.effect === 'marqueeDown' ) { - $contentDiv.css('height', '100%'); - $contentDiv.find('.scroll').css('height', '100%').children() + // Set the height of the scroller to 100% + $contentDiv.find('.scroll') + .css('height', '100%') + .children() .css({'white-space': 'normal', float: 'none'}); } diff --git a/modules/templates/currency-static.xml b/modules/templates/currency-static.xml index 2b0aabefcf..4168dc6072 100644 --- a/modules/templates/currency-static.xml +++ b/modules/templates/currency-static.xml @@ -1,5 +1,5 @@