diff --git a/lib/Controller/Applications.php b/lib/Controller/Applications.php index 67d816f652..81c2af0a58 100644 --- a/lib/Controller/Applications.php +++ b/lib/Controller/Applications.php @@ -173,14 +173,32 @@ public function grid(Request $request, Response $response) public function authorizeRequest(Request $request, Response $response) { // Pull authorize params from our session - if (!$authParams = $this->session->get('authParams')) { + /** @var AuthorizationRequest $authParams */ + $authParams = $this->session->get('authParams'); + if (!$authParams) { throw new InvalidArgumentException(__('Authorisation Parameters missing from session.'), 'authParams'); } + // Process any scopes. + $scopes = []; + $authScopes = $authParams->getScopes(); + if ($authScopes !== null) { + foreach ($authScopes as $scope) { + $this->getLog()->debug('Loading scope: ' . $scope->getIdentifier()); + $scopes[] = $this->applicationScopeFactory->getById($scope->getIdentifier()); + } + } + + // `all` is the default scope + if (count($scopes) <= 0) { + $scopes[] = $this->applicationScopeFactory->getById('all'); + } + // Get, show page $this->getState()->template = 'applications-authorize-page'; $this->getState()->setData([ - 'authParams' => $authParams + 'authParams' => $authParams, + 'scopes' => $scopes ]); return $this->render($request, $response); @@ -196,20 +214,17 @@ public function authorizeRequest(Request $request, Response $response) public function authorize(Request $request, Response $response) { // Pull authorize params from our session - if (!$authParams = $this->session->get('authParams')) { + /** @var AuthorizationRequest $authRequest */ + $authRequest = $this->session->get('authParams'); + if (!$authRequest) { throw new InvalidArgumentException(__('Authorisation Parameters missing from session.'), 'authParams'); } $sanitizedQueryParams = $this->getSanitizer($request->getParams()); - // get auth server - /** @var AuthorizationRequest $authRequest */ - $authRequest = $this->session->get('authParams'); - $apiKeyPaths = $this->getConfig()->getApiKeyDetails(); - $privateKey = $apiKeyPaths['privateKeyPath']; - $encryptionKey = $apiKeyPaths['publicKeyPath']; + $encryptionKey = $apiKeyPaths['encryptionKey']; $server = new AuthorizationServer( $this->applicationFactory, @@ -231,21 +246,18 @@ public function authorize(Request $request, Response $response) // Default scope $server->setDefaultScope('all'); + // get oauth User Entity and set the UserId to the current web userId + $authRequest->setUser($this->getUser()); + // We are authorized if ($sanitizedQueryParams->getString('authorization') === 'Approve') { - $authRequest->setAuthorizationApproved(true); - - // get oauth User Entity and set the UserId to the current web userId - $authRequest->setUser($this->getUser()); - - // Redirect back to the home page - return $server->completeAuthorizationRequest($authRequest, $response); - } - else { + } else { $authRequest->setAuthorizationApproved(false); - return $server->completeAuthorizationRequest($authRequest, $response); } + + // Redirect back to the specified redirect url + return $server->completeAuthorizationRequest($authRequest, $response); } /** @@ -470,6 +482,7 @@ public function edit(Request $request, Response $response, $id) $client->name = $sanitizedParams->getString('name'); $client->authCode = $sanitizedParams->getCheckbox('authCode'); $client->clientCredentials = $sanitizedParams->getCheckbox('clientCredentials'); + $client->isConfidential = $sanitizedParams->getCheckbox('isConfidential'); if ($sanitizedParams->getCheckbox('resetKeys') == 1) { $client->resetSecret(); diff --git a/lib/Controller/DayPart.php b/lib/Controller/DayPart.php index 7f50f0fa39..cfea6be78f 100644 --- a/lib/Controller/DayPart.php +++ b/lib/Controller/DayPart.php @@ -1,6 +1,6 @@ dayPartFactory->getById($id); + if ($dayPart->isSystemDayPart()) { + throw new InvalidArgumentException('Cannot delete system dayPart', 'dayPartId'); + } + if (!$this->getUser()->checkDeleteable($dayPart)) { throw new AccessDeniedException(); } diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index 27ba9c3c06..3490c1c8af 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -377,9 +377,9 @@ function displayManage(Request $request, Response $response, $id) 'sizeRemaining' => round((double)($totalSize - $completeSize) / (pow(1024, $base)), 2), ], 'defaults' => [ - 'fromDate' => Carbon::now()->subSeconds(86400 * 35)->format(DateFormatHelper::getSystemFormat()), - 'fromDateOneDay' => Carbon::now()->subSeconds(86400)->format(DateFormatHelper::getSystemFormat()), - 'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat()) + 'fromDate' => Carbon::now()->startOfMonth()->format(DateFormatHelper::getSystemFormat()), + 'fromDateOneDay' => Carbon::now()->subDay()->format(DateFormatHelper::getSystemFormat()), + 'toDate' => Carbon::now()->endOfMonth()->format(DateFormatHelper::getSystemFormat()) ] ]); diff --git a/lib/Controller/DisplayGroup.php b/lib/Controller/DisplayGroup.php index f54271fd22..bb0ed61aab 100644 --- a/lib/Controller/DisplayGroup.php +++ b/lib/Controller/DisplayGroup.php @@ -1900,8 +1900,11 @@ public function changeLayout(Request $request, Response $response, $id) // in this case we should build it and notify before we send the action // notify should NOT collect now, as we will do that during our own action. $layout = $this->layoutFactory->concurrentRequestLock($layout); - $layout->xlfToDisk(['notify' => true, 'collectNow' => false]); - $this->layoutFactory->concurrentRequestRelease($layout); + try { + $layout->xlfToDisk(['notify' => true, 'collectNow' => false]); + } finally { + $this->layoutFactory->concurrentRequestRelease($layout); + } } } @@ -2092,8 +2095,11 @@ public function overlayLayout(Request $request, Response $response, $id) // in this case we should build it and notify before we send the action // notify should NOT collect now, as we will do that during our own action. $layout = $this->layoutFactory->concurrentRequestLock($layout); - $layout->xlfToDisk(['notify' => true, 'collectNow' => false]); - $this->layoutFactory->concurrentRequestRelease($layout); + try { + $layout->xlfToDisk(['notify' => true, 'collectNow' => false]); + } finally { + $this->layoutFactory->concurrentRequestRelease($layout); + } } } diff --git a/lib/Controller/DisplayProfile.php b/lib/Controller/DisplayProfile.php index 79c61c44a8..33bf12456f 100644 --- a/lib/Controller/DisplayProfile.php +++ b/lib/Controller/DisplayProfile.php @@ -622,11 +622,26 @@ public function copy(Request $request, Response $response, $id) // Create a form out of the config object. $displayProfile = $this->displayProfileFactory->getById($id); - if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId) + if ($this->getUser()->userTypeId != 1 && $this->getUser()->userId != $displayProfile->userId) { throw new AccessDeniedException(__('You do not have permission to delete this profile')); + } + // clear DisplayProfileId, commands and set isDefault to 0 $new = clone $displayProfile; $new->name = $this->getSanitizer($request->getParams())->getString('name'); + + foreach ($displayProfile->commands as $command) { + /* @var \Xibo\Entity\Command $command */ + if (!empty($command->commandStringDisplayProfile)) { + // if the original Display Profile has a commandString + // assign this command with the same commandString to new Display Profile + // commands with only default commandString are not directly assigned to Display profile + $command->commandString = $command->commandStringDisplayProfile; + $command->validationString = $command->validationStringDisplayProfile; + $new->assignCommand($command); + } + } + $new->save(); // Return @@ -639,4 +654,4 @@ public function copy(Request $request, Response $response, $id) return $this->render($request, $response); } -} \ No newline at end of file +} diff --git a/lib/Controller/Layout.php b/lib/Controller/Layout.php index 008f01554e..1241c03b23 100644 --- a/lib/Controller/Layout.php +++ b/lib/Controller/Layout.php @@ -1,6 +1,6 @@ layoutFactory->getByParentId($id); } - // Work out our resolution - if ($layout->schemaVersion < 2) - $resolution = $this->resolutionFactory->getByDesignerDimensions($layout->width, $layout->height); - else - $resolution = $this->resolutionFactory->getByDimensions($layout->width, $layout->height); + // Work out our resolution, if it does not exist, create it. + try { + if ($layout->schemaVersion < 2) { + $resolution = $this->resolutionFactory->getByDesignerDimensions($layout->width, $layout->height); + } else { + $resolution = $this->resolutionFactory->getByDimensions($layout->width, $layout->height); + } + } catch (NotFoundException $notFoundException) { + $this->getLog()->info('Layout Designer with an unknown resolution, we will create it with name: ' . $layout->width . ' x ' . $layout->height); + + $resolution = $this->resolutionFactory->create($layout->width . ' x ' . $layout->height, (int)$layout->width, (int)$layout->height); + $resolution->userId = $this->userFactory->getSystemUser()->userId; + $resolution->save(); + } $moduleFactory = $this->moduleFactory; $isTemplate = $layout->hasTag('template'); @@ -367,7 +376,6 @@ function add(Request $request, Response $response) if ($templateId != 0) { // Load the template $template = $this->layoutFactory->loadById($templateId); - $template->load(); // Empty all of the ID's $layout = clone $template; @@ -383,13 +391,7 @@ function add(Request $request, Response $response) } // Set the owner - $layout->setOwner($this->getUser()->userId); - - // Ensure we have Playlists for each region - foreach ($layout->regions as $region) { - // Set the ownership of this region to the user creating from template - $region->setOwner($this->getUser()->userId, true); - } + $layout->setOwner($this->getUser()->userId, true); } else { $layout = $this->layoutFactory->createFromResolution( $resolutionId, @@ -419,12 +421,13 @@ function add(Request $request, Response $response) $layout->setOriginals(); } - foreach ($layout->regions as $region) { + $allRegions = array_merge($layout->regions, $layout->drawers); + foreach ($allRegions as $region) { /* @var Region $region */ if ($templateId != null && $template !== null) { // Match our original region id to the id in the parent layout - $original = $template->getRegion($region->getOriginalValue('regionId')); + $original = $template->getRegionOrDrawer($region->getOriginalValue('regionId')); // Make sure Playlist closure table from the published one are copied over $original->getPlaylist()->cloneClosureTable($region->getPlaylist()->playlistId); @@ -2188,8 +2191,13 @@ public function status(Request $request, Response $response, $id) { // Get the layout $layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($id)); - $layout = $this->layoutFactory->decorateLockedProperties($layout); - $layout->xlfToDisk(); + try { + $layout = $this->layoutFactory->decorateLockedProperties($layout); + $layout->xlfToDisk(); + } finally { + // Release lock + $this->layoutFactory->concurrentRequestRelease($layout); + } switch ($layout->status) { @@ -2233,9 +2241,6 @@ public function status(Request $request, Response $response, $id) $this->session->refreshExpiry = false; } - // Release lock - $this->layoutFactory->concurrentRequestRelease($layout); - return $this->render($request, $response); } @@ -2627,52 +2632,54 @@ public function publishForm(Request $request, Response $response, $id) public function publish(Request $request, Response $response, $id) { Profiler::start('Layout::publish', $this->getLog()); - $layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($id)); - $sanitizedParams = $this->getSanitizer($request->getParams()); - $publishDate = $sanitizedParams->getDate('publishDate'); - $publishNow = $sanitizedParams->getCheckbox('publishNow'); - - // Make sure we have permission - if (!$this->getUser()->checkEditable($layout)) { - throw new AccessDeniedException(__('You do not have permissions to edit this layout')); - } - - // if we have publish date update it in database - if (isset($publishDate) && !$publishNow) { - $layout->setPublishedDate($publishDate); - } + $layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($id), true); + try { + $sanitizedParams = $this->getSanitizer($request->getParams()); + $publishDate = $sanitizedParams->getDate('publishDate'); + $publishNow = $sanitizedParams->getCheckbox('publishNow'); + + // Make sure we have permission + if (!$this->getUser()->checkEditable($layout)) { + throw new AccessDeniedException(__('You do not have permissions to edit this layout')); + } - // We want to take the draft layout, and update the campaign links to point to the draft, then remove the - // parent. - if ($publishNow || (isset($publishDate) && $publishDate->format('U') < Carbon::now()->format('U')) ) { - $draft = $this->layoutFactory->getByParentId($id); - $draft->publishDraft(); - $draft->load(); + // if we have publish date update it in database + if (isset($publishDate) && !$publishNow) { + $layout->setPublishedDate($publishDate); + } - // We also build the XLF at this point, and if we have a problem we prevent publishing and raise as an - // error message - $draft->xlfToDisk(['notify' => true, 'exceptionOnError' => true, 'exceptionOnEmptyRegion' => false]); + // We want to take the draft layout, and update the campaign links to point to the draft, then remove the + // parent. + if ($publishNow || (isset($publishDate) && $publishDate->format('U') < Carbon::now()->format('U'))) { + $draft = $this->layoutFactory->getByParentId($id); + $draft->publishDraft(); + $draft->load(); + + // We also build the XLF at this point, and if we have a problem we prevent publishing and raise as an + // error message + $draft->xlfToDisk(['notify' => true, 'exceptionOnError' => true, 'exceptionOnEmptyRegion' => false]); + + // Return + $this->getState()->hydrate([ + 'httpStatus' => 200, + 'message' => sprintf(__('Published %s'), $draft->layout), + 'data' => $draft + ]); + } else { + // Return + $this->getState()->hydrate([ + 'httpStatus' => 200, + 'message' => sprintf(__('Layout will be published on %s'), $publishDate), + 'data' => $layout + ]); + } - // Return - $this->getState()->hydrate([ - 'httpStatus' => 200, - 'message' => sprintf(__('Published %s'), $draft->layout), - 'data' => $draft - ]); - } else { - // Return - $this->getState()->hydrate([ - 'httpStatus' => 200, - 'message' => sprintf(__('Layout will be published on %s'), $publishDate), - 'data' => $layout - ]); + Profiler::end('Layout::publish', $this->getLog()); + } finally { + // Release lock + $this->layoutFactory->concurrentRequestRelease($layout, true); } - Profiler::end('Layout::publish', $this->getLog()); - - // Release lock - $this->layoutFactory->concurrentRequestRelease($layout); - return $this->render($request, $response); } @@ -2779,7 +2786,10 @@ public function discard(Request $request, Response $response, $id) public function getLayoutCodes(Request $request, Response $response) { $parsedParams = $this->getSanitizer($request->getQueryParams()); - $codes = $this->layoutFactory->getLayoutCodes($this->gridRenderFilter([], $parsedParams)); + + $codes = $this->layoutFactory->getLayoutCodes($this->gridRenderFilter([ + 'code' => $parsedParams->getString('code') + ], $parsedParams)); // Store the table rows $this->getState()->template = 'grid'; diff --git a/lib/Controller/Library.php b/lib/Controller/Library.php index 2f62315be1..e6552516c8 100644 --- a/lib/Controller/Library.php +++ b/lib/Controller/Library.php @@ -25,6 +25,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Psr7\Stream; +use Intervention\Image\ImageManagerStatic as Img; use Mimey\MimeTypes; use Respect\Validation\Validator as v; use Slim\Http\Response as Response; @@ -2559,7 +2560,7 @@ public function uploadFromUrl(Request $request, Response $response) } // Validate the URL - if (!v::url()->notEmpty()->validate(urldecode($url)) || !filter_var($url, FILTER_VALIDATE_URL)) { + if (!v::url()->notEmpty()->validate($url) || !filter_var($url, FILTER_VALIDATE_URL)) { throw new InvalidArgumentException(__('Provided URL is invalid'), 'url'); } @@ -2571,7 +2572,8 @@ public function uploadFromUrl(Request $request, Response $response) } // remote file size - $size = $this->getRemoteFileSize($url); + $downloadInfo = $this->getDownloadInfo($url); + $size = $downloadInfo['size']; if (ByteFormatter::toBytes(Environment::getMaxUploadSize()) < $size) { throw new InvalidArgumentException(sprintf(__('This file size exceeds your environment Max Upload Size %s'), Environment::getMaxUploadSize()), 'size'); @@ -2583,7 +2585,7 @@ public function uploadFromUrl(Request $request, Response $response) if (!empty($extension)) { $ext = $extension; } else { - $ext = $this->getRemoteFileExtension($url); + $ext = $downloadInfo['extension']; } // check if we have type provided in the request (available via API), if not get the module type from the extension @@ -2630,16 +2632,15 @@ public function uploadFromUrl(Request $request, Response $response) return $this->render($request, $response); } - /** - * @param $url - * @return int - * @throws InvalidArgumentException - */ - private function getRemoteFileSize($url) + private function getDownloadInfo($url) { - $size = -1; + $downloadInfo = []; $guzzle = new Client($this->getConfig()->getGuzzleProxy()); + // first try to get the extension from pathinfo + $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); + $size = -1; + try { $head = $guzzle->head($url); $contentLength = $head->getHeader('Content-Length'); @@ -2648,52 +2649,28 @@ private function getRemoteFileSize($url) $size = $value; } - } catch (RequestException $e) { - $this->getLog()->debug('Upload from url failed for URL ' . $url . ' with following message ' . $e->getMessage()); - throw new InvalidArgumentException(('File not found'), 'url'); - } - - if ($size <= 0) { - throw new InvalidArgumentException(('Cannot determine the file size'), 'size'); - } + if ($extension == '') { + $contentType = $head->getHeaderLine('Content-Type'); - return (int)$size; - } + $extension = $contentType; - /** - * @param $url - * @return string - * @throws InvalidArgumentException - */ - private function getRemoteFileExtension($url) - { - // first try to get the extension from pathinfo - $extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); - - // failing that get the extension from Content-Type header via Guzzle - if ($extension == '') { - $guzzle = new Client($this->getConfig()->getGuzzleProxy()); - $head = $guzzle->head($url); - $contentType = $head->getHeaderLine('Content-Type'); - - $extension = $contentType; - - if ($contentType === 'binary/octet-stream' && $head->hasHeader('x-amz-meta-filetype')) { - $amazonContentType = $head->getHeaderLine('x-amz-meta-filetype'); - $extension = $amazonContentType; - } - - // get the extension corresponding to the mime type - $mimeTypes = new MimeTypes(); - $extension = $mimeTypes->getExtension($extension); - } + if ($contentType === 'binary/octet-stream' && $head->hasHeader('x-amz-meta-filetype')) { + $amazonContentType = $head->getHeaderLine('x-amz-meta-filetype'); + $extension = $amazonContentType; + } - // if we could not determine the file extension at this point, throw an error - if ($extension == '') { - throw new InvalidArgumentException(('Cannot determine the file extension'), 'extension'); + // get the extension corresponding to the mime type + $mimeTypes = new MimeTypes(); + $extension = $mimeTypes->getExtension($extension); + } + } catch (RequestException $e) { + $this->getLog()->debug('Upload from url head request failed for URL ' . $url . ' with following message ' . $e->getMessage()); } - return $extension; + $downloadInfo['size'] = $size; + $downloadInfo['extension'] = $extension; + + return $downloadInfo; } /** @@ -2704,6 +2681,8 @@ private function getRemoteFileExtension($url) * @param Response $response * * @return Response + * @throws AccessDeniedException + * @throws ConfigurationException * @throws InvalidArgumentException * @throws NotFoundException */ @@ -2711,30 +2690,27 @@ public function addThumbnail($request, $response) { $sanitizedParams = $this->getSanitizer($request->getParams()); $libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION'); + self::ensureLibraryExists($libraryLocation); - $image = $request->getParam('image'); + $imageData = $request->getParam('image'); $mediaId = $sanitizedParams->getInt('mediaId'); $media = $this->mediaFactory->getById($mediaId); - if (preg_match('/^data:image\/(\w+);base64,/', $image, $type)) { - $image = substr($image, strpos($image, ',') + 1); - $type = strtolower($type[1]); + if (!$this->getUser()->checkEditable($media)) { + throw new AccessDeniedException(); + } - if (!in_array($type, [ 'jpg', 'jpeg', 'gif', 'png' ])) { - throw new InvalidArgumentException(__('Provided base64 encoded image has incorrect file extension.')); - } - $image = str_replace( ' ', '+', $image ); - $image = base64_decode($image); + try { + Img::configure(['driver' => 'gd']); - if ($image === false) { - throw new InvalidArgumentException(__("Image decoding failed.")); - } - } else { - throw new InvalidArgumentException(__('Incorrect image data')); + // Load the image + $image = Img::make($imageData); + $image->save($libraryLocation . $mediaId . '_' . $media->mediaType . 'cover.png'); + } catch (\Exception $exception) { + $this->getLog()->error('Exception adding Video cover image. e = ' . $exception->getMessage()); + throw new InvalidArgumentException(__('Invalid image data')); } - file_put_contents($libraryLocation . "{$mediaId}_{$media->mediaType}cover.{$type}", $image); - return $response->withStatus(204); } diff --git a/lib/Controller/Preview.php b/lib/Controller/Preview.php index 7410d6dd53..042084700a 100644 --- a/lib/Controller/Preview.php +++ b/lib/Controller/Preview.php @@ -120,21 +120,21 @@ public function show(Request $request, Response $response, $id ) public function getXlf(Request $request, Response $response, $id) { $layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($id)); + try { + if (!$this->getUser()->checkViewable($layout)) { + throw new AccessDeniedException(); + } - if (!$this->getUser()->checkViewable($layout)) { - throw new AccessDeniedException(); - } - - echo file_get_contents($layout->xlfToDisk([ - 'notify' => false, - 'collectNow' => false, - ])); - - $this->setNoOutput(true); - - // Release lock - $this->layoutFactory->concurrentRequestRelease($layout); + echo file_get_contents($layout->xlfToDisk([ + 'notify' => false, + 'collectNow' => false, + ])); + $this->setNoOutput(); + } finally { + // Release lock + $this->layoutFactory->concurrentRequestRelease($layout); + } return $this->render($request, $response); } } diff --git a/lib/Controller/Schedule.php b/lib/Controller/Schedule.php index 25fbeb4404..a1b7db8fee 100644 --- a/lib/Controller/Schedule.php +++ b/lib/Controller/Schedule.php @@ -357,6 +357,10 @@ function eventData(Request $request, Response $response) $row->campaign = __('Private Item'); } $title = __('%s scheduled on %s', $row->campaign, $displayGroupList); + + if ($row->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) { + $title .= __(' with Share of Voice %d seconds per hour', $row->shareOfVoice); + } } // Day diff from start date to end date diff --git a/lib/Controller/Settings.php b/lib/Controller/Settings.php index 8e34f3ae3e..9c9a121926 100644 --- a/lib/Controller/Settings.php +++ b/lib/Controller/Settings.php @@ -757,6 +757,12 @@ public function update(Request $request, Response $response) private function handleChangedSettings($setting, $oldValue, $newValue, &$changedSettings) { if ($oldValue != $newValue) { + + if ($setting === 'SYSTEM_USER') { + $oldSystemUser = $this->userFactory->getById($oldValue); + $oldSystemUser->adjustSystemObjects($newValue); + } + if ($setting === 'ELEVATE_LOG_UNTIL') { $changedSettings[$setting] = Carbon::createFromTimestamp($oldValue)->format(DateFormatHelper::getSystemFormat()) . ' > ' . Carbon::createFromTimestamp($newValue)->format(DateFormatHelper::getSystemFormat()); } else { diff --git a/lib/Controller/User.php b/lib/Controller/User.php index b10204f8f8..e43431f9c0 100644 --- a/lib/Controller/User.php +++ b/lib/Controller/User.php @@ -1,6 +1,6 @@ getUser()->isSuperAdmin() && $user->userId != $this->getConfig()->getSetting('SYSTEM_USER') + && $this->getUser()->userId !== $user->userId ) { // Delete $user->buttons[] = [ @@ -1030,6 +1031,7 @@ public function edit(Request $request, Response $response, $id) public function delete(Request $request, Response $response, $id) { $user = $this->userFactory->getById($id); + $sanitizedParams = $this->getSanitizer($request->getParams()); // System User if ($user->userId == $this->getConfig()->getSetting('SYSTEM_USER')) { @@ -1040,7 +1042,14 @@ public function delete(Request $request, Response $response, $id) throw new AccessDeniedException(); } - $sanitizedParams = $this->getSanitizer($request->getParams()); + if ($this->getUser()->userId === $user->userId) { + throw new InvalidArgumentException(__('Cannot delete your own User from the CMS.')); + } + + if ($sanitizedParams->getCheckbox('deleteAllItems') && $user->isSuperAdmin()) { + throw new InvalidArgumentException(__('Cannot delete all items owned by a Super Admin, please reassign to a different User.')); + } + $user->setChildAclDependencies($this->userGroupFactory); $user->setChildObjectDependencies($this->campaignFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory, $this->displayFactory, $this->displayGroupFactory, $this->widgetFactory, $this->playerVersionFactory, $this->playlistFactory, $this->dataSetFactory, $this->dayPartFactory); diff --git a/lib/Entity/Application.php b/lib/Entity/Application.php index 1b9ab5cec3..a30000343b 100644 --- a/lib/Entity/Application.php +++ b/lib/Entity/Application.php @@ -26,6 +26,7 @@ use Xibo\Factory\ApplicationRedirectUriFactory; use Xibo\Factory\ApplicationScopeFactory; use Xibo\Helper\Random; +use Xibo\OAuth\ScopeEntity; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; @@ -186,7 +187,7 @@ public function unassignScope($scope) { * @var ApplicationScope $a * @var ApplicationScope $b */ - return $a->getId() - $b->getId(); + return $a->getId() !== $b->getId(); }); } @@ -369,6 +370,20 @@ public function getRedirectUri() } } + /** + * @return \League\OAuth2\Server\Entities\ScopeEntityInterface[] + */ + public function getScopes() + { + $scopes = []; + foreach ($this->scopes as $applicationScope) { + $scope = new ScopeEntity(); + $scope->setIdentifier($applicationScope->getId()); + $scopes[] = $scope; + } + return $scopes; + } + /** @inheritDoc */ public function isConfidential() { diff --git a/lib/Entity/ApplicationScope.php b/lib/Entity/ApplicationScope.php index eb07603104..f8dc7e76be 100644 --- a/lib/Entity/ApplicationScope.php +++ b/lib/Entity/ApplicationScope.php @@ -22,7 +22,6 @@ namespace Xibo\Entity; -use League\OAuth2\Server\Entities\ScopeEntityInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; use Xibo\Support\Exception\AccessDeniedException; @@ -31,7 +30,7 @@ * Class ApplicationScope * @package Xibo\Entity */ -class ApplicationScope implements \JsonSerializable, ScopeEntityInterface +class ApplicationScope implements \JsonSerializable { use EntityTrait; @@ -87,15 +86,4 @@ public function checkRoute($method, $route) if (count($route) <= 0) throw new AccessDeniedException(__('Access to this route is denied for this scope')); } - - /** @inheritDoc */ - public function getIdentifier() :string - { - return $this->getId(); - } - - public function getDescription() :string - { - return $this->description; - } -} \ No newline at end of file +} diff --git a/lib/Entity/DayPart.php b/lib/Entity/DayPart.php index a84ed164d9..49fb207555 100644 --- a/lib/Entity/DayPart.php +++ b/lib/Entity/DayPart.php @@ -1,6 +1,6 @@ isAlways || $this->isCustom); + } + /** * @return int */ @@ -213,7 +218,8 @@ public function load() public function save($options = []) { $options = array_merge([ - 'validate' => true + 'validate' => true, + 'recalculateHash' => true ], $options); if ($options['validate']) @@ -225,11 +231,17 @@ public function save($options = []) // Update $this->update(); - // Compare the time hash with a new time hash to see if we need to update associated schedules - if ($this->timeHash != $this->calculateTimeHash()) - $this->handleEffectedSchedules(); - else - $this->getLog()->debug('Daypart hash identical, no need to update schedules. ' . $this->timeHash . ' vs ' . $this->calculateTimeHash()); + // When we change user on reassignAllTo, we do save dayPart, + // however it will not have required childObjectDependencies to run the below checks + // it is also not needed to run them when we just changed the owner. + if ($options['recalculateHash']) { + // Compare the time hash with a new time hash to see if we need to update associated schedules + if ($this->timeHash != $this->calculateTimeHash()) { + $this->handleEffectedSchedules(); + } else { + $this->getLog()->debug('Daypart hash identical, no need to update schedules. ' . $this->timeHash . ' vs ' . $this->calculateTimeHash()); + } + } } } @@ -238,6 +250,10 @@ public function save($options = []) */ public function delete() { + if ($this->isSystemDayPart()) { + throw new InvalidArgumentException('Cannot delete system dayParts'); + } + // Delete all events using this daypart $schedules = $this->scheduleFactory->getByDayPartId($this->dayPartId); diff --git a/lib/Entity/DisplayProfile.php b/lib/Entity/DisplayProfile.php index 7a6be9a16e..8eee31de82 100644 --- a/lib/Entity/DisplayProfile.php +++ b/lib/Entity/DisplayProfile.php @@ -1,6 +1,6 @@ displayProfileId = null; + $this->commands = []; $this->isDefault = 0; } @@ -442,6 +443,10 @@ public function delete() throw new InvalidArgumentException(__('This Display Profile is currently assigned to one or more Displays'), 'displayProfileId'); } + if ($this->isDefault === 1) { + throw new InvalidArgumentException(__('Cannot delete default Display Profile.'), 'isDefault'); + } + $this->getStore()->update('DELETE FROM `displayprofile` WHERE displayprofileid = :displayProfileId', ['displayProfileId' => $this->displayProfileId]); } diff --git a/lib/Entity/Layout.php b/lib/Entity/Layout.php index a70de6a178..da105219dc 100644 --- a/lib/Entity/Layout.php +++ b/lib/Entity/Layout.php @@ -302,6 +302,9 @@ class Layout implements \JsonSerializable // Handle empty regions private $hasEmptyRegion = false; + // Flag to indicate we've not built this layout this session. + private $hasBuilt = false; + public static $loadOptionsMinimum = [ 'loadPlaylists' => false, 'loadTags' => false, @@ -1921,6 +1924,24 @@ public function toZip($dataSetFactory, $fileName, $options = []) $zip->close(); } + /** + * Is a build of this layout required? + * @return bool + */ + public function isBuildRequired(): bool + { + return $this->status == 3 || !file_exists($this->getCachePath()); + } + + /** + * Has this Layout built this session? + * @return bool + */ + public function hasBuilt(): bool + { + return $this->hasBuilt; + } + /** * Save the XLF to disk if necessary * @param array $options @@ -2024,8 +2045,11 @@ public function xlfToDisk($options = []) 'notify' => $options['notify'], 'collectNow' => $options['collectNow'] ]); + + $this->hasBuilt = true; } else { $this->getLog()->debug('xlfToDisk: no build required for layoutId: ' . $this->layoutId); + $this->hasBuilt = false; } Profiler::end('Layout::xlfToDisk', $this->getLog()); diff --git a/lib/Entity/User.php b/lib/Entity/User.php index 00d7a3739a..d21fe983e0 100644 --- a/lib/Entity/User.php +++ b/lib/Entity/User.php @@ -1,6 +1,6 @@ campaigns = $this->campaignFactory->getByOwnerId($this->userId); $this->layouts = $this->layoutFactory->getByOwnerId($this->userId); - $this->media = $this->mediaFactory->getByOwnerId($this->userId); + $this->media = $this->mediaFactory->getByOwnerId($this->userId, 1); $this->events = $this->scheduleFactory->getByOwnerId($this->userId); $this->playlists = $this->playlistFactory->getByOwnerId($this->userId); $this->displayGroups = $this->displayGroupFactory->getByOwnerId($this->userId, -1); @@ -740,9 +740,12 @@ public function load($all = false) public function countChildren() { $this->load(true); + $displayProfiles = intval($this->getStore()->select('SELECT COUNT(*) AS cnt FROM `displayprofile` WHERE userId = :userId', ['userId' => $this->userId])[0]['cnt']); + $commands = intval($this->getStore()->select('SELECT COUNT(*) AS cnt FROM `command` WHERE userId = :userId', ['userId' => $this->userId])[0]['cnt']); + $savedReports = intval($this->getStore()->select('SELECT COUNT(*) AS cnt FROM `saved_report` WHERE userId = :userId', ['userId' => $this->userId])[0]['cnt']); - $count = count($this->campaigns) + count($this->layouts) + count($this->media) + count($this->events) + count($this->playlists) + count($this->displayGroups) + count($this->dayParts); - $this->getLog()->debug('Counted Children on %d, there are %d', $this->userId, $count); + $count = count($this->campaigns) + count($this->layouts) + count($this->media) + count($this->events) + count($this->playlists) + count($this->displayGroups) + count($this->dayParts) + $displayProfiles + $commands + $savedReports; + $this->getLog()->debug(sprintf('Counted Children on %d, there are %d', $this->userId, $count)); return $count; } @@ -755,17 +758,22 @@ public function countChildren() */ public function reassignAllTo($user) { - $this->getLog()->debug('Reassign all to %s', $user->userName); + $this->getLog()->debug(sprintf('Reassign all to %s', $user->userName)); $this->load(true); - $this->getLog()->debug('There are %d children', $this->countChildren()); + $this->getLog()->debug(sprintf('There are %d children', $this->countChildren())); + + // find System User, this is not necessarily the user we were asked to reassignAllTo + $systemUser = $this->userFactory->getSystemUser(); // Reassign media - $this->getStore()->update('UPDATE `media` SET userId = :userId WHERE userId = :oldUserId', [ - 'userId' => $user->userId, - 'oldUserId' => $this->userId - ]); + foreach ($this->media as $media) { + /* @var Media $media */ + ($media->mediaType === 'module') ? $media->setOwner($systemUser->userId) : $media->setOwner($user->getOwnerId()); + + $media->save(); + } // Reassign events $this->getStore()->update('UPDATE `schedule` SET userId = :userId WHERE userId = :oldUserId', [ @@ -804,10 +812,10 @@ public function reassignAllTo($user) ]); // Reassign display groups - $this->getStore()->update('UPDATE `displaygroup` SET userId = :userId WHERE userId = :oldUserId', [ - 'userId' => $user->userId, - 'oldUserId' => $this->userId - ]); + foreach ($this->displayGroups as $displayGroup) { + ($displayGroup->isDisplaySpecific === 1) ? $displayGroup->setOwner($systemUser->userId) : $displayGroup->setOwner($user->getOwnerId()); + $displayGroup->save(['saveTags' => false, 'manageDynamicDisplayLinks' => false]); + } // Reassign display profiles $this->getStore()->update('UPDATE `displayprofile` SET userId = :userId WHERE userId = :oldUserId', [ @@ -821,20 +829,37 @@ public function reassignAllTo($user) 'oldUserId' => $this->userId ]); + foreach ($this->dayParts as $dayPart) { + ($dayPart->isSystemDayPart()) ? $dayPart->setOwner($systemUser->userId) : $dayPart->setOwner($user->userId); + $dayPart->save(['validate' => false, 'recalculateHash' => false]); + } + // Reassign resolutions $this->getStore()->update('UPDATE `resolution` SET userId = :userId WHERE userId = :oldUserId', [ 'userId' => $user->userId, 'oldUserId' => $this->userId ]); - // Reassign Dayparts - $this->getStore()->update('UPDATE `daypart` SET userId = :userId WHERE userId = :oldUserId', [ + // Reassign Saved Reports + $this->getStore()->update('UPDATE `saved_report` SET userId = :userId WHERE userId = :oldUserId', [ 'userId' => $user->userId, 'oldUserId' => $this->userId ]); - // Reassign saved_resports - $this->getStore()->update('UPDATE `saved_report` SET userId = :userId WHERE userId = :oldUserId', [ + // Reassign Report Schedule + $this->getStore()->update('UPDATE `reportschedule` SET userId = :userId WHERE userId = :oldUserId', [ + 'userId' => $user->userId, + 'oldUserId' => $this->userId + ]); + + // Reassign Display Profiles + $this->getStore()->update('UPDATE `displayprofile` SET userId = :userId WHERE userId = :oldUserId', [ + 'userId' => $user->userId, + 'oldUserId' => $this->userId + ]); + + // Reassign Commands + $this->getStore()->update('UPDATE `command` SET userId = :userId WHERE userId = :oldUserId', [ 'userId' => $user->userId, 'oldUserId' => $this->userId ]); @@ -845,6 +870,12 @@ public function reassignAllTo($user) 'oldUserId' => $this->userId ]); + // Reassign Notifications + $this->getStore()->update('UPDATE `notification` SET userId = :userId WHERE userId = :oldUserId', [ + 'userId' => $user->userId, + 'oldUserId' => $this->userId + ]); + // Delete oAuth Clients - security concern $this->getStore()->update('DELETE FROM `oauth_clients` WHERE userId = :userId', ['userId' => $this->userId]); @@ -852,7 +883,7 @@ public function reassignAllTo($user) $this->loaded = false; $this->load(true); - $this->getLog()->debug('Reassign and reload complete, there are %d children', $this->countChildren()); + $this->getLog()->debug(sprintf('Reassign and reload complete, there are %d children', $this->countChildren())); } /** @@ -950,13 +981,16 @@ public function save($options = []) */ public function delete() { - $this->getLog()->debug('Deleting %d', $this->userId); + $this->getLog()->debug(sprintf('Deleting %d', $this->userId)); // We must ensure everything is loaded before we delete if ($this->hash == null) { $this->load(true); } + // get system user + $systemUser = $this->userFactory->getSystemUser(); + // Remove the user specific group $group = $this->userGroupFactory->getById($this->groupId); $group->delete(); @@ -993,11 +1027,23 @@ public function delete() $event->delete(); } + // this needs to happen before we delete media, otherwise we will get an error. + $this->getStore()->update('DELETE FROM `saved_report` WHERE userId = :userId', ['userId' => $this->userId]); + $this->getStore()->update('DELETE FROM `reportschedule` WHERE userId = :userId', ['userId' => $this->userId]); + // Delete any media foreach ($this->media as $media) { /* @var Media $media */ - $media->setChildObjectDependencies($this->layoutFactory, $this->widgetFactory, $this->displayGroupFactory, $this->displayFactory, $this->scheduleFactory, $this->playerVersionFactory); - $media->delete(); + if ($media->mediaType === 'module') { + // Modules files should be owned by a super admin, + // if we are deleting super admin, we would expect these to be reassigned already + // if that is not the case, due to changed userTypeId reassign them now instead of deleting. + $media->setOwner($systemUser->userId); + $media->save(); + } else { + $media->setChildObjectDependencies($this->layoutFactory, $this->widgetFactory, $this->displayGroupFactory, $this->displayFactory, $this->scheduleFactory, $this->playerVersionFactory); + $media->delete(); + } } // Delete Playlists owned by this user @@ -1007,22 +1053,52 @@ public function delete() } // Display Groups owned by this user - foreach($this->displayGroupFactory->getByOwnerId($this->userId) as $displayGroup) { - $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory); - $displayGroup->delete(); + foreach ($this->displayGroups as $displayGroup) { + // Display specific Display Groups should be owned by a super admin, + // if we are deleting super admin, we would expect these to be reassigned already, + // if that is not the case, due to changed userTypeId reassign them now instead of deleting. + if ($displayGroup->isDisplaySpecific === 1) { + $displayGroup->setOwner($systemUser->userId); + $displayGroup->save(['saveTags' => false, 'manageDynamicDisplayLinks' => false]); + } else { + $displayGroup->setChildObjectDependencies($this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory); + $displayGroup->delete(); + } } - foreach($this->dataSetFactory->getByOwnerId($this->userId) as $dataSet) { + foreach ($this->dataSetFactory->getByOwnerId($this->userId) as $dataSet) { $dataSet->delete(); } + // Delete Actions $this->getStore()->update('DELETE FROM `action` WHERE ownerId = :userId', ['userId' => $this->userId]); // Delete oAuth clients $this->getStore()->update('DELETE FROM `oauth_clients` WHERE userId = :userId', ['userId' => $this->userId]); + + foreach ($this->dayParts as $dayPart) { + // System DayParts should be owned by a super admin, + // if we are deleting super admin, we would expect these to be reassigned already + // if that is not the case, due to changed userTypeId reassign them now instead of deleting. + if ($dayPart->isSystemDayPart()) { + $dayPart->setOwner($systemUser->userId); + $dayPart->save(['validate' => false, 'recalculateHash' => false]); + } else { + $dayPart->setChildObjectDependencies($this->displayGroupFactory, $this->displayFactory, $this->layoutFactory, $this->mediaFactory, $this->scheduleFactory, $this->dayPartFactory); + $dayPart->delete(); + } + } + // Delete user specific entities $this->getStore()->update('DELETE FROM `resolution` WHERE userId = :userId', ['userId' => $this->userId]); - $this->getStore()->update('DELETE FROM `daypart` WHERE userId = :userId', ['userId' => $this->userId]); + $this->getStore()->update('DELETE FROM `command` WHERE userId = :userId', ['userId' => $this->userId]); + // if there are any default Display Profiles owned by this user, reassign them instead of deleting. + $this->getStore()->update('UPDATE `displayprofile` SET userId = :userId WHERE userId = :oldUserId AND isDefault = 1', [ + 'userId' => $systemUser->userId, + 'oldUserId' => $this->userId + ]); + $this->getStore()->update('DELETE FROM `displayprofile` WHERE userId = :userId', ['userId' => $this->userId]); + $this->getStore()->update('DELETE FROM `notification` WHERE userId = :userId', ['userId' => $this->userId]); $this->getStore()->update('DELETE FROM `session` WHERE userId = :userId', ['userId' => $this->userId]); $this->getStore()->update('DELETE FROM `user` WHERE userId = :userId', ['userId' => $this->userId]); } @@ -1603,4 +1679,31 @@ public function updateRecoveryCodes($recoveryCodes) $this->getStore()->update($sql, $params); } + + /** + * Triggered from Settings Controller when the SYSTEM_USER is changed + * following this change, we want to reassign some system objects to the new SYSTEM_USER + * + * @param int $newSystemUserId + */ + public function adjustSystemObjects(int $newSystemUserId) + { + // Reassign Module files + $this->getStore()->update('UPDATE `media` SET userId = :userId WHERE userId = :oldUserId AND type = \'module\'', [ + 'userId' => $newSystemUserId, + 'oldUserId' => $this->userId + ]); + + // Reassign Display specific Display Groups + $this->getStore()->update('UPDATE `displaygroup` SET userId = :userId WHERE userId = :oldUserId AND isDisplaySpecific = 1', [ + 'userId' => $newSystemUserId, + 'oldUserId' => $this->userId + ]); + + // Reassign system dayparts + $this->getStore()->update('UPDATE `daypart` SET userId = :userId WHERE userId = :oldUserId AND (isCustom = 1 OR isAlways = 1)', [ + 'userId' => $newSystemUserId, + 'oldUserId' => $this->userId + ]); + } } diff --git a/lib/Factory/ApplicationFactory.php b/lib/Factory/ApplicationFactory.php index 669093870d..e528333ee0 100644 --- a/lib/Factory/ApplicationFactory.php +++ b/lib/Factory/ApplicationFactory.php @@ -150,7 +150,8 @@ public function query($sortOrder = null, $filterBy = []) `user`.UserName AS owner, `oauth_clients`.authCode, `oauth_clients`.clientCredentials, - `oauth_clients`.userId '; + `oauth_clients`.userId, + `oauth_clients`.isConfidential'; $body = ' FROM `oauth_clients` '; $body .= ' INNER JOIN `user` ON `user`.userId = `oauth_clients`.userId '; @@ -173,8 +174,9 @@ public function query($sortOrder = null, $filterBy = []) // Sorting? $order = ''; - if (is_array($sortOrder)) + if (is_array($sortOrder)) { $order .= 'ORDER BY ' . implode(',', $sortOrder); + } $limit = ''; // Paging @@ -265,4 +267,4 @@ public function validateClient($clientIdentifier, $clientSecret, $grantType) return true; } -} \ No newline at end of file +} diff --git a/lib/Factory/ApplicationScopeFactory.php b/lib/Factory/ApplicationScopeFactory.php index c2861fd5fa..8ca1194bc7 100644 --- a/lib/Factory/ApplicationScopeFactory.php +++ b/lib/Factory/ApplicationScopeFactory.php @@ -136,7 +136,10 @@ public function getScopeEntityByIdentifier($scopeIdentifier) $this->getLog()->debug('getScopeEntityByIdentifier: ' . $scopeIdentifier); try { - return $this->getById($scopeIdentifier); + $applicationScope = $this->getById($scopeIdentifier); + $scope = new ScopeEntity(); + $scope->setIdentifier($applicationScope->getId()); + return $scope; } catch (NotFoundException $e) { return null; } @@ -158,7 +161,7 @@ public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface $found = false; /** @var \Xibo\Entity\Application $clientEntity */ - foreach ($clientEntity->scopes as $validScope) { + foreach ($clientEntity->getScopes() as $validScope) { if ($validScope->getIdentifier() === $scope->getIdentifier()) { $found = true; break; @@ -172,4 +175,4 @@ public function finalizeScopes(array $scopes, $grantType, ClientEntityInterface return $finalScopes; } -} \ No newline at end of file +} diff --git a/lib/Factory/LayoutFactory.php b/lib/Factory/LayoutFactory.php index 9a4f607c52..7771556b20 100644 --- a/lib/Factory/LayoutFactory.php +++ b/lib/Factory/LayoutFactory.php @@ -1164,7 +1164,7 @@ public function createFromZip($zipFile, $layoutName, $userId, $template, $replac } catch (NotFoundException $notFoundException) { $this->getLog()->info('Import is for an unknown resolution, we will create it with name: ' . $layout->width . ' x ' . $layout->height); - $resolution = $this->resolutionFactory->create($layout->width . ' x ' . $layout->height, $layout->width, $layout->height); + $resolution = $this->resolutionFactory->create($layout->width . ' x ' . $layout->height, (int)$layout->width, (int)$layout->height); $resolution->userId = $userId; $resolution->save(); } @@ -1767,12 +1767,20 @@ public function createNestedPlaylistWidgets($widgets, $combined, &$playlists) * @param array $filterBy * @return array */ - public function getLayoutCodes($filterBy = []) + public function getLayoutCodes($filterBy = []): array { $parsedFilter = $this->getSanitizer($filterBy); $params = []; $select = 'SELECT DISTINCT code '; - $body = ' FROM layout WHERE code IS NOT NULL ORDER BY code'; + $body = ' FROM layout WHERE code IS NOT NULL '; + + // get by Code + if ($parsedFilter->getString('code') != '') { + $body.= ' AND layout.code LIKE :code '; + $params['code'] = '%' . $parsedFilter->getString('code') . '%'; + } + + $order = ' ORDER BY code'; // Paging $limit = ''; @@ -1780,8 +1788,8 @@ public function getLayoutCodes($filterBy = []) $limit = ' LIMIT ' . intval($parsedFilter->getInt('start'), 0) . ', ' . $parsedFilter->getInt('length', ['default' => 10]); } - $sql = $select . $body . $limit; - $entries = $this->getStore()->select($sql, []); + $sql = $select . $body . $order . $limit; + $entries = $this->getStore()->select($sql, $params); // Paging if ($limit != '' && count($entries) > 0) { @@ -1973,7 +1981,7 @@ public function query($sortOrder = null, $filterBy = []) $this->nameFilter('layout', 'layout', $terms, $body, $params, ($parsedFilter->getCheckbox('useRegexForName') == 1)); } - if ($parsedFilter->getString('layoutExact', $filterBy) != '') { + if ($parsedFilter->getString('layoutExact') != '') { $body.= " AND layout.layout = :exact "; $params['exact'] = $parsedFilter->getString('layoutExact'); } @@ -2071,12 +2079,12 @@ public function query($sortOrder = null, $filterBy = []) } // get by Code - if ($parsedFilter->getString('code', $filterBy) != '') { + if ($parsedFilter->getString('code') != '') { $body.= " AND layout.code = :code "; $params['code'] = $parsedFilter->getString('code'); } - if ($parsedFilter->getString('codeLike', $filterBy) != '') { + if ($parsedFilter->getString('codeLike') != '') { $body.= ' AND layout.code LIKE :codeLike '; $params['codeLike'] = '%' . $parsedFilter->getString('codeLike') . '%'; } @@ -2528,8 +2536,13 @@ public function decorateLockedProperties(Layout $layout): Layout * @param int $tries * @throws \Xibo\Support\Exception\GeneralException */ - public function concurrentRequestLock(Layout $layout, $pass = 1, $ttl = 300, $wait = 6, $tries = 10): Layout + public function concurrentRequestLock(Layout $layout, $force = false, $pass = 1, $ttl = 300, $wait = 6, $tries = 10): Layout { + // Does this layout require building? + if (!$force && !$layout->isBuildRequired()) { + return $layout; + } + $lock = $this->getPool()->getItem('locks/layout_build/' . $layout->campaignId); // Set the invalidation method to simply return the value (not that we use it, but it gets us a miss on expiry) @@ -2576,7 +2589,7 @@ public function concurrentRequestLock(Layout $layout, $pass = 1, $ttl = 300, $wa // Recursive request (we've decremented the number of tries) $pass++; - return $this->concurrentRequestLock($layout, $pass, $ttl, $wait, $tries); + return $this->concurrentRequestLock($layout, $force, $pass, $ttl, $wait, $tries); } } } @@ -2584,8 +2597,12 @@ public function concurrentRequestLock(Layout $layout, $pass = 1, $ttl = 300, $wa /** * Release a lock on concurrent requests */ - public function concurrentRequestRelease(Layout $layout) + public function concurrentRequestRelease(Layout $layout, bool $force = false) { + if (!$force && !$layout->hasBuilt()) { + return; + } + $this->getLog()->debug('Releasing lock ' . $layout->campaignId); $lock = $this->getPool()->getItem('locks/layout_build/' . $layout->campaignId); diff --git a/lib/Factory/MediaFactory.php b/lib/Factory/MediaFactory.php index ed1cf6b6a8..907621e08f 100644 --- a/lib/Factory/MediaFactory.php +++ b/lib/Factory/MediaFactory.php @@ -1,6 +1,6 @@ query(null, array('disableUserCheck' => 1, 'ownerId' => $ownerId, 'isEdited' => 1)); + return $this->query(null, ['disableUserCheck' => 1, 'ownerId' => $ownerId, 'isEdited' => 1, 'allModules' => $allModules]); } /** diff --git a/lib/Factory/NotificationFactory.php b/lib/Factory/NotificationFactory.php index d18cacfad2..2debe255f4 100644 --- a/lib/Factory/NotificationFactory.php +++ b/lib/Factory/NotificationFactory.php @@ -189,6 +189,11 @@ public function query($sortOrder = null, $filterBy = []) $params['createToDt'] = $sanitizedFilter->getInt('createToDt'); } + if ($sanitizedFilter->getInt('onlyReleased') === 1) { + $body .= ' AND `notification`.releaseDt <= :now '; + $params['now'] = Carbon::now()->format('U'); + } + // User Id? if ($sanitizedFilter->getInt('userId') !== null) { $body .= ' AND `notification`.notificationId IN ( diff --git a/lib/Factory/PermissionFactory.php b/lib/Factory/PermissionFactory.php index b76edc89b1..557c3bca8a 100644 --- a/lib/Factory/PermissionFactory.php +++ b/lib/Factory/PermissionFactory.php @@ -128,7 +128,7 @@ public function createForEveryone($userGroupFactory, $entity, $objectId, $view, * Get Permissions by Entity ObjectId * @param string $entity * @param int $objectId - * @return array[Permission] + * @return Permission[] */ public function getByObjectId($entity, $objectId) { @@ -169,14 +169,14 @@ public function getByObjectId($entity, $objectId) * @param int $objectId * @param array[string] $sortOrder * @param array[mixed] $filterBy - * @return array[Permission] + * @return Permission[] * @throws NotFoundException */ public function getAllByObjectId($user, $entity, $objectId, $sortOrder = null, $filterBy = []) { // Look up the entityId for any add operation that might occur $entityId = $this->getStore()->select('SELECT entityId FROM permissionentity WHERE entity = :entity', ['entity' => $entity]); - + $sanitizedFilter = $this->getSanitizer($filterBy); if (count($entityId) <= 0) { @@ -320,7 +320,7 @@ public function getAllByObjectId($user, $entity, $objectId, $sortOrder = null, $ * Gets all permissions for a user group * @param string $entity * @param int $groupId - * @return array[Permission] + * @return Permission[] */ public function getByGroupId($entity, $groupId) { @@ -361,28 +361,48 @@ public function getByGroupId($entity, $groupId) * Gets all permissions for a set of user groups * @param string $entity * @param int $userId - * @return array[Permission] + * @return Permission[] */ public function getByUserId($entity, $userId) { - $permissions = array(); + $permissions = []; $sql = ' - SELECT `permission`.`permissionId`, `permission`.`groupId`, `permission`.`objectId`, `permission`.`view`, `permission`.`edit`, `permission`.`delete`, permissionentity.entityId + SELECT `permission`.`permissionId`, + `permission`.`groupId`, + `permission`.`objectId`, + `permission`.`view`, + `permission`.`edit`, + `permission`.`delete`, + `permissionentity`.entityId FROM `permission` INNER JOIN `permissionentity` ON `permissionentity`.entityId = permission.entityId INNER JOIN `group` ON `group`.groupId = `permission`.groupId - LEFT OUTER JOIN `lkusergroup` + INNER JOIN `lkusergroup` ON `lkusergroup`.groupId = `group`.groupId - LEFT OUTER JOIN `user` + INNER JOIN `user` ON lkusergroup.UserID = `user`.UserID - AND `user`.userId = :userId - WHERE entity = :entity - AND (`user`.userId IS NOT NULL OR `group`.IsEveryone = 1) + WHERE `permissionentity`.entity = :entity + AND `user`.userId = :userId + UNION + SELECT `permission`.`permissionId`, + `permission`.`groupId`, + `permission`.`objectId`, + `permission`.`view`, + `permission`.`edit`, + `permission`.`delete`, + `permissionentity`.entityId + FROM `permission` + INNER JOIN `permissionentity` + ON `permissionentity`.entityId = permission.entityId + INNER JOIN `group` + ON `group`.groupId = `permission`.groupId + WHERE `permissionentity`.entity = :entity + AND `group`.IsEveryone = 1 '; - $params = array('entity' => $entity, 'userId' => $userId); + $params = ['entity' => $entity, 'userId' => $userId]; foreach ($this->getStore()->select($sql, $params) as $row) { $permission = $this->createEmpty(); @@ -414,4 +434,4 @@ public function getFullPermissions() $permission->modifyPermissions = 1; return $permission; } -} \ No newline at end of file +} diff --git a/lib/Factory/UserFactory.php b/lib/Factory/UserFactory.php index 8c960a4275..ba510791c4 100644 --- a/lib/Factory/UserFactory.php +++ b/lib/Factory/UserFactory.php @@ -1,6 +1,6 @@ query(null, array('disableUserCheck' => 1, 'userTypeId' => 1)); + return $this->query(['userId'], ['disableUserCheck' => 1, 'userTypeId' => 1]); } /** @@ -198,9 +198,7 @@ public function getDoohUsers() */ public function getSystemUser() { - $user = $this->getById($this->configService->getSetting('SYSTEM_USER')); - - return $user; + return $this->getById($this->configService->getSetting('SYSTEM_USER')); } /** diff --git a/lib/Factory/UserGroupFactory.php b/lib/Factory/UserGroupFactory.php index 2b95ca828a..05704c3522 100644 --- a/lib/Factory/UserGroupFactory.php +++ b/lib/Factory/UserGroupFactory.php @@ -615,11 +615,6 @@ public function getFeatures() 'group' => 'users-management', 'title' => __('Page which shows all User Groups that have been created') ], - 'usergroup.add' => [ - 'feature' => 'usergroup.add', - 'group' => 'users-management', - 'title' => __('Include "Add User Group" button to allow for additional User Groups to be added') - ], 'usergroup.modify' => [ 'feature' => 'usergroup.modify', 'group' => 'users-management', diff --git a/lib/Helper/Environment.php b/lib/Helper/Environment.php index caaff7b4e0..5a2601b69b 100644 --- a/lib/Helper/Environment.php +++ b/lib/Helper/Environment.php @@ -29,7 +29,7 @@ */ class Environment { - public static $WEBSITE_VERSION_NAME = '3.0.1'; + public static $WEBSITE_VERSION_NAME = '3.0.2'; public static $XMDS_VERSION = '6'; public static $XLF_VERSION = 3; public static $VERSION_REQUIRED = '7.2.9'; diff --git a/lib/Middleware/ApiAuthentication.php b/lib/Middleware/ApiAuthentication.php index ba7bcc5226..f277ef315a 100644 --- a/lib/Middleware/ApiAuthentication.php +++ b/lib/Middleware/ApiAuthentication.php @@ -22,12 +22,14 @@ namespace Xibo\Middleware; use League\OAuth2\Server\Grant\AuthCodeGrant; +use League\OAuth2\Server\Grant\RefreshTokenGrant; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Psr\Http\Server\MiddlewareInterface as Middleware; use Psr\Http\Server\RequestHandlerInterface as RequestHandler; use Slim\App as App; +use Xibo\OAuth\RefreshTokenRepository; use Xibo\Support\Exception\ConfigurationException; /** @@ -99,6 +101,8 @@ public function process(Request $request, RequestHandler $handler): Response new \DateInterval('PT1H') ); + $server->enableGrantType(new RefreshTokenGrant(new RefreshTokenRepository())); + return $server; } catch (\LogicException $exception) { $logger->error($exception->getMessage()); diff --git a/lib/Middleware/ApiAuthorization.php b/lib/Middleware/ApiAuthorization.php index 2c34e473cb..e8c8470ebc 100644 --- a/lib/Middleware/ApiAuthorization.php +++ b/lib/Middleware/ApiAuthorization.php @@ -126,14 +126,19 @@ public function process(Request $request, RequestHandler $handler): Response if (is_array($scopes) && count($scopes) > 0) { foreach ($scopes as $scope) { // Valid routes - if ($scope->id != 'all') { - $logger->debug(sprintf('Test authentication for %s %s against scope %s', - $resource, $request->getMethod(), $scope->id)); + if ($scope !== 'all') { + $logger->debug( + sprintf( + 'Test authentication for %s %s against scope %s', + $resource, + $request->getMethod(), + $scope + ) + ); // Check the route and request method try { - $applicationScopeFactory->getById($scope->id)->checkRoute($request->getMethod(), - $resource); + $applicationScopeFactory->getById($scope)->checkRoute($request->getMethod(), $resource); } catch (NotFoundException $notFoundException) { throw new AccessDeniedException(); } diff --git a/lib/Middleware/Handlers.php b/lib/Middleware/Handlers.php index 545469ae9d..2d1c26fa7e 100644 --- a/lib/Middleware/Handlers.php +++ b/lib/Middleware/Handlers.php @@ -23,6 +23,7 @@ namespace Xibo\Middleware; use Illuminate\Support\Str; +use League\OAuth2\Server\Exception\OAuthServerException; use Nyholm\Psr7\Factory\Psr17Factory; use Slim\Exception\HttpNotFoundException; use Slim\Exception\HttpSpecializedException; @@ -62,7 +63,7 @@ public static function jsonErrorHandler($container) /** @var Response $response */ $response = $decoratedResponseFactory->createResponse(500); - if ($exception instanceof GeneralException) { + if ($exception instanceof GeneralException || $exception instanceof OAuthServerException) { return $exception->generateHttpResponse($response); } else if ($exception instanceof HttpSpecializedException) { return $response->withJson([ @@ -252,6 +253,11 @@ private static function writeLog($request, bool $logErrors, bool $logErrorDetail if ($logErrorDetails) { $logger->debug($exception->getTraceAsString()); + + $previous = $exception->getPrevious(); + if ($previous !== null) { + $logger->debug(get_class($previous) . ': ' . $previous->getMessage()); + } } } } diff --git a/lib/Middleware/SAMLAuthentication.php b/lib/Middleware/SAMLAuthentication.php index b413d91a06..870f436d1a 100644 --- a/lib/Middleware/SAMLAuthentication.php +++ b/lib/Middleware/SAMLAuthentication.php @@ -85,17 +85,12 @@ public function addRoutes() $app->post('/saml/acs', function (\Slim\Http\ServerRequest $request, \Slim\Http\Response $response) { // Log some interesting things $this->getLog()->debug('Arrived at the ACS route with own URL: ' . Utils::getSelfRoutedURLNoQuery()); - $parsedRequest = $this->getSanitizer($request->getParsedBody()); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); // Pull out the SAML settings $samlSettings = $this->getConfig()->samlSettings; $auth = new Auth($samlSettings); $auth->processResponse(); - $priorRoute = ($parsedRequest->getString('priorRoute')); - $redirect = ($priorRoute == '' || $priorRoute == '/' || stripos($priorRoute, $routeParser->urlFor('login'))) ? $routeParser->urlFor('home') : $priorRoute; - // Check for errors $errors = $auth->getErrors(); @@ -275,7 +270,10 @@ public function addRoutes() ]); } - // Redirect to User Homepage + // Redirect back to the originally-requested url + $params = $request->getParams(); + $redirect = $params['RelayState'] ?? $this->getRouteParser()->urlFor('home'); + return $response->withRedirect($redirect); } }); diff --git a/lib/Middleware/State.php b/lib/Middleware/State.php index 7acc5651de..0a65ec0a8d 100644 --- a/lib/Middleware/State.php +++ b/lib/Middleware/State.php @@ -194,6 +194,9 @@ public static function setState(App $app, Request $request): Request // Setup the translations for gettext Translate::InitLocale($container->get('configService')); + // Set Carbon locale + Carbon::setLocale(Translate::GetLocale(2)); + // Default timezone date_default_timezone_set($container->get('configService')->getSetting("defaultTimezone")); diff --git a/lib/OAuth/ScopeEntity.php b/lib/OAuth/ScopeEntity.php new file mode 100644 index 0000000000..056bd0227e --- /dev/null +++ b/lib/OAuth/ScopeEntity.php @@ -0,0 +1,37 @@ +. + */ + +namespace Xibo\OAuth; + +use League\OAuth2\Server\Entities\ScopeEntityInterface; +use League\OAuth2\Server\Entities\Traits\EntityTrait; +use League\OAuth2\Server\Entities\Traits\ScopeTrait; + +/** + * Class ScopeEntity + * @package Xibo\OAuth + */ +class ScopeEntity implements ScopeEntityInterface +{ + use ScopeTrait; + use EntityTrait; +} diff --git a/lib/Widget/NotificationView.php b/lib/Widget/NotificationView.php index 1c47aed664..6278b9dc6a 100644 --- a/lib/Widget/NotificationView.php +++ b/lib/Widget/NotificationView.php @@ -222,11 +222,13 @@ private function getNotifications($isPreview, $displayId = null) if ($isPreview) $notifications = $this->getNotificationFactory()->query(['releaseDt DESC', 'createDt DESC', 'subject'], [ 'releaseDt' => ($age === 0) ? null : Carbon::now()->subMinutes($age)->format('U'), + 'onlyReleased' => 1, 'userId' => $this->getUser()->userId ]); else $notifications = $this->getNotificationFactory()->query(['releaseDt DESC', 'createDt DESC', 'subject'], [ 'releaseDt' => ($age === 0) ? null : Carbon::now()->subMinutes($age)->format('U'), + 'onlyReleased' => 1, 'displayId' => $displayId ]); @@ -254,7 +256,7 @@ private function getNotifications($isPreview, $displayId = null) break; case '[Date]': - $replace = Carbon::createFromTimestamp($notification->releaseDt)->format($dateFormat); + $replace = Carbon::createFromTimestamp($notification->releaseDt)->translatedFormat($dateFormat); break; } @@ -358,6 +360,7 @@ public function getModifiedDate($displayId) $notifications = $this->getNotificationFactory()->query(['releaseDt DESC', 'createDt DESC'], [ 'releaseDt' => ($age === 0) ? null : Carbon::now()->subMinutes($age)->format('U'), 'displayId' => $displayId, + 'onlyReleased' => 1, 'length' => 1 ]); diff --git a/lib/Widget/Ticker.php b/lib/Widget/Ticker.php index 128bb6c24d..a21fbb0178 100644 --- a/lib/Widget/Ticker.php +++ b/lib/Widget/Ticker.php @@ -831,8 +831,7 @@ private function getRssItems($text) break; case '[Date]': - $replace = Carbon::createFromTimestamp($item->getDate()->format('U'))->format($dateFormat); - + $replace = Carbon::createFromTimestamp($item->getDate()->format('U'))->translatedFormat($dateFormat); break; case '[PermaLink]': diff --git a/lib/XTR/MaintenanceRegularTask.php b/lib/XTR/MaintenanceRegularTask.php index 16f25a6bd3..be9a32fe75 100644 --- a/lib/XTR/MaintenanceRegularTask.php +++ b/lib/XTR/MaintenanceRegularTask.php @@ -246,12 +246,15 @@ private function buildLayouts() /* @var \Xibo\Entity\Layout $layout */ try { $layout = $this->layoutFactory->concurrentRequestLock($layout); - $layout->xlfToDisk(['notify' => true]); - - // Commit after each build - // https://github.com/xibosignage/xibo/issues/1593 - $this->store->commitIfNecessary(); - $this->layoutFactory->concurrentRequestRelease($layout); + try { + $layout->xlfToDisk(['notify' => true]); + + // Commit after each build + // https://github.com/xibosignage/xibo/issues/1593 + $this->store->commitIfNecessary(); + } finally { + $this->layoutFactory->concurrentRequestRelease($layout); + } } catch (\Exception $e) { $this->log->error('Maintenance cannot build Layout %d, %s.', $layout->layoutId, $e->getMessage()); } @@ -413,13 +416,19 @@ private function publishLayouts() throw new GeneralException(__($layout->statusMessage)); } else { // publish the layout - $layout = $this->layoutFactory->concurrentRequestLock($layout); - $draft = $this->layoutFactory->getByParentId($layout->layoutId); - $draft->publishDraft(); - $draft->load(); - $draft->xlfToDisk(['notify' => true, 'exceptionOnError' => true, 'exceptionOnEmptyRegion' => false]); - - $this->layoutFactory->concurrentRequestRelease($layout); + $layout = $this->layoutFactory->concurrentRequestLock($layout, true); + try { + $draft = $this->layoutFactory->getByParentId($layout->layoutId); + $draft->publishDraft(); + $draft->load(); + $draft->xlfToDisk([ + 'notify' => true, + 'exceptionOnError' => true, + 'exceptionOnEmptyRegion' => false + ]); + } finally { + $this->layoutFactory->concurrentRequestRelease($layout, true); + } $this->log->info('Published layout ID ' . $layout->layoutId . ' new layout id is ' . $draft->layoutId); } } catch (GeneralException $e) { diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php index 20d1471ca0..d3473a8a35 100644 --- a/lib/Xmds/Soap.php +++ b/lib/Xmds/Soap.php @@ -617,12 +617,14 @@ protected function doRequiredFiles($serverKey, $hardwareKey, $httpDownloads) // Load this layout $layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->loadById($layoutId)); - $layout->loadPlaylists(); - - // Make sure its XLF is up to date - $path = $layout->xlfToDisk(['notify' => false]); + try { + $layout->loadPlaylists(); - $this->layoutFactory->concurrentRequestRelease($layout); + // Make sure its XLF is up to date + $path = $layout->xlfToDisk(['notify' => false]); + } finally { + $this->layoutFactory->concurrentRequestRelease($layout); + } // If the status is *still* 4, then we skip this layout as it cannot build if ($layout->status === ModuleWidget::$STATUS_INVALID) { diff --git a/lib/Xmds/Soap3.php b/lib/Xmds/Soap3.php index f45b79436c..e878aba741 100644 --- a/lib/Xmds/Soap3.php +++ b/lib/Xmds/Soap3.php @@ -185,8 +185,11 @@ function GetFile($serverKey, $hardwareKey, $filePath, $fileType, $chunkOffset, $ // Load the layout $layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($fileId)); - $path = $layout->xlfToDisk(); - $this->layoutFactory->concurrentRequestRelease($layout); + try { + $path = $layout->xlfToDisk(); + } finally { + $this->layoutFactory->concurrentRequestRelease($layout); + } $file = file_get_contents($path); $chunkSize = filesize($path); diff --git a/lib/Xmds/Soap4.php b/lib/Xmds/Soap4.php index b00576857b..ef12ff5ff9 100644 --- a/lib/Xmds/Soap4.php +++ b/lib/Xmds/Soap4.php @@ -356,8 +356,11 @@ function GetFile($serverKey, $hardwareKey, $fileId, $fileType, $chunkOffset, $ch // Load the layout $layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($fileId)); - $path = $layout->xlfToDisk(); - $this->layoutFactory->concurrentRequestRelease($layout); + try { + $path = $layout->xlfToDisk(); + } finally { + $this->layoutFactory->concurrentRequestRelease($layout); + } $file = file_get_contents($path); $chunkSize = filesize($path); diff --git a/lib/routes-web.php b/lib/routes-web.php index 96f7e9dcc4..5a02aad301 100644 --- a/lib/routes-web.php +++ b/lib/routes-web.php @@ -469,9 +469,7 @@ ->addMiddleware(new FeatureAuth($app->getContainer(), ['usergroup.view'])) ->setName('group.view'); -$app->get('/group/form/add', ['\Xibo\Controller\UserGroup','addForm']) - ->addMiddleware(new FeatureAuth($app->getContainer(), ['usergroup.add'])) - ->setName('group.add.form'); +$app->get('/group/form/add', ['\Xibo\Controller\UserGroup','addForm'])->setName('group.add.form'); $app->group('', function(\Slim\Routing\RouteCollectorProxy $group) { $group->get('/group/form/edit/{id}', ['\Xibo\Controller\UserGroup','editForm'])->setName('group.edit.form'); diff --git a/lib/routes.php b/lib/routes.php index f1c06a6fa7..de2844471d 100644 --- a/lib/routes.php +++ b/lib/routes.php @@ -536,14 +536,14 @@ $app->post('/user/permissions/{entity}', ['\Xibo\Controller\User','permissionsMulti'])->setName('user.set.permissions.multi'); $app->post('/user', ['\Xibo\Controller\User','add']) - ->addMiddleware(new \Xibo\Middleware\FeatureAuth($app->getContainer(), ['user.add'])) + ->addMiddleware(new \Xibo\Middleware\FeatureAuth($app->getContainer(), ['users.add'])) ->setName('user.add'); $app->group('', function (RouteCollectorProxy $group) { $group->put('/user/{id}', ['\Xibo\Controller\User','edit'])->setName('user.edit'); $group->delete('/user/{id}', ['\Xibo\Controller\User','delete'])->setName('user.delete'); $group->post('/user/{id}/usergroup/assign', ['\Xibo\Controller\User','assignUserGroup'])->setName('user.assign.userGroup'); -})->addMiddleware(new \Xibo\Middleware\FeatureAuth($app->getContainer(), ['user.modify'])); +})->addMiddleware(new \Xibo\Middleware\FeatureAuth($app->getContainer(), ['users.modify'])); /** * User Group @@ -554,9 +554,7 @@ */ $app->get('/group', ['\Xibo\Controller\UserGroup','grid'])->setName('group.search'); -$app->post('/group', ['\Xibo\Controller\UserGroup','add']) - ->addMiddleware(new \Xibo\Middleware\FeatureAuth($app->getContainer(), ['usergroup.add'])) - ->setName('group.add'); +$app->post('/group', ['\Xibo\Controller\UserGroup','add'])->setName('group.add'); $app->group('', function (RouteCollectorProxy $group) { $group->put('/group/{id}', ['\Xibo\Controller\UserGroup','edit'])->setName('group.edit'); diff --git a/locale/default.pot b/locale/default.pot index 02b620f5c3..5cf0c08a18 100644 --- 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: 2021-07-14 16:07+0100\n" +"POT-Creation-Date: 2021-08-02 10:29+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -3772,7 +3772,7 @@ msgstr "" #: cache/77/7706497b9b32c7de36827c213b2a8601711f0af41de2261b354b6b986d1c5049.php:181 #: cache/21/21e7349348636e2320001b4071000dd442f65742ad810e150643b3f9794881cf.php:165 #: cache/21/21e7349348636e2320001b4071000dd442f65742ad810e150643b3f9794881cf.php:294 -#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:283 +#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:292 #: cache/d6/d672ce9ebb470acd8dc2a0cb66e2234b139381f1e3d5dbeb664c11e99bc60f3f.php:188 #: cache/d6/d672ce9ebb470acd8dc2a0cb66e2234b139381f1e3d5dbeb664c11e99bc60f3f.php:319 #: cache/e5/e54ea368150fb0e872396ad12cdd6097e072346898812d5cd64c42b733049904.php:129 @@ -6399,7 +6399,7 @@ msgstr "" #: cache/3a/3a57d968d2b3ea047a6cbc125d3ffeb5cc016ca022840d3b4002b22e1a0c869b.php:56 #: cache/78/78e302e137cf151cd25aa928cf2056f410b12e070db936764c1ca5309902d112.php:73 #: cache/d6/d672ce9ebb470acd8dc2a0cb66e2234b139381f1e3d5dbeb664c11e99bc60f3f.php:96 -#: lib/XTR/MaintenanceRegularTask.php:268 +#: lib/XTR/MaintenanceRegularTask.php:271 msgid "Tidy Library" msgstr "" @@ -8046,7 +8046,7 @@ msgstr "" #: cache/2a/2aee6043c3cda7e0b32dc764721ad7a9402be770bc2456a747cbc713a42ea0b3.php:218 #: cache/63/6375875be8cd52bf7c309054babc8bde8599a27cf31c056351341a85ed859b16.php:265 #: cache/d3/d3273d78dbc6bccc3d7f19def2a4bef341bd1b3a6390ef65db8cadb616cd4a06.php:113 -#: lib/Entity/Layout.php:2013 lib/Controller/Region.php:697 +#: lib/Entity/Layout.php:2034 lib/Controller/Region.php:697 msgid "Empty Region" msgstr "" @@ -10335,7 +10335,7 @@ msgid "An optional description of the Layout. (1 - 250 characters)" msgstr "" #: cache/77/77a605daa65ad15865fc7916d845912dc07f146330a2cab026798adb2856c351.php:53 -#: lib/Middleware/Handlers.php:139 +#: lib/Middleware/Handlers.php:140 msgid "Sorry we could not find that page." msgstr "" @@ -10823,8 +10823,7 @@ msgid "Can this Application keep a secret?" msgstr "" #: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:209 -#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:240 -msgid "Redirect URI" +msgid "New Redirect URI" msgstr "" #: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:214 @@ -10833,11 +10832,19 @@ msgid "" "Authorization Code Grants" msgstr "" -#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:225 +#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:224 +msgid "Existing Redirect URI" +msgstr "" + +#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:248 msgid "Select sharing to grant to this application (scopes)." msgstr "" -#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:288 +#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:258 +msgid "Scopes" +msgstr "" + +#: cache/94/94f9ceecb93c3316d3080d4aae46e60a999d6abef0ccb4af36413e5c7e83eb72.php:297 msgid "" "Set the owner of this Application. If you are not an admin you will not be " "able to reverse this action." @@ -14572,6 +14579,10 @@ msgstr "" msgid "Authorize Request" msgstr "" +#: cache/51/519ed163d0b4033d1f84b24f4c2c3bf7bbd435d55f1432c273a058d772034ef5.php:65 +msgid "would like access to the following scopes" +msgstr "" + #: cache/51/51f7093cefb1adde75d1e7eb69243b2e5257a3697fb9238e024098b0ea0d12e2.php:56 msgid "Run Task Now" msgstr "" @@ -16931,7 +16942,7 @@ msgstr "" msgid "Please select at least 1 Playlist to embed" msgstr "" -#: lib/Widget/SubPlaylist.php:288 lib/Entity/Layout.php:2385 +#: lib/Widget/SubPlaylist.php:288 lib/Entity/Layout.php:2415 msgid "Cannot add the same SubPlaylist twice." msgstr "" @@ -16983,7 +16994,7 @@ msgstr "" msgid "Please enter a display group name" msgstr "" -#: lib/Entity/DisplayGroup.php:672 lib/Entity/Layout.php:1043 +#: lib/Entity/DisplayGroup.php:672 lib/Entity/Layout.php:1046 #: lib/Entity/DataSet.php:730 msgid "Description can not be longer than 254 characters" msgstr "" @@ -17069,7 +17080,7 @@ msgstr "" msgid "Existing data is incompatible with your new configuration" msgstr "" -#: lib/Entity/ApplicationScope.php:88 +#: lib/Entity/ApplicationScope.php:87 msgid "Access to this route is denied for this scope" msgstr "" @@ -17299,7 +17310,7 @@ msgstr "" msgid "The Region dimensions cannot be empty or negative" msgstr "" -#: lib/Entity/Region.php:393 lib/Entity/Layout.php:1062 +#: lib/Entity/Region.php:393 lib/Entity/Layout.php:1065 msgid "Layer must be 0 or a positive number" msgstr "" @@ -17329,57 +17340,57 @@ msgstr "" msgid "Folder needs to have a name, between 1 and 254 characters." msgstr "" -#: lib/Entity/Layout.php:527 +#: lib/Entity/Layout.php:530 msgid "Cannot find region" msgstr "" -#: lib/Entity/Layout.php:545 +#: lib/Entity/Layout.php:548 msgid "Cannot find drawer region" msgstr "" -#: lib/Entity/Layout.php:566 +#: lib/Entity/Layout.php:569 msgid "Cannot find Region or Drawer" msgstr "" -#: lib/Entity/Layout.php:937 +#: lib/Entity/Layout.php:940 msgid "This layout is used as the global default and cannot be deleted" msgstr "" -#: lib/Entity/Layout.php:1034 +#: lib/Entity/Layout.php:1037 msgid "The layout dimensions cannot be empty" msgstr "" -#: lib/Entity/Layout.php:1039 +#: lib/Entity/Layout.php:1042 msgid "Layout Name must be between 1 and 50 characters" msgstr "" -#: lib/Entity/Layout.php:1057 +#: lib/Entity/Layout.php:1060 #, php-format msgid "You already own a Layout called '%s'. Please choose another name." msgstr "" -#: lib/Entity/Layout.php:1068 +#: lib/Entity/Layout.php:1071 msgid "Please use only alphanumeric characters in Layout Code identifier" msgstr "" -#: lib/Entity/Layout.php:1080 +#: lib/Entity/Layout.php:1083 msgid "Layout with provided code already exists" msgstr "" -#: lib/Entity/Layout.php:1744 lib/Controller/Fault.php:122 +#: lib/Entity/Layout.php:1747 lib/Controller/Fault.php:122 msgid "Can't create ZIP. Error Code: " msgstr "" -#: lib/Entity/Layout.php:2005 +#: lib/Entity/Layout.php:2026 #, php-format msgid "There is an error with this Layout: %s" msgstr "" -#: lib/Entity/Layout.php:2052 lib/Entity/Layout.php:2169 +#: lib/Entity/Layout.php:2080 lib/Entity/Layout.php:2199 msgid "Not a Draft" msgstr "" -#: lib/Entity/Layout.php:2254 +#: lib/Entity/Layout.php:2284 msgid "Draft Layouts must have a parent" msgstr "" @@ -17572,7 +17583,7 @@ msgid "Tidy Logs" msgstr "" #: lib/XTR/MaintenanceDailyTask.php:99 lib/XTR/StatsArchiveTask.php:137 -#: lib/XTR/StatsArchiveTask.php:329 lib/Controller/User.php:413 +#: lib/XTR/StatsArchiveTask.php:332 lib/Controller/User.php:413 msgid "Disabled" msgstr "" @@ -17605,7 +17616,7 @@ msgid "Run report results: %s." msgstr "" #: lib/XTR/ReportScheduleTask.php:163 lib/XTR/AuditLogArchiveTask.php:175 -#: lib/XTR/StatsArchiveTask.php:212 lib/Service/ReportService.php:341 +#: lib/XTR/StatsArchiveTask.php:215 lib/Service/ReportService.php:341 #, php-format msgid "Can't create ZIP. Error Code: %s" msgstr "" @@ -17669,13 +17680,13 @@ msgstr "" msgid "AuditLog Export %s to %s" msgstr "" -#: lib/XTR/AuditLogArchiveTask.php:204 lib/XTR/StatsArchiveTask.php:277 +#: lib/XTR/AuditLogArchiveTask.php:204 lib/XTR/StatsArchiveTask.php:280 msgid "" "No super admins to use as the archive owner, please set one in the " "configuration." msgstr "" -#: lib/XTR/AuditLogArchiveTask.php:212 lib/XTR/StatsArchiveTask.php:285 +#: lib/XTR/AuditLogArchiveTask.php:212 lib/XTR/StatsArchiveTask.php:288 msgid "Archive Owner not found" msgstr "" @@ -17731,27 +17742,27 @@ msgstr "" msgid "Build Layouts" msgstr "" -#: lib/XTR/MaintenanceRegularTask.php:297 +#: lib/XTR/MaintenanceRegularTask.php:300 msgid "Library allowance exceeded" msgstr "" -#: lib/XTR/MaintenanceRegularTask.php:346 +#: lib/XTR/MaintenanceRegularTask.php:349 #, php-format msgid "%s is downloading %d files too many times" msgstr "" -#: lib/XTR/MaintenanceRegularTask.php:351 +#: lib/XTR/MaintenanceRegularTask.php:354 #, php-format msgid "" "Please check the bandwidth graphs and display status for %s to investigate " "the issue." msgstr "" -#: lib/XTR/MaintenanceRegularTask.php:378 +#: lib/XTR/MaintenanceRegularTask.php:381 msgid "Playlist Duration Updates" msgstr "" -#: lib/XTR/MaintenanceRegularTask.php:399 +#: lib/XTR/MaintenanceRegularTask.php:402 msgid "Publishing layouts with set publish dates" msgstr "" @@ -17764,16 +17775,16 @@ msgstr "" msgid "Stats Export %s to %s - %s" msgstr "" -#: lib/XTR/StatsArchiveTask.php:297 +#: lib/XTR/StatsArchiveTask.php:300 msgid "Tidy Stats" msgstr "" -#: lib/XTR/StatsArchiveTask.php:323 +#: lib/XTR/StatsArchiveTask.php:326 #, php-format msgid "Done - %d deleted." msgstr "" -#: lib/XTR/StatsArchiveTask.php:326 +#: lib/XTR/StatsArchiveTask.php:329 msgid "Error." msgstr "" @@ -17814,7 +17825,7 @@ msgstr "" msgid "Sorry this account does not exist or cannot be authenticated." msgstr "" -#: lib/Middleware/Handlers.php:148 +#: lib/Middleware/Handlers.php:149 msgid "Unexpected Error, please contact support." msgstr "" @@ -17979,10 +17990,10 @@ msgstr "" msgid "Assign Layouts" msgstr "" -#: lib/Controller/DisplayGroup.php:653 lib/Controller/DisplayGroup.php:2403 +#: lib/Controller/DisplayGroup.php:653 lib/Controller/DisplayGroup.php:2409 #: lib/Controller/Template.php:404 lib/Controller/Help.php:204 -#: lib/Controller/DataSetColumn.php:370 lib/Controller/Applications.php:371 -#: lib/Controller/Applications.php:438 lib/Controller/DisplayProfile.php:308 +#: lib/Controller/DataSetColumn.php:370 lib/Controller/Applications.php:383 +#: lib/Controller/Applications.php:450 lib/Controller/DisplayProfile.php:308 #: lib/Controller/DisplayProfile.php:635 lib/Controller/DayPart.php:418 #: lib/Controller/User.php:709 lib/Controller/Campaign.php:466 #: lib/Controller/Campaign.php:958 lib/Controller/Notification.php:589 @@ -17998,7 +18009,7 @@ msgstr "" #: lib/Controller/DisplayGroup.php:784 lib/Controller/Library.php:1272 #: lib/Controller/Help.php:236 lib/Controller/DataSetColumn.php:547 -#: lib/Controller/Applications.php:542 lib/Controller/DisplayProfile.php:465 +#: lib/Controller/Applications.php:555 lib/Controller/DisplayProfile.php:465 #: lib/Controller/DayPart.php:531 lib/Controller/User.php:971 #: lib/Controller/Campaign.php:636 lib/Controller/Notification.php:719 #: lib/Controller/Playlist.php:812 lib/Controller/Tag.php:521 @@ -18017,7 +18028,7 @@ msgstr "" #: lib/Controller/DisplayGroup.php:835 lib/Controller/Library.php:900 #: lib/Controller/Help.php:261 lib/Controller/DataSetColumn.php:637 -#: lib/Controller/Applications.php:574 lib/Controller/DisplayProfile.php:545 +#: lib/Controller/Applications.php:587 lib/Controller/DisplayProfile.php:545 #: lib/Controller/DayPart.php:642 lib/Controller/User.php:1070 #: lib/Controller/Campaign.php:717 lib/Controller/Notification.php:778 #: lib/Controller/Playlist.php:873 lib/Controller/Tag.php:649 @@ -18118,40 +18129,40 @@ msgid "Layouts unassigned from %s" msgstr "" #: lib/Controller/DisplayGroup.php:1718 lib/Controller/DisplayGroup.php:1767 -#: lib/Controller/DisplayGroup.php:1919 lib/Controller/DisplayGroup.php:1969 -#: lib/Controller/DisplayGroup.php:2109 lib/Controller/DisplayGroup.php:2211 -#: lib/Controller/DisplayGroup.php:2592 +#: lib/Controller/DisplayGroup.php:1922 lib/Controller/DisplayGroup.php:1972 +#: lib/Controller/DisplayGroup.php:2115 lib/Controller/DisplayGroup.php:2217 +#: lib/Controller/DisplayGroup.php:2598 #, php-format msgid "Command Sent to %s" msgstr "" -#: lib/Controller/DisplayGroup.php:1854 lib/Controller/DisplayGroup.php:2049 +#: lib/Controller/DisplayGroup.php:1854 lib/Controller/DisplayGroup.php:2052 msgid "Please provide a Layout ID or Campaign ID" msgstr "" -#: lib/Controller/DisplayGroup.php:1864 lib/Controller/DisplayGroup.php:2059 +#: lib/Controller/DisplayGroup.php:1864 lib/Controller/DisplayGroup.php:2062 msgid "Please provide Layout specific campaign ID" msgstr "" -#: lib/Controller/DisplayGroup.php:1870 lib/Controller/DisplayGroup.php:2065 +#: lib/Controller/DisplayGroup.php:1870 lib/Controller/DisplayGroup.php:2068 msgid "Cannot find layout by campaignId" msgstr "" -#: lib/Controller/DisplayGroup.php:1875 lib/Controller/DisplayGroup.php:2070 +#: lib/Controller/DisplayGroup.php:1875 lib/Controller/DisplayGroup.php:2073 msgid "Please provide Layout id or Campaign id" msgstr "" -#: lib/Controller/DisplayGroup.php:2234 lib/Controller/DisplayProfile.php:537 +#: lib/Controller/DisplayGroup.php:2240 lib/Controller/DisplayProfile.php:537 #: lib/Controller/DisplayProfile.php:567 lib/Controller/DisplayProfile.php:626 msgid "You do not have permission to delete this profile" msgstr "" -#: lib/Controller/DisplayGroup.php:2504 +#: lib/Controller/DisplayGroup.php:2510 #, php-format msgid "Display %s moved to Folder %s" msgstr "" -#: lib/Controller/DisplayGroup.php:2584 +#: lib/Controller/DisplayGroup.php:2590 msgid "Please provide a Trigger Code" msgstr "" @@ -18290,35 +18301,35 @@ msgstr "" msgid "Your library is full. Library Limit: %s MB" msgstr "" -#: lib/Controller/Library.php:2577 +#: lib/Controller/Library.php:2578 #, php-format msgid "This file size exceeds your environment Max Upload Size %s" msgstr "" -#: lib/Controller/Library.php:2609 +#: lib/Controller/Library.php:2610 #, php-format msgid "" "Invalid Module type or extension. Module type %s does not allow for %s " "extension" msgstr "" -#: lib/Controller/Library.php:2625 +#: lib/Controller/Library.php:2626 msgid "Media upload from URL was successful" msgstr "" -#: lib/Controller/Library.php:2724 +#: lib/Controller/Library.php:2700 msgid "Provided base64 encoded image has incorrect file extension." msgstr "" -#: lib/Controller/Library.php:2730 +#: lib/Controller/Library.php:2706 msgid "Image decoding failed." msgstr "" -#: lib/Controller/Library.php:2733 +#: lib/Controller/Library.php:2709 msgid "Incorrect image data" msgstr "" -#: lib/Controller/Library.php:2833 +#: lib/Controller/Library.php:2809 #, php-format msgid "Media %s moved to Folder %s" msgstr "" @@ -18348,23 +18359,23 @@ msgstr "" msgid "Latest news not enabled." msgstr "" -#: lib/Controller/Applications.php:177 lib/Controller/Applications.php:200 +#: lib/Controller/Applications.php:179 lib/Controller/Applications.php:220 msgid "Authorisation Parameters missing from session." msgstr "" -#: lib/Controller/Applications.php:363 lib/Controller/Applications.php:415 +#: lib/Controller/Applications.php:375 lib/Controller/Applications.php:427 msgid "Please enter Application name" msgstr "" -#: lib/Controller/Applications.php:419 +#: lib/Controller/Applications.php:431 msgid "Please select user" msgstr "" -#: lib/Controller/Applications.php:423 +#: lib/Controller/Applications.php:435 msgid "Invalid user type" msgstr "" -#: lib/Controller/Applications.php:532 +#: lib/Controller/Applications.php:545 msgid "You do not have permission to assign this user" msgstr "" @@ -18380,7 +18391,7 @@ msgstr "" #: lib/Controller/Action.php:276 lib/Controller/Action.php:423 #: lib/Controller/Action.php:485 lib/Controller/Action.php:614 #: lib/Controller/Action.php:672 lib/Controller/Action.php:732 -#: lib/Controller/Layout.php:2751 +#: lib/Controller/Layout.php:2755 msgid "Layout is not checked out" msgstr "" @@ -18975,9 +18986,9 @@ msgstr "" #: lib/Controller/Layout.php:805 lib/Controller/Layout.php:910 #: lib/Controller/Layout.php:958 lib/Controller/Layout.php:1008 #: lib/Controller/Layout.php:1077 lib/Controller/Layout.php:1116 -#: lib/Controller/Layout.php:2479 lib/Controller/Layout.php:2530 -#: lib/Controller/Layout.php:2569 lib/Controller/Layout.php:2637 -#: lib/Controller/Layout.php:2696 lib/Controller/Layout.php:2746 +#: lib/Controller/Layout.php:2481 lib/Controller/Layout.php:2532 +#: lib/Controller/Layout.php:2571 lib/Controller/Layout.php:2640 +#: lib/Controller/Layout.php:2700 lib/Controller/Layout.php:2750 msgid "You do not have permissions to edit this layout" msgstr "" @@ -19009,19 +19020,19 @@ msgstr "" msgid "For Layout %s Enable Stats Collection is set to %s" msgstr "" -#: lib/Controller/Layout.php:1424 lib/Controller/Layout.php:2197 +#: lib/Controller/Layout.php:1424 lib/Controller/Layout.php:2202 msgid "This Layout is ready to play" msgstr "" -#: lib/Controller/Layout.php:1428 lib/Controller/Layout.php:2201 +#: lib/Controller/Layout.php:1428 lib/Controller/Layout.php:2206 msgid "There are items on this Layout that can only be assessed by the Display" msgstr "" -#: lib/Controller/Layout.php:1432 lib/Controller/Layout.php:2205 +#: lib/Controller/Layout.php:1432 lib/Controller/Layout.php:2210 msgid "This Layout has not been built yet" msgstr "" -#: lib/Controller/Layout.php:1436 lib/Controller/Layout.php:2209 +#: lib/Controller/Layout.php:1436 lib/Controller/Layout.php:2214 msgid "This Layout is invalid and should not be scheduled" msgstr "" @@ -19067,39 +19078,39 @@ msgid "Cannot copy a Draft Layout" msgstr "" #: lib/Controller/Layout.php:2058 lib/Controller/Layout.php:2134 -#: lib/Controller/Layout.php:2265 lib/Controller/Layout.php:2302 +#: lib/Controller/Layout.php:2267 lib/Controller/Layout.php:2304 msgid "Cannot manage tags on a Draft Layout" msgstr "" -#: lib/Controller/Layout.php:2422 +#: lib/Controller/Layout.php:2424 msgid "Cannot download non-region specific module" msgstr "" -#: lib/Controller/Layout.php:2535 +#: lib/Controller/Layout.php:2537 msgid "Layout is already checked out" msgstr "" -#: lib/Controller/Layout.php:2544 +#: lib/Controller/Layout.php:2546 #, php-format msgid "Checked out %s" msgstr "" -#: lib/Controller/Layout.php:2659 +#: lib/Controller/Layout.php:2662 #, php-format msgid "Published %s" msgstr "" -#: lib/Controller/Layout.php:2666 +#: lib/Controller/Layout.php:2669 #, php-format msgid "Layout will be published on %s" msgstr "" -#: lib/Controller/Layout.php:2763 +#: lib/Controller/Layout.php:2767 #, php-format msgid "Discarded %s" msgstr "" -#: lib/Controller/Layout.php:2806 +#: lib/Controller/Layout.php:2810 msgid "This function is available only to Super Admins." msgstr "" @@ -20512,41 +20523,41 @@ msgstr "" msgid "Failed to write to database after %d retries. Please try again later." msgstr "" -#: lib/Xmds/Soap3.php:221 lib/Xmds/Soap4.php:396 +#: lib/Xmds/Soap3.php:224 lib/Xmds/Soap4.php:399 msgid "Unknown FileType Requested." msgstr "" -#: lib/Xmds/Soap.php:1701 +#: lib/Xmds/Soap.php:1703 msgid "Dates are too far apart" msgstr "" -#: lib/Xmds/Soap.php:2077 +#: lib/Xmds/Soap.php:2079 #, php-format msgid "Recovery for Display %s" msgstr "" -#: lib/Xmds/Soap.php:2078 +#: lib/Xmds/Soap.php:2080 #, php-format msgid "Display ID %d is now back online %s" msgstr "" -#: lib/Xmds/Soap.php:2149 +#: lib/Xmds/Soap.php:2151 msgid "Bandwidth allowance exceeded" msgstr "" -#: lib/Xmds/Soap4.php:376 +#: lib/Xmds/Soap4.php:379 msgid "Media exists but file missing from library. " msgstr "" -#: lib/Xmds/Soap4.php:380 +#: lib/Xmds/Soap4.php:383 msgid "Unable to get file pointer" msgstr "" -#: lib/Xmds/Soap4.php:390 +#: lib/Xmds/Soap4.php:393 msgid "Empty file" msgstr "" -#: lib/Xmds/Soap4.php:733 +#: lib/Xmds/Soap4.php:736 msgid "Incorrect Screen shot Format" msgstr "" diff --git a/modules/weather-form-edit.twig b/modules/weather-form-edit.twig index 181652c26a..ec36284217 100644 --- a/modules/weather-form-edit.twig +++ b/modules/weather-form-edit.twig @@ -286,7 +286,7 @@ - {% if module.getSetting("apiKey") == "" %} + {% if module.getSetting("apiKey") == "" and module.getSetting("owmApiKey") == "" %} {% set message %}{% trans "The Weather Widget has not been configured yet, please ask your CMS Administrator to look at it for you." %}{% endset %} {{ forms.message(message, "alert alert-danger") }} {% endif %} diff --git a/ui/src/core/file-upload.js b/ui/src/core/file-upload.js index 4b2b0f7cae..c6e91825e8 100644 --- a/ui/src/core/file-upload.js +++ b/ui/src/core/file-upload.js @@ -27,14 +27,16 @@ var videoImageCovers = {}; */ function openUploadForm(options) { - options = $.extend({}, { + options = $.extend(true, {}, { templateId: "template-file-upload", - multi: true, videoImageCovers: true, className: "upload-modal", animateDialog: true, formOpenedEvent: null, - layoutImport: false + templateOptions : { + layoutImport: false, + multi: true + } }, options); // Keep a cache of the upload template (unless we are a non-standard form) @@ -84,7 +86,7 @@ function openUploadForm(options) { } // If we are not a multi-upload, then limit to 1 - if (!options.multi) { + if (!options.templateOptions.multi) { uploadOptions = $.extend({}, uploadOptions, { maxNumberOfFiles: 1, limitMultiFileUploads: 1 @@ -301,9 +303,9 @@ function saveVideoCoverImage(data) { delete videoImageCovers[results.name]; // this calls function in library controller that decodes the image and - // saves it to library as "/{$mediaId}_videocover.{$type}". + // saves it to library as "{libraryLocation}/{$mediaId}_{mediaType}cover.png". $.ajax({ - url: "/library/thumbnail", + url: addMediaThumbnailUrl, type: "POST", data: thumbnailData }); diff --git a/ui/src/helpers/form-helpers.js b/ui/src/helpers/form-helpers.js index a5e68be622..8ca0a584c9 100644 --- a/ui/src/helpers/form-helpers.js +++ b/ui/src/helpers/form-helpers.js @@ -897,8 +897,8 @@ let formHelpers = function() { animateDialog: false, initialisedBy: "library-upload", className: self.namespace.getUploadDialogClassName(), - multi: false, templateOptions: { + multi: false, oldMediaId: mediaId, widgetId: widgetId, updateInAllChecked: uploadFormUpdateAllDefault, diff --git a/ui/src/vendor/calendar/js/calendar.js b/ui/src/vendor/calendar/js/calendar.js index 85593caa35..26b84d2c37 100644 --- a/ui/src/vendor/calendar/js/calendar.js +++ b/ui/src/vendor/calendar/js/calendar.js @@ -783,7 +783,8 @@ if(!String.prototype.formatNum) { layoutDisplayOrder: event.displayOrder, eventPriority: event.isPriority, itemClass: elementPriorityClass, - itemIcon: elementPriorityIcon + itemIcon: elementPriorityIcon, + shareOfVoice: event.shareOfVoice }); } } diff --git a/views/applications-authorize-page.twig b/views/applications-authorize-page.twig index 990ed2aabb..e82d4487b4 100644 --- a/views/applications-authorize-page.twig +++ b/views/applications-authorize-page.twig @@ -1,6 +1,6 @@ {# /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -24,23 +24,26 @@ {% import "inline.twig" as inline %} {% block pageContent %} -
+
{% trans "Authorize Request" %}
-

{{ authParams.client.getName() }} would like access

+

{{ authParams.client.getName() }}

+
{{ "would like access to the following scopes"|trans }}:
    - {% for scope in authParams.scopes %} + {% for scope in scopes %}
  • - {{ scope.getIdentifier() }} : {{ scope.getDescription() }} + {{ scope.description }}
  • {% endfor %}
- - - +
+ + + +
diff --git a/views/applications-form-edit.twig b/views/applications-form-edit.twig index 4cc8cbdb02..9b8f6417e2 100644 --- a/views/applications-form-edit.twig +++ b/views/applications-form-edit.twig @@ -1,6 +1,6 @@ {# /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -72,18 +72,25 @@ {% set helpText %}{% trans "Can this Application keep a secret?" %}{% endset %} {{ forms.checkbox("isConfidential", title, client.isConfidential, helpText) }} - {% set title %}{% trans "Redirect URI" %}{% endset %} + {% set title %}{% trans "New Redirect URI" %}{% endset %} {% set helpText %}{% trans "White listed redirect URI's that will be allowed, only application for Authorization Code Grants" %}{% endset %} {{ forms.input("redirectUri[]", title, "", helpText) }} + +
+ {{ "Existing Redirect URI"|trans }}: +
+ + {% for url in client.redirectUris %} + {{ forms.input("redirectUri[]", "", url.redirectUri) }} + {% endfor %}
{% set message %}{% trans "Select sharing to grant to this application (scopes)." %}{% endset %} {{ forms.message(message) }} - {% for url in client.redirectUris %} - {% set title %}{% trans "Redirect URI" %}{% endset %} - {{ forms.input("redirectUri[]", title, url.redirectUri) }} - {% endfor %} +
+ {{ "Scopes"|trans }}: +
{% for scope in scopes %} {% set title %}{{ scope.description }}{% endset %} diff --git a/views/authed-sidebar.twig b/views/authed-sidebar.twig index 0292d304ae..0546d0d6f9 100644 --- a/views/authed-sidebar.twig +++ b/views/authed-sidebar.twig @@ -70,7 +70,7 @@ {% endif %} {% endif %} - {% if currentUser.featureEnabled("user.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %} + {% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %} {% set userMenuViewable = true %} {% else %} {% set userMenuViewable = false %} diff --git a/views/authed-topbar.twig b/views/authed-topbar.twig index 3c7d6f844b..fb1d1ef984 100644 --- a/views/authed-topbar.twig +++ b/views/authed-topbar.twig @@ -126,7 +126,7 @@ {% endif %} {% endif %} - {% if currentUser.featureEnabled("user.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %} + {% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %} {% set userMenuViewable = true %} {% else %} {% set userMenuViewable = false %} diff --git a/views/base.twig b/views/base.twig index e8eecf68e2..931fc22854 100644 --- a/views/base.twig +++ b/views/base.twig @@ -122,6 +122,7 @@ var pingUrl = "{{ url_for("ping") }}"; var foldersUrl = "{{ url_for("folders.search") }}"; var permissionsUrl = "{{ url_for("user.set.permissions.multi", {entity: ":entity"}) }}"; + var addMediaThumbnailUrl = "{{ url_for("library.thumbnail.add") }}"; {% autoescape "js" %} var dataTablesLanguage = { diff --git a/views/dashboard-icon-page.twig b/views/dashboard-icon-page.twig index 40af6676a7..cd68fee88a 100644 --- a/views/dashboard-icon-page.twig +++ b/views/dashboard-icon-page.twig @@ -62,7 +62,7 @@
{% endif %} - {% if currentUser.featureEnabled("user.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %} + {% if currentUser.featureEnabled("users.view") and (currentUser.isGroupAdmin() or currentUser.isSuperAdmin()) %}
diff --git a/views/display-page-manage.twig b/views/display-page-manage.twig index 1a2cfd967a..4ae3257dc4 100644 --- a/views/display-page-manage.twig +++ b/views/display-page-manage.twig @@ -194,8 +194,8 @@
- {{ inline.date("fromDt", "From Date", defaults.fromDate, "", "", "", "") }} - {{ inline.date("toDt", "To Date", defaults.toDate, "", "", "", "") }} + {{ inline.dateMonth("fromDt", "From Date", defaults.fromDate, "", "", "", "") }} + {{ inline.dateMonth("toDt", "To Date", defaults.toDate, "", "", "", "") }} {{ inline.hidden("displayId", display.displayId) }}
diff --git a/views/layout-designer-page.twig b/views/layout-designer-page.twig index 6c0e06a993..2692cc23d3 100644 --- a/views/layout-designer-page.twig +++ b/views/layout-designer-page.twig @@ -224,7 +224,6 @@ openUploadForm({ url: $(e.target).data().addNewBackgroundUrl, title: "{% trans "Add Background Image" %}", - multi: false, videoImageCovers: false, buttons: { main: { @@ -236,6 +235,7 @@ } }, templateOptions: { + multi: false, trans: { addFiles: "{% trans "Browse/Add Image" %}", startUpload: "{% trans "Start Upload" %}", diff --git a/views/layout-page.twig b/views/layout-page.twig index d18a58c2d0..8aadb5b6aa 100644 --- a/views/layout-page.twig +++ b/views/layout-page.twig @@ -312,7 +312,6 @@ openUploadForm({ url: "{{ url_for("layout.import") }}", title: "{{ "Upload Layout"|trans }}", - multi: true, videoImageCovers: false, buttons: { main: { diff --git a/views/library-page.twig b/views/library-page.twig index 7f9564fee0..3fa8b071e9 100644 --- a/views/library-page.twig +++ b/views/library-page.twig @@ -362,7 +362,6 @@ openUploadForm({ url: "{{ url_for("library.add") }}", title: "{% trans "Upload media" %}", - multi: false, buttons: { main: { label: "{% trans "Done" %}", @@ -374,6 +373,7 @@ } }, templateOptions: { + multi: false, oldMediaId: mediaId, oldFolderId: folderId, updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %}, diff --git a/views/media-manager-page.twig b/views/media-manager-page.twig index b164169ba8..2f5c78bf27 100644 --- a/views/media-manager-page.twig +++ b/views/media-manager-page.twig @@ -172,7 +172,6 @@ openUploadForm({ url: "{{ url_for("library.add") }}", title: "{% trans "Upload media" %}", - multi: false, buttons: { main: { label: "{% trans "Done" %}", @@ -184,6 +183,7 @@ } }, templateOptions: { + multi: false, oldMediaId: mediaId, widgetId: widgetId, updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %}, @@ -251,7 +251,6 @@ openUploadForm({ url: "{{ url_for("library.add") }}", title: "{% trans "Upload media" %}", - multi: false, buttons: { main: { label: "{% trans "Done" %}", @@ -263,6 +262,7 @@ } }, templateOptions: { + multi: false, oldMediaId: mediaId, widgetId: widgetId, updateInAllChecked: {% if settings.LIBRARY_MEDIA_UPDATEINALL_CHECKB == 1 %}true{% else %}false{% endif %}, diff --git a/views/notification-page.twig b/views/notification-page.twig index ef28e268f3..3ea3b82ba4 100644 --- a/views/notification-page.twig +++ b/views/notification-page.twig @@ -213,7 +213,6 @@ var upload = openUploadForm({ url: "{{ url_for("notification.addattachment") }}", title: "{% trans "Browse/Add attachment" %}", - multi: false, videoImageCovers: false, animateDialog: false, className: "second-dialog", @@ -227,6 +226,7 @@ } }, templateOptions: { + multi: false, trans: { addFiles: "{% trans "Browse/Add Attachment" %}", startUpload: "{% trans "Start Upload" %}", diff --git a/views/playersoftware-page.twig b/views/playersoftware-page.twig index 2394e3c52f..0c87935013 100644 --- a/views/playersoftware-page.twig +++ b/views/playersoftware-page.twig @@ -146,7 +146,6 @@ openUploadForm({ url: "{{ url_for("library.add") }}", title: "{% trans "Upload Version" %}", - multi: false, videoImageCovers: false, buttons: { main: { @@ -159,6 +158,7 @@ } }, templateOptions: { + multi: false, trans: { addFiles: "{% trans "Add files" %}", startUpload: "{% trans "Start upload" %}", diff --git a/views/schedule-page.twig b/views/schedule-page.twig index f0c19b5704..d55aa77689 100644 --- a/views/schedule-page.twig +++ b/views/schedule-page.twig @@ -660,7 +660,11 @@ + <% if (layouts.type == 4) { %> + + {% verbatim %}<% if (layouts.type == 4) { %>{% endverbatim %} + + {% verbatim %}<% } %>{% endverbatim %} @@ -726,6 +733,9 @@ <% } %> + <% if (layout.shareOfVoice) { %> + + <% } %> <% diff --git a/views/user-form-delete.twig b/views/user-form-delete.twig index 74f3507348..0b18a11c7c 100644 --- a/views/user-form-delete.twig +++ b/views/user-form-delete.twig @@ -1,6 +1,6 @@ {# /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -77,9 +77,11 @@ {% set message %}{% trans "Are you sure you want to delete? You may not be able to delete this user if they have associated content. You can retire users by using the Edit Button." %}{% endset %} {{ forms.message(message) }} - {% set title %}{% trans "Delete all items owned by this User?" %}{% endset %} - {% set helpText %}{% trans "Check to delete all items owned by this user, including Layouts, Media, Schedules, etc." %}{% endset %} - {{ forms.checkbox("deleteAllItems", title, 0, helpText) }} + {% if not user.isSuperAdmin() %} + {% set title %}{% trans "Delete all items owned by this User?" %}{% endset %} + {% set helpText %}{% trans "Check to delete all items owned by this user, including Layouts, Media, Schedules, etc." %}{% endset %} + {{ forms.checkbox("deleteAllItems", title, 0, helpText) }} + {% endif %} {% set title %}{% trans "Reassign items to another User" %}{% endset %} {% set helpText %}{% trans "Reassign all items this User owns to the selected User." %}{% endset %} diff --git a/views/user-form-edit.twig b/views/user-form-edit.twig index cd50cc2564..219a55aea4 100644 --- a/views/user-form-edit.twig +++ b/views/user-form-edit.twig @@ -1,6 +1,6 @@ {# /** - * Copyright (C) 2020 Xibo Signage Ltd + * Copyright (C) 2021 Xibo Signage Ltd * * Xibo - Digital Signage - http://www.xibo.org.uk * @@ -85,9 +85,11 @@ - {% set title %}{% trans "User Type" %}{% endset %} - {% set helpText %}{% trans "What is this users type?" %}{% endset %} - {{ forms.dropdown("userTypeId", "single", title, user.userTypeId, options.userTypes, "userTypeId", "userType", helpText) }} + {% if currentUser.isSuperAdmin() %} + {% set title %}{% trans "User Type" %}{% endset %} + {% set helpText %}{% trans "What is this users type?" %}{% endset %} + {{ forms.dropdown("userTypeId", "single", title, user.userTypeId, options.userTypes, "userTypeId", "userType", helpText) }} + {% endif %} {% set title %}{% trans "Library Quota" %}{% endset %} {% set helpText %}{% trans "The quota that should be applied. Enter 0 for no quota." %}{% endset %} diff --git a/views/user-form-onboarding.twig b/views/user-form-onboarding.twig index da17ccfdad..24cecf3ca1 100644 --- a/views/user-form-onboarding.twig +++ b/views/user-form-onboarding.twig @@ -43,10 +43,12 @@ 2

{{ "Credentials"|trans }}

+ {% if currentUser.featureEnabled("folder.view") %}
3

{{ "Sharing"|trans }}

+ {% endif %} @@ -90,7 +92,7 @@ - diff --git a/views/user-page.twig b/views/user-page.twig index 121c552a7b..9b4eef1d83 100644 --- a/views/user-page.twig +++ b/views/user-page.twig @@ -282,7 +282,9 @@ $(dialog).find('[data-toggle="popover"]').popover(); // Init the folder panel + {% if currentUser.featureEnabled("folder.view") %} initFolderPanel(dialog); + {% endif %} var navListItems = $(dialog).find('div.setup-panel div a'), allWells = $(dialog).find('.setup-content'), @@ -344,6 +346,7 @@ XiboFormSubmit($form, e, function(xhr) { // Callback if (xhr.success && xhr.id) { + {% if currentUser.featureEnabled("folder.view") %} // Submit the folder ownerships var selected = $(dialog).find("#container-folder-tree").jstree("get_selected"); var groupIds = {}; @@ -361,6 +364,7 @@ toastr.error("{{ "Problem saving folder sharing, please check the User created." }}"); } }); + {% endif %} XiboDialogClose(); } diff --git a/views/usergroup-form-edit.twig b/views/usergroup-form-edit.twig index feaef1fd77..2f2b391675 100644 --- a/views/usergroup-form-edit.twig +++ b/views/usergroup-form-edit.twig @@ -42,7 +42,7 @@
diff --git a/web/api/authorize/index.php b/web/api/authorize/index.php index 010a70c8a5..52fac6ee5c 100644 --- a/web/api/authorize/index.php +++ b/web/api/authorize/index.php @@ -82,12 +82,10 @@ /** @var \League\OAuth2\Server\AuthorizationServer $server */ $server = $app->getContainer()->get('server'); $authRequest = $server->validateAuthorizationRequest($request); - $app->getContainer()->get('session')->set('authParams', $authRequest); + // Redirect the user to the UI - save the auth params in the session. - //$app->getContainer()->get('session')->set('authParams', $authParams); - //$app->redirect(str_replace('/api/authorize/', '/application/authorize', $app->request()->getPath())); - // We know we are at /api/authorize, so convert that to /application/authorize - return $response->withRedirect('/application/authorize'); + $app->getContainer()->get('session')->set('authParams', $authRequest); + return $response->withRedirect(str_replace('/api/authorize/', '/application/authorize', $request->getUri()->getPath())); })->setName('home'); @@ -97,13 +95,8 @@ $app->getContainer()->get('logService')->debug('Request for access token using grant_type: %s', $request->getParam('grant_type')); $server = $app->getContainer()->get('server'); - try { - // Try to respond to the request - return $server->respondToAccessTokenRequest($request, $response); - } catch (\League\OAuth2\Server\Exception\OAuthServerException $exception) { - // All instances of OAuthServerException can be formatted into a HTTP response - return $exception->generateHttpResponse($response); - } + // Try to respond to the request + return $server->respondToAccessTokenRequest($request, $response); }); // Run app
+ <% } else { %> + <% } %> <% if (layouts.type == 1) { %> {% endverbatim %}{% trans "Layouts" %}{% verbatim %} <% } else if (layouts.type == 3) { %> @@ -685,6 +689,9 @@ {% trans "From Date" %} {% trans "To Date" %} {% trans "Layout Duration" %}{% trans "Share of Voice" %}{% trans "Display Order" %} {% trans "Priority" %} {% trans "Visible" %}<%= layout.layoutDuration %><%= layout.shareOfVoice %><%= layout.layoutDisplayOrder %> <%= layout.eventPriority %>