Files
Cloud-CMS/lib/Controller/Layout.php
Matt Batchelder 05ce0da296 Initial Upload
2025-12-02 10:32:59 -05:00

3541 lines
129 KiB
PHP

<?php
/*
* Copyright (C) 2025 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\Controller;
use Carbon\Carbon;
use GuzzleHttp\Psr7\Stream;
use Intervention\Image\ImageManagerStatic as Img;
use Mimey\MimeTypes;
use Parsedown;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Stash\Interfaces\PoolInterface;
use Stash\Item;
use Xibo\Entity\Region;
use Xibo\Entity\Session;
use Xibo\Event\TemplateProviderImportEvent;
use Xibo\Factory\CampaignFactory;
use Xibo\Factory\DataSetFactory;
use Xibo\Factory\DisplayGroupFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\PlaylistFactory;
use Xibo\Factory\ResolutionFactory;
use Xibo\Factory\TagFactory;
use Xibo\Factory\UserFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Factory\WidgetDataFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Helper\LayoutUploadHandler;
use Xibo\Helper\Profiler;
use Xibo\Helper\SendFile;
use Xibo\Helper\Status;
use Xibo\Service\MediaService;
use Xibo\Service\MediaServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Render\WidgetDownloader;
use Xibo\Widget\SubPlaylistItem;
/**
* Class Layout
* @package Xibo\Controller
*
*/
class Layout extends Base
{
/**
* @var Session
*/
private $session;
/**
* @var UserFactory
*/
private $userFactory;
/**
* @var ResolutionFactory
*/
private $resolutionFactory;
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var ModuleFactory
*/
private $moduleFactory;
/**
* @var UserGroupFactory
*/
private $userGroupFactory;
/**
* @var TagFactory
*/
private $tagFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/** @var DataSetFactory */
private $dataSetFactory;
/** @var CampaignFactory */
private $campaignFactory;
/** @var DisplayGroupFactory */
private $displayGroupFactory;
/** @var PoolInterface */
private $pool;
/** @var MediaServiceInterface */
private $mediaService;
private WidgetFactory $widgetFactory;
private PlaylistFactory $playlistFactory;
/**
* Set common dependencies.
* @param Session $session
* @param UserFactory $userFactory
* @param ResolutionFactory $resolutionFactory
* @param LayoutFactory $layoutFactory
* @param ModuleFactory $moduleFactory
* @param UserGroupFactory $userGroupFactory
* @param TagFactory $tagFactory
* @param MediaFactory $mediaFactory
* @param DataSetFactory $dataSetFactory
* @param CampaignFactory $campaignFactory
* @param $displayGroupFactory
*/
public function __construct(
$session,
$userFactory,
$resolutionFactory,
$layoutFactory,
$moduleFactory,
$userGroupFactory,
$tagFactory,
$mediaFactory,
$dataSetFactory,
$campaignFactory,
$displayGroupFactory,
$pool,
MediaServiceInterface $mediaService,
WidgetFactory $widgetFactory,
private readonly WidgetDataFactory $widgetDataFactory,
PlaylistFactory $playlistFactory,
) {
$this->session = $session;
$this->userFactory = $userFactory;
$this->resolutionFactory = $resolutionFactory;
$this->layoutFactory = $layoutFactory;
$this->moduleFactory = $moduleFactory;
$this->userGroupFactory = $userGroupFactory;
$this->tagFactory = $tagFactory;
$this->mediaFactory = $mediaFactory;
$this->dataSetFactory = $dataSetFactory;
$this->campaignFactory = $campaignFactory;
$this->displayGroupFactory = $displayGroupFactory;
$this->pool = $pool;
$this->mediaService = $mediaService;
$this->widgetFactory = $widgetFactory;
$this->playlistFactory = $playlistFactory;
}
/**
* @return LayoutFactory
*/
public function getLayoutFactory()
{
return $this->layoutFactory;
}
/**
* @return DataSetFactory
*/
public function getDataSetFactory()
{
return $this->dataSetFactory;
}
/**
* Displays the Layout Page
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function displayPage(Request $request, Response $response)
{
// Call to render the template
$this->getState()->template = 'layout-page';
return $this->render($request, $response);
}
/**
* Display the Layout Designer
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayDesigner(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->loadById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($layout))
throw new AccessDeniedException();
// Get the parent layout if it's editable
if ($layout->isEditable()) {
// Get the Layout using the Draft ID
$layout = $this->layoutFactory->getByParentId($id);
}
// 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 Editor 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');
// Get a list of timezones
$timeZones = [];
foreach (DateFormatHelper::timezoneList() as $key => $value) {
$timeZones[] = ['id' => $key, 'value' => $value];
}
// Set up any JavaScript translations
$data = [
'publishedLayoutId' => $id,
'layout' => $layout,
'resolution' => $resolution,
'isTemplate' => $isTemplate,
'zoom' => $sanitizedParams->getDouble('zoom', [
'default' => $this->getUser()->getOptionValue('defaultDesignerZoom', 1)
]),
'modules' => $moduleFactory->getAssignableModules(),
'timeZones' => $timeZones,
];
// Call the render the template
$this->getState()->template = 'layout-designer-page';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Add a Layout
* @SWG\Post(
* path="/layout",
* operationId="layoutAdd",
* tags={"layout"},
* summary="Add a Layout",
* description="Add a new Layout to the CMS",
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The layout name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="The layout description",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="layoutId",
* in="formData",
* description="If the Layout should be created with a Template, provide the ID, otherwise don't provide",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="resolutionId",
* in="formData",
* description="If a Template is not provided, provide the resolutionId for this Layout.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="returnDraft",
* in="formData",
* description="Should we return the Draft Layout or the Published Layout on Success?",
* type="boolean",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Code identifier for this Layout",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this object should be assigned to",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function add(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$name = $sanitizedParams->getString('name');
$description = $sanitizedParams->getString('description');
$enableStat = $sanitizedParams->getCheckbox('enableStat');
$autoApplyTransitions = (int)$this->getConfig()->getSetting('DEFAULT_TRANSITION_AUTO_APPLY');
$code = $sanitizedParams->getString('code', ['defaultOnEmptyString' => true]);
// Folders
$folderId = $sanitizedParams->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
// Name
if (empty($name)) {
// Create our own name for this layout.
$name = sprintf(__('Untitled %s'), Carbon::now()->format(DateFormatHelper::getSystemFormat()));
}
// Tags
if ($this->getUser()->featureEnabled('tag.tagging')) {
$tags = $this->tagFactory->tagsFromString($sanitizedParams->getString('tags'));
} else {
$tags = [];
}
$templateId = $sanitizedParams->getString('layoutId');
$resolutionId = $sanitizedParams->getInt('resolutionId');
$template = null;
// If we have a templateId provided then we create from there.
if (!empty($templateId)) {
$this->getLog()->debug('add: loading template for clone operation. templateId: ' . $templateId);
// Load the template
$template = $this->layoutFactory->loadById($templateId);
// Empty all the ID's
$layout = clone $template;
// Overwrite our new properties
$layout->layout = $name;
$layout->description = $description;
$layout->code = $code;
$layout->updateTagLinks($tags);
$this->getLog()->debug('add: loaded and cloned, about to setOwner. templateId: ' . $templateId);
// Set the owner
$layout->setOwner($this->getUser()->userId, true);
} else {
$this->getLog()->debug('add: no template, using resolution: ' . $resolutionId);
// Empty template so we create a blank layout with the provided resolution
if (empty($resolutionId)) {
// Get the nearest landscape resolution we can
$resolution = $this->resolutionFactory->getClosestMatchingResolution(1920, 1080);
// Get the ID
$resolutionId = $resolution->resolutionId;
$this->getLog()->debug('add: resolution resolved: ' . $resolutionId);
}
$layout = $this->layoutFactory->createFromResolution(
$resolutionId,
$this->getUser()->userId,
$name,
$description,
$tags,
$code,
false
);
}
// Do we have an 'Enable Layout Stats Collection?' checkbox?
// If not, we fall back to the default Stats Collection setting.
if (!$sanitizedParams->hasParam('enableStat')) {
$enableStat = (int)$this->getConfig()->getSetting('LAYOUT_STATS_ENABLED_DEFAULT');
}
// Set layout enableStat flag
$layout->enableStat = $enableStat;
// Set auto apply transitions flag
$layout->autoApplyTransitions = $autoApplyTransitions;
// set folderId
$layout->folderId = $folderId;
// Save
$layout->save(['appendCountOnDuplicate' => true]);
if ($templateId != null && $template !== null) {
$layout->copyActions($layout, $template);
// set Layout original values to current values
$layout->setOriginals();
}
$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->getRegionOrDrawer($region->getOriginalValue('regionId'));
// Make sure Playlist closure table from the published one are copied over
$original->getPlaylist()->cloneClosureTable($region->getPlaylist()->playlistId);
// set Region original values to current values
$region->setOriginals();
foreach ($region->regionPlaylist->widgets as $widget) {
// set Widget original values to current values
$widget->setOriginals();
}
}
$campaign = $this->campaignFactory->getById($layout->campaignId);
$playlist = $region->getPlaylist();
$playlist->folderId = $campaign->folderId;
$playlist->permissionsFolderId = $campaign->permissionsFolderId;
$playlist->save();
}
$this->getLog()->debug('Layout Added');
// Automatically checkout the new layout for edit
$layout = $this->layoutFactory->checkoutLayout($layout, $sanitizedParams->getCheckbox('returnDraft'));
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Added %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Edit Layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/layout/{layoutId}",
* operationId="layoutEdit",
* summary="Edit Layout",
* description="Edit a Layout",
* tags={"layout"},
* @SWG\Parameter(
* name="layoutId",
* type="integer",
* in="path",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The Layout Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="The Layout Description",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="tags",
* in="formData",
* description="A comma separated list of Tags",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="retired",
* in="formData",
* description="A flag indicating whether this Layout is retired.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="enableStat",
* in="formData",
* description="Flag indicating whether the Layout stat is enabled",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="code",
* in="formData",
* description="Code identifier for this Layout",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this object should be assigned to",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*/
public function edit(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
$folderChanged = false;
$nameChanged = false;
// check if we're dealing with the template
$isTemplate = $layout->hasTag('template');
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout))
throw new AccessDeniedException();
// Make sure we're not a draft
if ($layout->isChild()) {
throw new InvalidArgumentException(__('Cannot edit Layout properties on a Draft'), 'layoutId');
}
$layout->layout = $sanitizedParams->getString('name');
$layout->description = $sanitizedParams->getString('description');
if ($this->getUser()->featureEnabled('tag.tagging')) {
$layout->updateTagLinks($this->tagFactory->tagsFromString($sanitizedParams->getString('tags')));
}
// if it was not a template, and user added template tag, throw an error.
if (!$isTemplate && $layout->hasTag('template')) {
throw new InvalidArgumentException(__('Cannot assign a Template tag to a Layout, to create a template use the Save Template button instead.'), 'tags');
}
$layout->retired = $sanitizedParams->getCheckbox('retired');
$layout->enableStat = $sanitizedParams->getCheckbox('enableStat');
$layout->code = $sanitizedParams->getString('code');
$layout->folderId = $sanitizedParams->getInt('folderId', ['default' => $layout->folderId]);
if ($layout->hasPropertyChanged('folderId')) {
if ($layout->folderId === 1) {
$this->checkRootFolderAllowSave();
}
$folderChanged = true;
}
if ($layout->hasPropertyChanged('layout')) {
$nameChanged = true;
}
// Save
$layout->save([
'saveLayout' => true,
'saveRegions' => false,
'saveTags' => true,
'setBuildRequired' => false,
'notify' => false
]);
if ($folderChanged || $nameChanged) {
// permissionsFolderId depends on the Campaign, hence why we need to get the edited Layout back here
$editedLayout = $this->layoutFactory->getById($layout->layoutId);
// this will return the original Layout we edited and its draft
$layouts = $this->layoutFactory->getByCampaignId($layout->campaignId, true, true);
foreach ($layouts as $savedLayout) {
// if we changed the name of the original Layout, updated its draft name as well
if ($savedLayout->isChild() && $nameChanged) {
$savedLayout->layout = $editedLayout->layout;
$savedLayout->save([
'saveLayout' => true,
'saveRegions' => false,
'saveTags' => false,
'setBuildRequired' => false,
'notify' => false
]);
}
// if the folder changed on original Layout, make sure we keep its regionPlaylists and draft regionPlaylists updated
if ($folderChanged) {
$savedLayout->load();
$allRegions = array_merge($savedLayout->regions, $savedLayout->drawers);
foreach ($allRegions as $region) {
$playlist = $region->getPlaylist();
$playlist->folderId = $editedLayout->folderId;
$playlist->permissionsFolderId = $editedLayout->permissionsFolderId;
$playlist->save();
}
}
}
}
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Edit Layout Background
* @param Request $request
* @param Response $response
* @param $id
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
* @SWG\Put(
* path="/layout/background/{layoutId}",
* operationId="layoutEditBackground",
* summary="Edit Layout Background",
* description="Edit a Layout Background",
* tags={"layout"},
* @SWG\Parameter(
* name="layoutId",
* type="integer",
* in="path",
* required=true
* ),
* @SWG\Parameter(
* name="backgroundColor",
* in="formData",
* description="A HEX color to use as the background color of this Layout.",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="backgroundImageId",
* in="formData",
* description="A media ID to use as the background image for this Layout.",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="backgroundzIndex",
* in="formData",
* description="The Layer Number to use for the background.",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="resolutionId",
* in="formData",
* description="The Resolution ID to use on this Layout.",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*/
public function editBackground(Request $request, Response $response, $id): Response
{
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
// Check that this Layout is a Draft
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
$layout->backgroundColor = $sanitizedParams->getString('backgroundColor');
$layout->backgroundImageId = $sanitizedParams->getInt('backgroundImageId');
$layout->backgroundzIndex = $sanitizedParams->getInt('backgroundzIndex');
$layout->autoApplyTransitions = $sanitizedParams->getCheckbox('autoApplyTransitions');
// Check the status of the media file
if ($layout->backgroundImageId) {
$media = $this->mediaFactory->getById($layout->backgroundImageId);
if ($media->mediaType === 'image' && $media->released === 2) {
throw new InvalidArgumentException(sprintf(
__('%s set as the layout background image is too large. Please ensure that none of the images in your layout are larger than your Resize Limit on their longest edge.'),//phpcs:ignore
$media->name
));
}
}
// Resolution
$saveRegions = false;
$resolution = $this->resolutionFactory->getById($sanitizedParams->getInt('resolutionId'));
if ($layout->width != $resolution->width || $layout->height != $resolution->height) {
$this->getLog()->debug('editBackground: resolution dimensions have changed, updating layout');
$layout->load([
'loadPlaylists' => false,
'loadPermissions' => false,
'loadCampaigns' => false,
'loadActions' => false,
]);
$layout->width = $resolution->width;
$layout->height = $resolution->height;
$layout->orientation = ($layout->width >= $layout->height) ? 'landscape' : 'portrait';
// Update the canvas region with its new width/height.
foreach ($layout->regions as $region) {
if ($region->type === 'canvas') {
$this->getLog()->debug('editBackground: canvas region needs changing too');
$region->width = $layout->width;
$region->height = $layout->height;
$saveRegions = true;
}
}
}
// Save
$layout->save([
'saveLayout' => true,
'saveRegions' => $saveRegions,
'saveTags' => true,
'setBuildRequired' => true,
'notify' => false
]);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Apply a template to a Layout
* @SWG\Put(
* path="/layout/applyTemplate/{layoutId}",
* operationId="layoutApplyTemplate",
* tags={"layout"},
* summary="Apply Template",
* description="Apply a new Template to an existing Layout, replacing it.",
* @SWG\Parameter(
* name="layoutId",
* type="integer",
* in="path",
* required=true
* ),
* @SWG\Parameter(
* name="templateId",
* in="formData",
* description="If the Layout should be created with a Template, provide the ID, otherwise don't provide",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function applyTemplate(Request $request, Response $response, $id): Response
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// Get the existing layout
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
// Check that this Layout is a Draft
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
// Discard the current draft and replace it
$layout->discardDraft(false);
// Is the source remote (undocumented as it should only be via WEB)
$source = $sanitizedParams->getString('source');
if ($source === 'remote') {
// Hand off to the connector
$event = new TemplateProviderImportEvent(
$sanitizedParams->getString('download'),
$sanitizedParams->getString('templateId'),
$this->getConfig()->getSetting('LIBRARY_LOCATION')
);
$this->getLog()->debug('Dispatching event. ' . $event->getName());
try {
$this->getDispatcher()->dispatch($event, $event->getName());
} catch (\Exception $exception) {
$this->getLog()->error('Template search: Exception in dispatched event: ' . $exception->getMessage());
$this->getLog()->debug($exception->getTraceAsString());
}
$template = $this->getLayoutFactory()->createFromZip(
$event->getFilePath(),
$layout->layout,
$this->getUser()->userId,
0,
0,
0,
0,
1,
$this->getDataSetFactory(),
'',
$this->mediaService,
$layout->folderId,
false,
);
$template->managePlaylistClosureTable();
$template->manageActions();
// Handle widget data
$fallback = $layout->getUnmatchedProperty('fallback');
if ($fallback !== null) {
foreach ($layout->getAllWidgets() as $widget) {
// Did this widget have fallback data included in its export?
if (array_key_exists($widget->tempWidgetId, $fallback)) {
foreach ($fallback[$widget->tempWidgetId] as $item) {
// We create the widget data with the new widgetId
$this->widgetDataFactory
->create(
$widget->widgetId,
$item['data'] ?? [],
intval($item['displayOrder'] ?? 1),
)
->save();
}
}
}
}
@unlink($event->getFilePath());
} else {
$templateId = $sanitizedParams->getInt('templateId');
$this->getLog()->debug('add: loading template for clone operation. templateId: ' . $templateId);
// Clone the template
$template = clone $this->layoutFactory->loadById($templateId);
// Overwrite our new properties
$template->layout = $layout->layout;
$template->setOwner($layout->ownerId);
}
// Persist the parentId
$template->parentId = $layout->parentId;
$template->campaignId = $layout->campaignId;
$template->publishedStatusId = 2;
$template->save(['validate' => false]);
// for remote source, we import the Layout and save the thumbnail to temporary file
// after save we can move the image to correct library folder, as we have campaignId
if ($source === 'remote' && !empty($layout->getUnmatchedProperty('thumbnail'))) {
rename($layout->getUnmatchedProperty('thumbnail'), $template->getThumbnailUri());
}
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $layout->layout),
'id' => $template->layoutId,
'data' => $template,
]);
return $this->render($request, $response);
}
/**
* Delete Layout Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function deleteForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
if (!$this->getUser()->checkDeleteable($layout))
throw new AccessDeniedException(__('You do not have permissions to delete this layout'));
$data = [
'layout' => $layout,
];
$this->getState()->template = 'layout-form-delete';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Clear Layout Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function clearForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
if (!$this->getUser()->checkDeleteable($layout))
throw new AccessDeniedException(__('You do not have permissions to clear this layout'));
$data = [
'layout' => $layout,
];
$this->getState()->template = 'layout-form-clear';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Retire Layout Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function retireForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
$data = [
'layout' => $layout,
];
$this->getState()->template = 'layout-form-retire';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Deletes a layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Delete(
* path="/layout/{layoutId}",
* operationId="layoutDelete",
* tags={"layout"},
* summary="Delete Layout",
* description="Delete a Layout",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
function delete(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->loadById($id);
if (!$this->getUser()->checkDeleteable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to delete this layout'));
}
// Make sure we're not a draft
if ($layout->isChild()) {
throw new InvalidArgumentException(__('Cannot delete Layout from its Draft, delete the parent'), 'layoutId');
}
$layout->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $layout->layout)
]);
return $this->render($request, $response);
}
/**
* Clears a layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\GeneralException
*
* @SWG\Post(
* path="/layout/{layoutId}",
* operationId="layoutClear",
* tags={"layout"},
* summary="Clear Layout",
* description="Clear a draft layouts canvas of all widgets and elements, leaving it blank.",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID to Clear, must be a draft.",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function clear(Request $request, Response $response, $id): Response
{
// Get the existing layout
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
// Check that this Layout is a Draft
if (!$layout->isChild()) {
throw new InvalidArgumentException(__('This Layout is not a Draft, please checkout.'), 'layoutId');
}
// Discard the current draft and replace it
$layout->discardDraft(false);
// Blank
$resolution = $this->resolutionFactory->getClosestMatchingResolution($layout->width, $layout->height);
$blank = $this->layoutFactory->createFromResolution(
$resolution->resolutionId,
$layout->ownerId,
$layout->layout,
null,
null,
null,
false
);
// Persist the parentId
$blank->parentId = $layout->parentId;
$blank->campaignId = $layout->campaignId;
$blank->publishedStatusId = 2;
$blank->save(['validate' => false, 'auditMessage' => 'Canvas Cleared']);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Cleared %s'), $layout->layout),
'id' => $blank->layoutId,
'data' => $blank,
]);
return $this->render($request, $response);
}
/**
* Retires a layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/layout/retire/{layoutId}",
* operationId="layoutRetire",
* tags={"layout"},
* summary="Retire Layout",
* description="Retire a Layout so that it isn't available to Schedule. Existing Layouts will still be played",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
function retire(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
// Make sure we're not a draft
if ($layout->isChild()) {
throw new InvalidArgumentException(__('Cannot modify Layout from its Draft'), 'layoutId');
}
// Make sure we aren't the global default
if ($layout->layoutId == $this->getConfig()->getSetting('DEFAULT_LAYOUT')) {
throw new InvalidArgumentException(__('This Layout is used as the global default and cannot be retired'),
'layoutId');
}
$layout->retired = 1;
$layout->save([
'saveLayout' => true,
'saveRegions' => false,
'saveTags' => false,
'setBuildRequired' => false
]);
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Retired %s'), $layout->layout)
]);
return $this->render($request, $response);
}
/**
* Unretire Layout Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function unretireForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
$data = [
'layout' => $layout,
];
$this->getState()->template = 'layout-form-unretire';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Unretires a layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/layout/unretire/{layoutId}",
* operationId="layoutUnretire",
* tags={"layout"},
* summary="Unretire Layout",
* description="Retire a Layout so that it isn't available to Schedule. Existing Layouts will still be played",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
function unretire(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
// Make sure we're not a draft
if ($layout->isChild()) {
throw new InvalidArgumentException(__('Cannot modify Layout from its Draft'), 'layoutId');
}
$layout->retired = 0;
$layout->save([
'saveLayout' => true,
'saveRegions' => false,
'saveTags' => false,
'setBuildRequired' => false,
]);
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Unretired %s'), $layout->layout)
]);
return $this->render($request, $response);
}
/**
* Set Enable Stats Collection of a layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Put(
* path="/layout/setenablestat/{layoutId}",
* operationId="layoutSetEnableStat",
* tags={"layout"},
* summary="Enable Stats Collection",
* description="Set Enable Stats Collection? to use for the collection of Proof of Play statistics for a Layout.",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="enableStat",
* in="formData",
* description="Flag indicating whether the Layout stat is enabled",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
function setEnableStat(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
// Make sure we're not a draft
if ($layout->isChild()) {
throw new InvalidArgumentException(__('Cannot modify Layout from its Draft'), 'layoutId');
}
$enableStat = $sanitizedParams->getCheckbox('enableStat');
$layout->enableStat = $enableStat;
$layout->save(['saveTags' => false]);
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('For Layout %s Enable Stats Collection is set to %s'), $layout->layout, ($layout->enableStat == 1) ? __('On') : __('Off'))
]);
return $this->render($request, $response);
}
/**
* Set Enable Stat Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function setEnableStatForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
$data = [
'layout' => $layout,
];
$this->getState()->template = 'layout-form-setenablestat';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Shows the Layout Grid
*
* @SWG\Get(
* path="/layout",
* operationId="layoutSearch",
* tags={"layout"},
* summary="Search Layouts",
* description="Search for Layouts viewable by this user",
* @SWG\Parameter(
* name="layoutId",
* in="query",
* description="Filter by Layout Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="parentId",
* in="query",
* description="Filter by parent Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="showDrafts",
* in="query",
* description="Flag indicating whether to show drafts",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="layout",
* in="query",
* description="Filter by partial Layout name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="userId",
* in="query",
* description="Filter by user Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="retired",
* in="query",
* description="Filter by retired flag",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="tags",
* in="query",
* description="Filter by Tags",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="exactTags",
* in="query",
* description="A flag indicating whether to treat the tags filter as an exact match",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="logicalOperator",
* in="query",
* description="When filtering by multiple Tags, which logical operator should be used? AND|OR",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ownerUserGroupId",
* in="query",
* description="Filter by users in this UserGroupId",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="publishedStatusId",
* in="query",
* description="Filter by published status id, 1 - Published, 2 - Draft",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="embed",
* in="query",
* description="Embed related data such as regions, playlists, widgets, tags, campaigns, permissions",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="campaignId",
* in="query",
* description="Get all Layouts for a given campaignId",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="query",
* description="Filter by Folder ID",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Layout")
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Twig\Error\LoaderError
* @throws \Twig\Error\RuntimeError
* @throws \Twig\Error\SyntaxError
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function grid(Request $request, Response $response)
{
$this->getState()->template = 'grid';
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
// Should we parse the description into markdown
$showDescriptionId = $parsedQueryParams->getInt('showDescriptionId');
// We might need to embed some extra content into the response if the "Show Description"
// is set to media listing
if ($showDescriptionId === 3) {
$embed = ['regions', 'playlists', 'widgets'];
} else {
// Embed?
$embed = ($parsedQueryParams->getString('embed') != null)
? explode(',', $parsedQueryParams->getString('embed'))
: [];
}
// Get all layouts
$layouts = $this->layoutFactory->query($this->gridRenderSort($parsedQueryParams), $this->gridRenderFilter([
'layout' => $parsedQueryParams->getString('layout'),
'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
'userId' => $parsedQueryParams->getInt('userId'),
'retired' => $parsedQueryParams->getInt('retired'),
'tags' => $parsedQueryParams->getString('tags'),
'exactTags' => $parsedQueryParams->getCheckbox('exactTags'),
'filterLayoutStatusId' => $parsedQueryParams->getInt('layoutStatusId'),
'layoutId' => $parsedQueryParams->getInt('layoutId'),
'parentId' => $parsedQueryParams->getInt('parentId'),
'showDrafts' => $parsedQueryParams->getInt('showDrafts'),
'ownerUserGroupId' => $parsedQueryParams->getInt('ownerUserGroupId'),
'mediaLike' => $parsedQueryParams->getString('mediaLike'),
'publishedStatusId' => $parsedQueryParams->getInt('publishedStatusId'),
'activeDisplayGroupId' => $parsedQueryParams->getInt('activeDisplayGroupId'),
'campaignId' => $parsedQueryParams->getInt('campaignId'),
'folderId' => $parsedQueryParams->getInt('folderId'),
'codeLike' => $parsedQueryParams->getString('codeLike'),
'orientation' => $parsedQueryParams->getString('orientation', ['defaultOnEmptyString' => true]),
'onlyMyLayouts' => $parsedQueryParams->getCheckbox('onlyMyLayouts'),
'logicalOperator' => $parsedQueryParams->getString('logicalOperator'),
'logicalOperatorName' => $parsedQueryParams->getString('logicalOperatorName'),
'campaignType' => 'list',
'modifiedSinceDt' => $parsedQueryParams->getDate('modifiedSinceDt'),
], $parsedQueryParams));
foreach ($layouts as $layout) {
/* @var \Xibo\Entity\Layout $layout */
if (in_array('regions', $embed)) {
$layout->load([
'loadPlaylists' => in_array('playlists', $embed),
'loadCampaigns' => in_array('campaigns', $embed),
'loadPermissions' => in_array('permissions', $embed),
'loadTags' => in_array('tags', $embed),
'loadWidgets' => in_array('widgets', $embed),
'loadActions' => in_array('actions', $embed)
]);
}
// Populate the status message
$layout->getStatusMessage();
// Add Locking information
$layout = $this->layoutFactory->decorateLockedProperties($layout);
// Annotate each Widget with its validity, tags and permissions
if (in_array('widget_validity', $embed) || in_array('tags', $embed) || in_array('permissions', $embed)) {
foreach ($layout->getAllWidgets() as $widget) {
try {
$module = $this->moduleFactory->getByType($widget->type);
} catch (NotFoundException $notFoundException) {
// This module isn't available, mark it as invalid.
$widget->isValid = false;
$widget->setUnmatchedProperty('moduleName', __('Invalid Module'));
$widget->setUnmatchedProperty('name', __('Invalid Module'));
$widget->setUnmatchedProperty('tags', []);
$widget->setUnmatchedProperty('isDeletable', 1);
continue;
}
$widget->setUnmatchedProperty('moduleName', $module->name);
$widget->setUnmatchedProperty('moduleDataType', $module->dataType);
if ($module->regionSpecific == 0) {
// Use the media assigned to this widget
$media = $this->mediaFactory->getById($widget->getPrimaryMediaId());
$widget->setUnmatchedProperty('name', $widget->getOptionValue('name', null) ?: $media->name);
// Augment with tags
$widget->setUnmatchedProperty('tags', $media->tags);
} else {
$widget->setUnmatchedProperty('name', $widget->getOptionValue('name', null) ?: $module->name);
$widget->setUnmatchedProperty('tags', []);
}
// Sub-playlists should calculate a fresh duration
if ($widget->type === 'subplaylist') {
// We know we have a provider class for this module.
$widget->calculateDuration($module);
}
if (in_array('widget_validity', $embed)) {
$status = 0;
$layout->assessWidgetStatus($module, $widget, $status);
$widget->isValid = $status === 1;
}
// apply default transitions to a dynamic parameters on widget object.
if ($layout->autoApplyTransitions == 1) {
$widgetTransIn = $widget->getOptionValue('transIn', $this->getConfig()->getSetting('DEFAULT_TRANSITION_IN'));
$widgetTransOut = $widget->getOptionValue('transOut', $this->getConfig()->getSetting('DEFAULT_TRANSITION_OUT'));
$widgetTransInDuration = $widget->getOptionValue('transInDuration', $this->getConfig()->getSetting('DEFAULT_TRANSITION_DURATION'));
$widgetTransOutDuration = $widget->getOptionValue('transOutDuration', $this->getConfig()->getSetting('DEFAULT_TRANSITION_DURATION'));
} else {
$widgetTransIn = $widget->getOptionValue('transIn', null);
$widgetTransOut = $widget->getOptionValue('transOut', null);
$widgetTransInDuration = $widget->getOptionValue('transInDuration', null);
$widgetTransOutDuration = $widget->getOptionValue('transOutDuration', null);
}
$widget->transitionIn = $widgetTransIn;
$widget->transitionOut = $widgetTransOut;
$widget->transitionDurationIn = $widgetTransInDuration;
$widget->transitionDurationOut = $widgetTransOutDuration;
if (in_array('permissions', $embed)) {
// Augment with editable flag
$widget->setUnmatchedProperty('isEditable', $this->getUser()->checkEditable($widget));
// Augment with deletable flag
$widget->setUnmatchedProperty('isDeletable', $this->getUser()->checkDeleteable($widget));
// Augment with viewable flag
$widget->setUnmatchedProperty('isViewable', $this->getUser()->checkViewable($widget));
// Augment with permissions flag
$widget->setUnmatchedProperty(
'isPermissionsModifiable',
$this->getUser()->checkPermissionsModifyable($widget)
);
}
}
/** @var Region[] $allRegions */
$allRegions = array_merge($layout->regions, $layout->drawers);
// Augment regions with permissions
foreach ($allRegions as $region) {
if (in_array('permissions', $embed)) {
// Augment with editable flag
$region->setUnmatchedProperty('isEditable', $this->getUser()->checkEditable($region));
// Augment with deletable flag
$region->setUnmatchedProperty('isDeletable', $this->getUser()->checkDeleteable($region));
// Augment with viewable flag
$region->setUnmatchedProperty('isViewable', $this->getUser()->checkViewable($region));
// Augment with permissions flag
$region->setUnmatchedProperty(
'isPermissionsModifiable',
$this->getUser()->checkPermissionsModifyable($region)
);
}
}
}
if ($this->isApi($request)) {
continue;
}
$layout->includeProperty('buttons');
// Thumbnail
$layout->setUnmatchedProperty('thumbnail', '');
if (file_exists($layout->getThumbnailUri())) {
$layout->setUnmatchedProperty(
'thumbnail',
$this->urlFor($request, 'layout.download.thumbnail', ['id' => $layout->layoutId])
);
}
// Fix up the description
$layout->setUnmatchedProperty('descriptionFormatted', $layout->description);
if ($layout->description != '') {
if ($showDescriptionId == 1) {
// Parse down for description
$layout->setUnmatchedProperty(
'descriptionFormatted',
Parsedown::instance()->setSafeMode(true)->text($layout->description)
);
} else if ($showDescriptionId == 2) {
$layout->setUnmatchedProperty('descriptionFormatted', strtok($layout->description, "\n"));
}
}
if ($showDescriptionId === 3) {
// Load in the entire object model - creating module objects so that we can get the name of each
// widget and its items.
foreach ($layout->regions as $region) {
foreach ($region->getPlaylist()->widgets as $widget) {
$module = $this->moduleFactory->getByType($widget->type);
$widget->setUnmatchedProperty('moduleName', $module->name);
$widget->setUnmatchedProperty('name', $widget->getOptionValue('name', $module->name));
}
}
// provide our layout object to a template to render immediately
$layout->setUnmatchedProperty('descriptionFormatted', $this->renderTemplateToString(
'layout-page-grid-widgetlist',
(array)$layout
));
}
$layout->setUnmatchedProperty('statusDescription', match ($layout->status) {
Status::$STATUS_VALID => __('This Layout is ready to play'),
Status::$STATUS_PLAYER => __('There are items on this Layout that can only be assessed by the Display'),
Status::$STATUS_NOT_BUILT => __('This Layout has not been built yet'),
default => __('This Layout is invalid and should not be scheduled'),
});
$layout->setUnmatchedProperty('enableStatDescription', match ($layout->enableStat) {
1 => __('This Layout has enable stat collection set to ON'),
default => __('This Layout has enable stat collection set to OFF'),
});
// Check if user has "delete permissions" - for layout designer to show/hide Delete button
$layout->setUnmatchedProperty('deletePermission', $this->getUser()->featureEnabled('layout.modify'));
// Check if user has view permissions to the schedule now page - for layout designer to show/hide
// the Schedule Now button
$layout->setUnmatchedProperty('scheduleNowPermission', $this->getUser()->featureEnabled('schedule.add'));
// Add some buttons for this row
if ($this->getUser()->featureEnabled('layout.modify')
&& $this->getUser()->checkEditable($layout)
) {
// Design Button
$layout->buttons[] = array(
'id' => 'layout_button_design',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'layout.designer', array('id' => $layout->layoutId)),
'text' => __('Design')
);
// Should we show a publish/discard button?
if ($layout->isEditable()) {
$layout->buttons[] = ['divider' => true];
$layout->buttons[] = array(
'id' => 'layout_button_publish',
'url' => $this->urlFor($request, 'layout.publish.form', ['id' => $layout->layoutId]),
'text' => __('Publish')
);
$layout->buttons[] = array(
'id' => 'layout_button_discard',
'url' => $this->urlFor($request, 'layout.discard.form', ['id' => $layout->layoutId]),
'text' => __('Discard')
);
$layout->buttons[] = ['divider' => true];
} else {
$layout->buttons[] = ['divider' => true];
// Checkout Button
$layout->buttons[] = array(
'id' => 'layout_button_checkout',
'url' => $this->urlFor($request, 'layout.checkout.form', ['id' => $layout->layoutId]),
'text' => __('Checkout'),
'dataAttributes' => [
['name' => 'auto-submit', 'value' => true],
['name' => 'commit-url', 'value' => $this->urlFor($request, 'layout.checkout', ['id' => $layout->layoutId])],
['name' => 'commit-method', 'value' => 'PUT']
]
);
$layout->buttons[] = ['divider' => true];
}
}
// Preview
if ($this->getUser()->featureEnabled('layout.view')) {
$layout->buttons[] = array(
'id' => 'layout_button_preview',
'external' => true,
'url' => '#',
'onclick' => 'createMiniLayoutPreview',
'onclickParam' => $this->urlFor($request, 'layout.preview', ['id' => $layout->layoutId]),
'text' => __('Preview Layout')
);
// Also offer a way to preview the draft layout.
if ($layout->hasDraft()) {
$layout->buttons[] = array(
'id' => 'layout_button_preview_draft',
'external' => true,
'url' => '#',
'onclick' => 'createMiniLayoutPreview',
'onclickParam' => $this->urlFor($request, 'layout.preview', ['id' => $layout->layoutId]) . '?isPreviewDraft=true',
'text' => __('Preview Draft Layout')
);
}
$layout->buttons[] = ['divider' => true];
}
// Schedule
if ($this->getUser()->featureEnabled('schedule.add')) {
$layout->buttons[] = array(
'id' => 'layout_button_schedule',
'url' => $this->urlFor($request, 'schedule.add.form', ['id' => $layout->campaignId, 'from' => 'Layout']),
'text' => __('Schedule')
);
}
// Assign to Campaign
if ($this->getUser()->featureEnabled('campaign.modify')) {
$layout->buttons[] = array(
'id' => 'layout_button_assignTo_campaign',
'url' => $this->urlFor($request, 'layout.assignTo.campaign.form', ['id' => $layout->layoutId]),
'text' => __('Assign to Campaign')
);
}
$layout->buttons[] = ['divider' => true];
if ($this->getUser()->featureEnabled('playlist.view')) {
$layout->buttons[] = [
'id' => 'layout_button_playlist_jump',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'playlist.view') .'?layoutId=' . $layout->layoutId,
'text' => __('Jump to Playlists included on this Layout')
];
}
if ($this->getUser()->featureEnabled('campaign.view')) {
$layout->buttons[] = [
'id' => 'layout_button_campaign_jump',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'campaign.view') .'?layoutId=' . $layout->layoutId,
'text' => __('Jump to Campaigns containing this Layout')
];
}
if ($this->getUser()->featureEnabled('library.view')) {
$layout->buttons[] = [
'id' => 'layout_button_media_jump',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'library.view') .'?layoutId=' . $layout->layoutId,
'text' => __('Jump to Media included on this Layout')
];
}
$layout->buttons[] = ['divider' => true];
// Only proceed if we have edit permissions
if ($this->getUser()->featureEnabled('layout.modify')
&& $this->getUser()->checkEditable($layout)
) {
// Edit Button
$layout->buttons[] = array(
'id' => 'layout_button_edit',
'url' => $this->urlFor($request, 'layout.edit.form', ['id' => $layout->layoutId]),
'text' => __('Edit')
);
if ($this->getUser()->featureEnabled('folder.view')) {
// Select Folder
$layout->buttons[] = [
'id' => 'campaign_button_selectfolder',
'url' => $this->urlFor($request, 'campaign.selectfolder.form', ['id' => $layout->campaignId]),
'text' => __('Select Folder'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request, 'campaign.selectfolder', ['id' => $layout->campaignId])],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'campaign_button_selectfolder'],
['name' => 'text', 'value' => __('Move to Folder')],
['name' => 'rowtitle', 'value' => $layout->layout],
['name' => 'form-callback', 'value' => 'moveFolderMultiSelectFormOpen']
]
];
}
// Copy Button
$layout->buttons[] = array(
'id' => 'layout_button_copy',
'url' => $this->urlFor($request, 'layout.copy.form', ['id' => $layout->layoutId]),
'text' => __('Copy')
);
// Retire Button
if ($layout->retired == 0) {
$layout->buttons[] = [
'id' => 'layout_button_retire',
'url' => $this->urlFor($request, 'layout.retire.form', ['id' => $layout->layoutId]),
'text' => __('Retire'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request, 'layout.retire', ['id' => $layout->layoutId])],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'layout_button_retire'],
['name' => 'text', 'value' => __('Retire')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $layout->layout]
]
];
} else {
$layout->buttons[] = array(
'id' => 'layout_button_unretire',
'url' => $this->urlFor($request, 'layout.unretire.form', ['id' => $layout->layoutId]),
'text' => __('Unretire'),
);
}
// Extra buttons if have delete permissions
if ($this->getUser()->checkDeleteable($layout)) {
// Delete Button
$layout->buttons[] = [
'id' => 'layout_button_delete',
'url' => $this->urlFor($request, 'layout.delete.form', ['id' => $layout->layoutId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request, 'layout.delete', ['id' => $layout->layoutId])],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'layout_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $layout->layout]
]
];
}
// Set Enable Stat
$layout->buttons[] = [
'id' => 'layout_button_setenablestat',
'url' => $this->urlFor($request, 'layout.setenablestat.form', ['id' => $layout->layoutId]),
'text' => __('Enable stats collection?'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request, 'layout.setenablestat', ['id' => $layout->layoutId])],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'layout_button_setenablestat'],
['name' => 'text', 'value' => __('Enable stats collection?')],
['name' => 'rowtitle', 'value' => $layout->layout],
['name' => 'form-callback', 'value' => 'setEnableStatMultiSelectFormOpen']
]
];
$layout->buttons[] = ['divider' => true];
if ($this->getUser()->featureEnabled('template.modify') && !$layout->isEditable()) {
// Save template button
$layout->buttons[] = array(
'id' => 'layout_button_save_template',
'url' => $this->urlFor($request, 'template.from.layout.form', ['id' => $layout->layoutId]),
'text' => __('Save Template')
);
}
// Export Button
if ($this->getUser()->featureEnabled('layout.export')) {
$layout->buttons[] = array(
'id' => 'layout_button_export',
'url' => $this->urlFor($request, 'layout.export.form', ['id' => $layout->layoutId]),
'text' => __('Export')
);
}
// Extra buttons if we have modify permissions
if ($this->getUser()->checkPermissionsModifyable($layout)) {
// Permissions button
$layout->buttons[] = [
'id' => 'layout_button_permissions',
'url' => $this->urlFor($request, 'user.permissions.form', ['entity' => 'Campaign', 'id' => $layout->campaignId]),
'text' => __('Share'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request, 'user.permissions.multi', ['entity' => 'Campaign', 'id' => $layout->campaignId])],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'layout_button_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $layout->layout],
['name' => 'sort-group', 'value' => 2],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
['name' => 'custom-handler-url', 'value' => $this->urlFor($request, 'user.permissions.multi.form', ['entity' => 'Campaign'])],
['name' => 'content-id-name', 'value' => 'campaignId']
]
];
}
}
}
// Store the table rows
$this->getState()->recordsTotal = $this->layoutFactory->countLast();
$this->getState()->setData($layouts);
return $this->render($request, $response);
}
/**
* Edit form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function editForm(Request $request, Response $response, $id)
{
// Get the layout
$layout = $this->layoutFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'layout-form-edit';
$this->getState()->setData([
'layout' => $layout,
'tagString' => $layout->getTagString(),
]);
return $this->render($request, $response);
}
/**
* Edit form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
function editBackgroundForm(Request $request, Response $response, $id)
{
// Get the layout
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Check Permissions
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
// Edits always happen on Drafts, get the draft Layout using the Parent Layout ID
if ($layout->schemaVersion < 2) {
$resolution = $this->resolutionFactory->getByDesignerDimensions($layout->width, $layout->height);
} else {
$resolution = $this->resolutionFactory->getByDimensions($layout->width, $layout->height);
}
// If we have a background image, output it
$backgroundId = $sanitizedParams->getInt('backgroundOverride', ['default' => $layout->backgroundImageId]);
$backgrounds = ($backgroundId != null) ? [$this->mediaFactory->getById($backgroundId)] : [];
$this->getState()->template = 'layout-form-background';
$this->getState()->setData([
'layout' => $layout,
'resolution' => $resolution,
'resolutions' => $this->resolutionFactory->query(
['resolution'],
[
'withCurrent' => $resolution->resolutionId,
'enabled' => 1
]
),
'backgroundId' => $backgroundId,
'backgrounds' => $backgrounds,
]);
return $this->render($request, $response);
}
/**
* Copy layout form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function copyForm(Request $request, Response $response, $id)
{
// Get the layout
$layout = $this->layoutFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($layout))
throw new AccessDeniedException();
$this->getState()->template = 'layout-form-copy';
$this->getState()->setData([
'layout' => $layout,
]);
return $this->render($request, $response);
}
/**
* Copies a layout
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ConfigurationException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @SWG\Post(
* path="/layout/copy/{layoutId}",
* operationId="layoutCopy",
* tags={"layout"},
* summary="Copy Layout",
* description="Copy a Layout, providing a new name if applicable",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID to Copy",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The name for the new Layout",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="description",
* in="formData",
* description="The Description for the new Layout",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="copyMediaFiles",
* in="formData",
* description="Flag indicating whether to make new Copies of all Media Files assigned to the Layout being Copied",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*/
public function copy(Request $request, Response $response, $id)
{
// Get the layout
$originalLayout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Check Permissions
if (!$this->getUser()->checkViewable($originalLayout)) {
throw new AccessDeniedException();
}
// Make sure we're not a draft
if ($originalLayout->isChild()) {
throw new InvalidArgumentException(__('Cannot copy a Draft Layout'), 'layoutId');
}
// Load the layout for Copy
$originalLayout->load();
// Clone
$layout = clone $originalLayout;
$this->getLog()->debug('Tag values from original layout: ' . $originalLayout->getTagString());
$layout->layout = $sanitizedParams->getString('name');
$layout->description = $sanitizedParams->getString('description');
$layout->updateTagLinks($originalLayout->tags);
$layout->setOwner($this->getUser()->userId, true);
// Copy the media on the layout and change the assignments.
// https://github.com/xibosignage/xibo/issues/1283
if ($sanitizedParams->getCheckbox('copyMediaFiles') == 1) {
// track which Media Id we already copied
$copiedMediaIds = [];
foreach ($layout->getAllWidgets() as $widget) {
// Copy the media
if ( $widget->type === 'image' || $widget->type === 'video' || $widget->type === 'pdf' || $widget->type === 'powerpoint' || $widget->type === 'audio' ) {
$oldMedia = $this->mediaFactory->getById($widget->getPrimaryMediaId());
// check if we already cloned this media, if not, do it and add it the array
if (!array_key_exists($oldMedia->mediaId, $copiedMediaIds)) {
$media = clone $oldMedia;
$media->setOwner($this->getUser()->userId);
$media->save();
$copiedMediaIds[$oldMedia->mediaId] = $media->mediaId;
} else {
// if we already cloned that media, look it up and assign to Widget.
$mediaId = $copiedMediaIds[$oldMedia->mediaId];
$media = $this->mediaFactory->getById($mediaId);
}
$widget->unassignMedia($oldMedia->mediaId);
$widget->assignMedia($media->mediaId);
// Update the widget option with the new ID
$widget->setOptionValue('uri', 'attrib', $media->storedAs);
}
}
// Also handle the background image, if there is one
if ($layout->backgroundImageId != 0) {
$oldMedia = $this->mediaFactory->getById($layout->backgroundImageId);
// check if we already cloned this media, if not, do it and add it the array
if (!array_key_exists($oldMedia->mediaId, $copiedMediaIds)) {
$media = clone $oldMedia;
$media->setOwner($this->getUser()->userId);
$media->save();
$copiedMediaIds[$oldMedia->mediaId] = $media->mediaId;
} else {
// if we already cloned that media, look it up and assign to Layout backgroundImage.
$mediaId = $copiedMediaIds[$oldMedia->mediaId];
$media = $this->mediaFactory->getById($mediaId);
}
$layout->backgroundImageId = $media->mediaId;
}
}
// Save the new layout
$layout->save();
$allRegions = array_merge($layout->regions, $layout->drawers);
// this will adjust source/target Ids in the copied layout
$layout->copyActions($layout, $originalLayout);
// Sub-Playlist
/** @var Region $region */
foreach ($allRegions as $region) {
// Match our original region id to the id in the parent layout
$original = $originalLayout->getRegionOrDrawer($region->getOriginalValue('regionId'));
// Make sure Playlist closure table from the published one are copied over
$original->getPlaylist()->cloneClosureTable($region->getPlaylist()->playlistId);
}
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Copied as %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* @SWG\Post(
* path="/layout/{layoutId}/tag",
* operationId="layoutTag",
* tags={"layout"},
* summary="Tag Layout",
* description="Tag a Layout with one or more tags",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout Id to Tag",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="tag",
* in="formData",
* description="An array of tags",
* type="array",
* required=true,
* @SWG\Items(type="string")
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function tag(Request $request, Response $response, $id)
{
// Edit permission
// Get the layout
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Check Permissions
if (!$this->getUser()->checkEditable($layout))
throw new AccessDeniedException();
// Make sure we're not a draft
if ($layout->isChild())
throw new InvalidArgumentException(__('Cannot manage tags on a Draft Layout'), 'layoutId');
$tags = $sanitizedParams->getArray('tag');
if (count($tags) <= 0) {
throw new InvalidArgumentException(__('No tags to assign'));
}
foreach ($tags as $tag) {
$layout->assignTag($this->tagFactory->tagFromString($tag));
}
$layout->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Tagged %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* @SWG\Post(
* path="/layout/{layoutId}/untag",
* operationId="layoutUntag",
* tags={"layout"},
* summary="Untag Layout",
* description="Untag a Layout with one or more tags",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout Id to Untag",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="tag",
* in="formData",
* description="An array of tags",
* type="array",
* required=true,
* @SWG\Items(type="string")
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function untag(Request $request, Response $response, $id)
{
// Edit permission
// Get the layout
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Check Permissions
if (!$this->getUser()->checkEditable($layout))
throw new AccessDeniedException();
// Make sure we're not a draft
if ($layout->isChild())
throw new InvalidArgumentException(__('Cannot manage tags on a Draft Layout'), 'layoutId');
$tags = $sanitizedParams->getArray('tag');
if (count($tags) <= 0)
throw new InvalidArgumentException(__('No tags to unassign'), 'tag');
foreach ($tags as $tag) {
$layout->unassignTag($this->tagFactory->tagFromString($tag));
}
$layout->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Untagged %s'), $layout->layout),
'id' => $layout->layoutId,
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Layout Status
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Get(
* path="/layout/status/{layoutId}",
* operationId="layoutStatus",
* tags={"layout"},
* summary="Layout Status",
* description="Calculate the Layout status and return a Layout",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout Id to get the status",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*/
public function status(Request $request, Response $response, $id)
{
// Get the layout
$layout = $this->layoutFactory->concurrentRequestLock($this->layoutFactory->getById($id));
try {
$layout = $this->layoutFactory->decorateLockedProperties($layout);
$layout->xlfToDisk();
} finally {
// Release lock
$this->layoutFactory->concurrentRequestRelease($layout);
}
switch ($layout->status) {
case Status::$STATUS_VALID:
$status = __('This Layout is ready to play');
break;
case Status::$STATUS_PLAYER:
$status = __('There are items on this Layout that can only be assessed by the Display');
break;
case Status::$STATUS_NOT_BUILT:
$status = __('This Layout has not been built yet');
break;
default:
$status = __('This Layout is invalid and should not be scheduled');
}
// We want a different return depending on whether we are arriving through the API or WEB routes
if ($this->isApi($request)) {
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => $status,
'id' => $layout->status,
'data' => $layout
]);
} else {
$this->getState()->html = $status;
$this->getState()->extra = [
'status' => $layout->status,
'duration' => $layout->duration,
'statusMessage' => $layout->getStatusMessage(),
'isLocked' => $layout->isLocked
];
$this->getState()->success = true;
$this->session->refreshExpiry = false;
}
return $this->render($request, $response);
}
/**
* Export Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function exportForm(Request $request, Response $response, $id)
{
// Get the layout
$layout = $this->layoutFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($layout))
throw new AccessDeniedException();
// Make sure we're not a draft
if ($layout->isChild()) {
throw new InvalidArgumentException(__('Cannot export Draft Layout'), 'layoutId');
}
// Render the form
$this->getState()->template = 'layout-form-export';
$this->getState()->setData([
'layout' => $layout,
'saveAs' => 'export_' . preg_replace('/[^a-z0-9]+/', '-', strtolower($layout->layout))
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function export(Request $request, Response $response, $id)
{
$this->setNoOutput(true);
// Get the layout
$layout = $this->layoutFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Check Permissions
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException();
}
// Make sure we're not a draft
if ($layout->isChild()) {
throw new InvalidArgumentException(__('Cannot export Draft Layout'), 'layoutId');
}
// Save As?
$saveAs = $sanitizedParams->getString('saveAs');
// Make sure our file name is reasonable
if (empty($saveAs)) {
$saveAs = 'export_' . preg_replace('/[^a-z0-9]+/', '-', strtolower($layout->layout));
} else {
$saveAs = preg_replace('/[^a-z0-9]+/', '-', strtolower($saveAs));
}
$fileName = $this->getConfig()->getSetting('LIBRARY_LOCATION') . 'temp/' . $saveAs . '.zip';
$layout->toZip(
$this->dataSetFactory,
$this->widgetDataFactory,
$fileName,
[
'includeData' => ($sanitizedParams->getCheckbox('includeData') == 1),
'includeFallback' => ($sanitizedParams->getCheckbox('includeFallback') == 1),
]
);
return $this->render($request, SendFile::decorateResponse(
$response,
$this->getConfig()->getSetting('SENDFILE_MODE'),
$fileName
));
}
/**
* TODO: Not sure how to document this.
* SWG\Post(
* path="/layout/import",
* operationId="layoutImport",
* tags={"layout"},
* summary="Import Layout",
* description="Upload and Import a Layout",
* consumes="multipart/form-data",
* SWG\Parameter(
* name="file",
* in="formData",
* description="The file",
* type="file",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*
* @param Request $request
* @param Response $response
* @return Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function import(Request $request, Response $response)
{
$this->getLog()->debug('Import Layout');
$parsedBody = $this->getSanitizer($request->getParams());
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Make sure the library exists
MediaService::ensureLibraryExists($this->getConfig()->getSetting('LIBRARY_LOCATION'));
// Make sure there is room in the library
$libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;
// Folders
$folderId = $parsedBody->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
$options = [
'userId' => $this->getUser()->userId,
'controller' => $this,
'dataSetFactory' => $this->getDataSetFactory(),
'widgetDataFactory' => $this->widgetDataFactory,
'image_versions' => [],
'accept_file_types' => '/\.zip$/i',
'libraryLimit' => $libraryLimit,
'libraryQuotaFull' => ($libraryLimit > 0 && $this->mediaService->libraryUsage() > $libraryLimit),
'mediaService' => $this->mediaService,
'sanitizerService' => $this->getSanitizerService(),
'folderId' => $folderId,
];
$this->setNoOutput();
// Hand off to the Upload Handler provided by jquery-file-upload
new LayoutUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options);
// Explicitly set the Content-Type header to application/json
return $response->withHeader('Content-Type', 'application/json');
}
/**
* Gets a file from the library
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function downloadBackground(Request $request, Response $response, $id)
{
$this->getLog()->debug('Layout Download background request for layoutId ' . $id);
$layout = $this->layoutFactory->getById($id);
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException();
}
if ($layout->backgroundImageId == null) {
throw new NotFoundException();
}
// This media may not be viewable, but we won't check it because the user has permission to view the
// layout that it is assigned to.
$media = $this->mediaFactory->getById($layout->backgroundImageId);
// Make a media module
if ($media->mediaType !== 'image') {
throw new NotFoundException(__('Layout background must be an image'));
}
// Hand over to the widget downloader
$downloader = new WidgetDownloader(
$this->getConfig()->getSetting('LIBRARY_LOCATION'),
$this->getConfig()->getSetting('SENDFILE_MODE'),
$this->getConfig()->getSetting('DEFAULT_RESIZE_LIMIT', 6000)
);
$downloader->useLogger($this->getLog()->getLoggerInterface());
$response = $downloader->imagePreview(
$this->getSanitizer([
'width' => $layout->width,
'height' => $layout->height,
'proportional' => 0,
]),
$media->storedAs,
$response,
);
$this->setNoOutput(true);
return $this->render($request, $response);
}
/**
* Assign to Campaign Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function assignToCampaignForm(Request $request, Response $response, $id)
{
// Get the layout
$layout = $this->layoutFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException();
}
// Render the form
$this->getState()->template = 'layout-form-assign-to-campaign';
$this->getState()->setData([
'layout' => $layout,
]);
return $this->render($request, $response);
}
/**
* Checkout Layout Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function checkoutForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
$data = ['layout' => $layout];
$this->getState()->template = 'layout-form-checkout';
$this->getState()->autoSubmit = $this->getAutoSubmit('layoutCheckoutForm');
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Checkout Layout
*
* @SWG\Put(
* path="/layout/checkout/{layoutId}",
* operationId="layoutCheckout",
* tags={"layout"},
* summary="Checkout Layout",
* description="Checkout a Layout so that it can be edited. The original Layout will still be played",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function checkout(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
// Can't checkout a Layout which can already be edited
if ($layout->isEditable()) {
throw new InvalidArgumentException(__('Layout is already checked out'), 'statusId');
}
// Checkout this Layout
$draft = $this->layoutFactory->checkoutLayout($layout);
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Checked out %s'), $layout->layout),
'id' => $draft->layoutId,
'data' => $draft
]);
return $this->render($request, $response);
}
/**
* Publish Layout Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function publishForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
$data = ['layout' => $layout];
$this->getState()->template = 'layout-form-publish';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Publish Layout
*
* @SWG\Put(
* path="/layout/publish/{layoutId}",
* operationId="layoutPublish",
* tags={"layout"},
* summary="Publish Layout",
* description="Publish a Layout, discarding the original",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="publishNow",
* in="formData",
* description="Flag, indicating whether to publish layout now",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="publishDate",
* in="formData",
* description="The date/time at which layout should be published",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function publish(Request $request, Response $response, $id)
{
Profiler::start('Layout::publish', $this->getLog());
$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'));
}
// if we have publish date update it in database
if (isset($publishDate) && !$publishNow) {
$layout->setPublishedDate($publishDate);
}
// 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();
// Make sure actions from all levels are valid before allowing publish
// Layout Actions
foreach ($draft->actions as $action) {
$action->validate();
}
/** @var Region[] $allRegions */
$allRegions = array_merge($draft->regions, $draft->drawers);
// Region Actions
foreach ($allRegions as $region) {
// Interactive Actions on Region
foreach ($region->actions as $action) {
$action->validate();
}
// Widget Actions
foreach ($region->getPlaylist()->widgets as $widget) {
// Interactive Actions on Widget
foreach ($widget->actions as $action) {
$action->validate();
}
}
}
// 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
]);
}
Profiler::end('Layout::publish', $this->getLog());
} finally {
// Release lock
$this->layoutFactory->concurrentRequestRelease($layout, true);
}
return $this->render($request, $response);
}
/**
* Discard Layout Form
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function discardForm(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
$data = ['layout' => $layout];
$this->getState()->template = 'layout-form-discard';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Discard Layout
*
* @SWG\Put(
* path="/layout/discard/{layoutId}",
* operationId="layoutDiscard",
* tags={"layout"},
* summary="Discard Layout",
* description="Discard a Layout restoring the original",
* @SWG\Parameter(
* name="layoutId",
* in="path",
* description="The Layout ID",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function discard(Request $request, Response $response, $id)
{
$layout = $this->layoutFactory->getById($id);
// Make sure we have permission
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException(__('You do not have permissions to edit this layout'));
}
// Make sure the Layout is checked out to begin with
if (!$layout->isEditable()) {
throw new InvalidArgumentException(__('Layout is not checked out'), 'statusId');
}
$draft = $this->layoutFactory->getByParentId($id);
$draft->discardDraft();
// The parent is no longer a draft
$layout->publishedStatusId = 1;
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Discarded %s'), $draft->layout),
'data' => $layout
]);
return $this->render($request, $response);
}
/**
* Query the Database for all Code identifiers assigned to Layouts.
*
* @param Request $request
* @param Response $response
* @return Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function getLayoutCodes(Request $request, Response $response)
{
$parsedParams = $this->getSanitizer($request->getQueryParams());
$codes = $this->layoutFactory->getLayoutCodes($this->gridRenderFilter([
'code' => $parsedParams->getString('code')
], $parsedParams));
// Store the table rows
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->layoutFactory->countLast();
$this->getState()->setData($codes);
return $this->render($request, $response);
}
/**
* Release the Layout Lock on specified layoutId
* Available only to the User that currently has the Layout locked.
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws InvalidArgumentException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function releaseLock(Request $request, Response $response, $id)
{
/** @var Item $lock */
$lock = $this->pool->getItem('locks/layout/' . $id);
$lockUserId = $lock->get()->userId;
if ($this->getUser()->userId !== $lockUserId) {
throw new InvalidArgumentException(__('This function is available only to User who originally locked this Layout.'));
}
$lock->set([]);
$lock->save();
return $this->render($request, $response);
}
/**
* Add a thumbnail
* @param Request $request
* @param Response $response
* @param $id
* @return Response
* @throws \Xibo\Support\Exception\AccessDeniedException
* @throws \Xibo\Support\Exception\InvalidArgumentException
* @throws \Xibo\Support\Exception\NotFoundException
* @throws \Xibo\Support\Exception\ConfigurationException
*/
public function addThumbnail(Request $request, Response $response, $id): Response
{
$libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
MediaService::ensureLibraryExists($libraryLocation);
// Check the Layout
$layout = $this->layoutFactory->getById($id);
// Make sure we have edit permissions
if (!$this->getUser()->checkEditable($layout)) {
throw new AccessDeniedException();
}
// Where would we save this to?
if ($layout->isChild()) {
// A draft
$saveTo = $libraryLocation . 'thumbs/' . $layout->campaignId . '_layout_thumb.png';
} else {
// Published
// we would usually expect this to be copied over when published.
$saveTo = $libraryLocation . 'thumbs/' . $layout->campaignId . '_campaign_thumb.png';
}
// Load this Layout
$layout->load();
// Create a thumbnail image
try {
Img::configure(['driver' => 'gd']);
if ($layout->backgroundImageId !== null && $layout->backgroundImageId !== 0) {
// Start from a background image
$media = $this->mediaFactory->getById($layout->backgroundImageId);
$image = Img::make($libraryLocation . $media->storedAs);
// Resize this image (without cropping it) to the size of this layout
$image->resize($layout->width, $layout->height);
} else {
// Start from a Canvas
$image = Img::canvas($layout->width, $layout->height, $layout->backgroundColor);
}
$countRegions = count($layout->regions);
// Draw some regions on it.
foreach ($layout->regions as $region) {
try {
// We don't do this for the canvas region.
if ($countRegions > 1 && $region->type === 'canvas') {
continue;
}
// Get widgets in this region
$playlist = $region->getPlaylist()->setModuleFactory($this->moduleFactory);
$widgets = $playlist->expandWidgets();
if (count($widgets) <= 0) {
// Render the region (draw a grey box)
$image->rectangle(
$region->left,
$region->top,
$region->left + $region->width,
$region->top + $region->height,
function ($draw) {
$draw->background('rgba(196, 196, 196, 0.6)');
}
);
if ($region->width >= 400) {
$image->text(
__('Empty Region'),
$region->left + ($region->width / 2),
$region->top + ($region->height / 2),
function ($font) {
$font->file(PROJECT_ROOT . '/web/theme/default/fonts/Railway.ttf');
$font->size(84);
$font->color('#000000');
$font->align('center');
$font->valign('center');
}
);
}
} else {
// Render just the first widget in the appropriate place
$widget = $widgets[0];
if ($widget->type === 'image') {
$media = $this->mediaFactory->getById($widget->getPrimaryMediaId());
$cover = Img::make($libraryLocation . $media->storedAs);
$proportional = $widget->getOptionValue('scaleType', 'stretch') !== 'stretch';
$fit = $widget->getOptionValue('scaleType', 'stretch') === 'fit';
if ($fit) {
$cover->fit($region->width, $region->height);
} else {
$cover->resize(
$region->width,
$region->height,
function ($constraint) use ($proportional) {
if ($proportional) {
$constraint->aspectRatio();
}
}
);
}
if ($proportional) {
$cover->resizeCanvas($region->width, $region->height);
}
$image->insert($cover, 'top-left', $region->left, $region->top);
} else if ($widget->type === 'video'
&& file_exists($libraryLocation . $widget->getPrimaryMediaId() . '_videocover.png')
) {
// Render the video cover
$cover = Img::make($libraryLocation . $widget->getPrimaryMediaId() . '_videocover.png');
$cover->resize($region->width, $region->height, function ($constraint) {
$constraint->aspectRatio();
});
$cover->resizeCanvas($region->width, $region->height);
$image->insert($cover, 'top-left', $region->left, $region->top);
} else {
// Draw the region in the widget colouring
$image->rectangle(
$region->left,
$region->top,
$region->left + $region->width,
$region->top + $region->height,
function ($draw) {
$draw->background('rgba(196, 196, 196, 0.6)');
}
);
$module = $this->moduleFactory->getByType($widget->type);
if ($region->width >= 400) {
$image->text(
$widget->getOptionValue('name', $module->name),
$region->left + ($region->width / 2),
$region->top + ($region->height / 2),
function ($font) {
$font->file(PROJECT_ROOT . '/web/theme/default/fonts/Railway.ttf');
$font->size(84);
$font->color('#000000');
$font->align('center');
$font->valign('center');
}
);
}
}
// Put a number of widgets counter in the bottom
$image->text(
'1 / ' . count($widgets),
$region->left + $region->width - 10,
$region->top + $region->height - 10,
function ($font) {
$font->file(PROJECT_ROOT . '/web/theme/default/fonts/Railway.ttf');
$font->size(36);
$font->color('#000000');
$font->align('right');
$font->valign('bottom');
}
);
}
} catch (\Exception $e) {
$this->getLog()->error('Problem generating region in thumbnail. e: ' . $e->getMessage());
}
}
// Resize the entire layout down to a thumbnail
$image->widen(1080);
// Save the file
$image->save($saveTo);
return $response->withStatus(204);
} catch (\Exception $e) {
$this->getLog()->error('Exception adding thumbnail to Layout. e = ' . $e->getMessage());
throw new InvalidArgumentException(__('Incorrect image data'));
}
}
/**
* Download the Layout Thumbnail
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function downloadThumbnail(Request $request, Response $response, $id)
{
$this->getLog()->debug('Layout thumbnail request for layoutId ' . $id);
$layout = $this->layoutFactory->getById($id);
if (!$this->getUser()->checkViewable($layout)) {
throw new AccessDeniedException();
}
// Get thumbnail uri
$uri = $layout->getThumbnailUri();
if (!file_exists($uri)) {
throw new NotFoundException(__('Thumbnail not found for Layout'));
}
$response = $response
->withHeader('Content-Length', filesize($uri))
->withHeader('Content-Type', (new MimeTypes())->getMimeType('png'));
$sendFileMode = $this->getConfig()->getSetting('SENDFILE_MODE');
if ($sendFileMode == 'Apache') {
$response = $response->withHeader('X-Sendfile', $uri);
} else if ($sendFileMode == 'Nginx') {
$response = $response->withHeader('X-Accel-Redirect', '/download/thumbs/' . basename($uri));
} else {
// Return the file with PHP
$response = $response->withBody(new Stream(fopen($uri, 'r')));
}
$this->setNoOutput();
return $this->render($request, $response);
}
/**
* Create a Layout with full screen Region with Media/Playlist specific Widget
* This is called as a first step when scheduling Media/Playlist eventType
* @SWG\Post(
* path="/layout/fullscreen",
* operationId="layoutAddFullScreen",
* tags={"layout"},
* summary="Add a Full Screen Layout",
* description="Add a new full screen Layout with specified Media/Playlist",
* @SWG\Parameter(
* name="id",
* in="formData",
* description="The Media or Playlist ID that should be added to this Layout",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="type",
* in="formData",
* description="The type of Layout to be created = media or playlist",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="resolutionId",
* in="formData",
* description="The Id of the resolution for this Layout, defaults to 1080p for playlist and closest resolution match for Media",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="backgroundColor",
* in="formData",
* description="A HEX color to use as the background color of this Layout. Default is black #000",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="layoutDuration",
* in="formData",
* description="Use with media type, to specify the duration this Media should play in one loop",
* type="boolean",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Layout"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
* @param Request $request
* @param Response $response
* @return Response|ResponseInterface
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
*/
public function createFullScreenLayout(Request $request, Response $response): Response|ResponseInterface
{
$params = $this->getSanitizer($request->getParams());
$type = $params->getString('type');
$id = $params->getInt('id');
$resolutionId = $params->getInt('resolutionId');
$backgroundColor = $params->getString('backgroundColor');
$duration = $params->getInt('layoutDuration');
if (empty($id)) {
throw new InvalidArgumentException(sprintf(__('Please select %s'), ucfirst($type)));
}
// We only create fullscreen layout from media files or playlist
if (!in_array($type, ['media', 'playlist'], true)) {
throw new InvalidArgumentException(__('Invalid type'));
}
$fullscreenLayout = $this->layoutFactory->createFullScreenLayout(
$type,
$id,
$resolutionId,
$backgroundColor,
$duration
);
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => sprintf(__('Created %s'), $fullscreenLayout->layout),
'data' => $fullscreenLayout
]);
return $this->render($request, $response);
}
}