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

3008 lines
106 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\Client;
use Illuminate\Support\Str;
use Intervention\Image\ImageManagerStatic as Img;
use Psr\Http\Message\ResponseInterface;
use Respect\Validation\Validator as v;
use Slim\Http\Response as Response;
use Slim\Http\ServerRequest as Request;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Xibo\Connector\ProviderDetails;
use Xibo\Connector\ProviderImport;
use Xibo\Entity\Media;
use Xibo\Entity\SearchResult;
use Xibo\Entity\SearchResults;
use Xibo\Event\LibraryProviderEvent;
use Xibo\Event\LibraryProviderImportEvent;
use Xibo\Event\LibraryProviderListEvent;
use Xibo\Event\MediaDeleteEvent;
use Xibo\Event\MediaFullLoadEvent;
use Xibo\Factory\DisplayFactory;
use Xibo\Factory\FolderFactory;
use Xibo\Factory\LayoutFactory;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\PermissionFactory;
use Xibo\Factory\PlaylistFactory;
use Xibo\Factory\ScheduleFactory;
use Xibo\Factory\TagFactory;
use Xibo\Factory\UserFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Helper\ByteFormatter;
use Xibo\Helper\DateFormatHelper;
use Xibo\Helper\Environment;
use Xibo\Helper\HttpsDetect;
use Xibo\Helper\LinkSigner;
use Xibo\Helper\XiboUploadHandler;
use Xibo\Service\MediaService;
use Xibo\Service\MediaServiceInterface;
use Xibo\Support\Exception\AccessDeniedException;
use Xibo\Support\Exception\ConfigurationException;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\LibraryFullException;
use Xibo\Support\Exception\NotFoundException;
use Xibo\Widget\Render\WidgetDownloader;
/**
* Class Library
* @package Xibo\Controller
*/
class Library extends Base
{
/** @var EventDispatcherInterface */
private $dispatcher;
/**
* @var UserFactory
*/
private $userFactory;
/**
* @var ModuleFactory
*/
private $moduleFactory;
/**
* @var TagFactory
*/
private $tagFactory;
/**
* @var MediaFactory
*/
private $mediaFactory;
/**
* @var WidgetFactory
*/
private $widgetFactory;
/**
* @var PlaylistFactory
*/
private $playlistFactory;
/**
* @var LayoutFactory
*/
private $layoutFactory;
/**
* @var PermissionFactory
*/
private $permissionFactory;
/**
* @var UserGroupFactory
*/
private $userGroupFactory;
/** @var DisplayFactory */
private $displayFactory;
/** @var ScheduleFactory */
private $scheduleFactory;
/** @var FolderFactory */
private $folderFactory;
/**
* @var MediaServiceInterface
*/
private $mediaService;
/**
* Set common dependencies.
* @param UserFactory $userFactory
* @param ModuleFactory $moduleFactory
* @param TagFactory $tagFactory
* @param MediaFactory $mediaFactory
* @param WidgetFactory $widgetFactory
* @param PermissionFactory $permissionFactory
* @param LayoutFactory $layoutFactory
* @param PlaylistFactory $playlistFactory
* @param UserGroupFactory $userGroupFactory
* @param DisplayFactory $displayFactory
* @param ScheduleFactory $scheduleFactory
* @param FolderFactory $folderFactory
*/
public function __construct(
$userFactory,
$moduleFactory,
$tagFactory,
$mediaFactory,
$widgetFactory,
$permissionFactory,
$layoutFactory,
$playlistFactory,
$userGroupFactory,
$displayFactory,
$scheduleFactory,
$folderFactory
) {
$this->moduleFactory = $moduleFactory;
$this->mediaFactory = $mediaFactory;
$this->widgetFactory = $widgetFactory;
$this->userFactory = $userFactory;
$this->tagFactory = $tagFactory;
$this->permissionFactory = $permissionFactory;
$this->layoutFactory = $layoutFactory;
$this->playlistFactory = $playlistFactory;
$this->userGroupFactory = $userGroupFactory;
$this->displayFactory = $displayFactory;
$this->scheduleFactory = $scheduleFactory;
$this->folderFactory = $folderFactory;
}
/**
* Get Module Factory
* @return ModuleFactory
*/
public function getModuleFactory()
{
return $this->moduleFactory;
}
/**
* Get Media Factory
* @return MediaFactory
*/
public function getMediaFactory()
{
return $this->mediaFactory;
}
/**
* Get Permission Factory
* @return PermissionFactory
*/
public function getPermissionFactory()
{
return $this->permissionFactory;
}
/**
* Get Widget Factory
* @return WidgetFactory
*/
public function getWidgetFactory()
{
return $this->widgetFactory;
}
/**
* Get Layout Factory
* @return LayoutFactory
*/
public function getLayoutFactory()
{
return $this->layoutFactory;
}
/**
* Get Playlist Factory
* @return PlaylistFactory
*/
public function getPlaylistFactory()
{
return $this->playlistFactory;
}
/**
* @return TagFactory
*/
public function getTagFactory()
{
return $this->tagFactory;
}
/**
* @return FolderFactory
*/
public function getFolderFactory()
{
return $this->folderFactory;
}
public function useMediaService(MediaServiceInterface $mediaService)
{
$this->mediaService = $mediaService;
}
public function getMediaService()
{
return $this->mediaService->setUser($this->getUser());
}
/**
* Displays the page logic
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function displayPage(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getQueryParams());
$mediaId = $sanitizedParams->getInt('mediaId');
if ($mediaId !== null) {
$media = $this->mediaFactory->getById($mediaId);
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
// Thumbnail
$module = $this->moduleFactory->getByType($media->mediaType);
$media->setUnmatchedProperty('thumbnail', '');
if ($module->hasThumbnail) {
$media->setUnmatchedProperty(
'thumbnail',
$this->urlFor($request, 'library.download', [
'id' => $media->mediaId
], [
'preview' => 1
])
);
}
$media->setUnmatchedProperty('fileSizeFormatted', ByteFormatter::format($media->fileSize));
$this->getState()->template = 'library-direct-media-details';
$this->getState()->setData([
'media' => $media
]);
} else {
// Users we have permission to see
$this->getState()->template = 'library-page';
$this->getState()->setData([
'modules' => $this->moduleFactory->getLibraryModules(),
'validExt' => implode('|', $this->moduleFactory->getValidExtensions([]))
]);
}
return $this->render($request, $response);
}
/**
* Set Enable Stats Collection of a media
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @SWG\Put(
* path="/library/setenablestat/{mediaId}",
* operationId="mediaSetEnableStat",
* tags={"library"},
* summary="Enable Stats Collection",
* description="Set Enable Stats Collection? to use for the collection of Proof of Play statistics for a media.",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media ID",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="enableStat",
* in="formData",
* description="The option to enable the collection of Media Proof of Play statistics",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function setEnableStat(Request $request, Response $response, $id)
{
// Get the Media
$media = $this->mediaFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
$enableStat = $this->getSanitizer($request->getParams())->getString('enableStat');
$media->enableStat = $enableStat;
$media->save(['saveTags' => false]);
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('For Media %s Enable Stats Collection is set to %s'), $media->name, __($media->enableStat))
]);
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)
{
// Get the Media
$media = $this->mediaFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
$data = [
'media' => $media,
];
$this->getState()->template = 'library-form-setenablestat';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* Prints out a Table of all media items
*
* @SWG\Get(
* path="/library",
* operationId="librarySearch",
* tags={"library"},
* summary="Library Search",
* description="Search the Library for this user",
* @SWG\Parameter(
* name="mediaId",
* in="query",
* description="Filter by Media Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="media",
* in="query",
* description="Filter by Media Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="type",
* in="query",
* description="Filter by Media Type",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ownerId",
* in="query",
* description="Filter by Owner Id",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="retired",
* in="query",
* description="Filter by Retired",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="tags",
* in="query",
* description="Filter by Tags - comma seperated",
* 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="duration",
* in="query",
* description="Filter by Duration - a number or less-than,greater-than,less-than-equal or great-than-equal followed by a | followed by a number",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="fileSize",
* in="query",
* description="Filter by File Size - a number or less-than,greater-than,less-than-equal or great-than-equal followed by a | followed by a number",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="ownerUserGroupId",
* in="query",
* description="Filter by users in this UserGroupId",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="query",
* description="Filter by Folder ID",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="isReturnPublicUrls",
* in="query",
* description="Should the thumbail URLs be authenticated S3 style public URL, default = false",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/Media")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function grid(Request $request, Response $response)
{
$user = $this->getUser();
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
// Variables used for link signing
$isReturnPublicUrls = $parsedQueryParams->getCheckbox('isReturnPublicUrls') == 1;
$thumbnailRouteName = $isReturnPublicUrls ? 'library.public.thumbnail' : 'library.thumbnail';
$encryptionKey = $this->getConfig()->getApiKeyDetails()['encryptionKey'];
$rootUrl = (new HttpsDetect())->getUrl();
// Construct the SQL
$mediaList = $this->mediaFactory->query($this->gridRenderSort($parsedQueryParams), $this->gridRenderFilter([
'mediaId' => $parsedQueryParams->getInt('mediaId'),
'name' => $parsedQueryParams->getString('media'),
'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
'nameExact' => $parsedQueryParams->getString('nameExact'),
'type' => $parsedQueryParams->getString('type'),
'types' => $parsedQueryParams->getArray('types'),
'tags' => $parsedQueryParams->getString('tags'),
'exactTags' => $parsedQueryParams->getCheckbox('exactTags'),
'ownerId' => $parsedQueryParams->getInt('ownerId'),
'retired' => $parsedQueryParams->getInt('retired'),
'duration' => $parsedQueryParams->getInt('duration'),
'fileSize' => $parsedQueryParams->getString('fileSize'),
'ownerUserGroupId' => $parsedQueryParams->getInt('ownerUserGroupId'),
'assignable' => $parsedQueryParams->getInt('assignable'),
'folderId' => $parsedQueryParams->getInt('folderId'),
'onlyMenuBoardAllowed' => $parsedQueryParams->getInt('onlyMenuBoardAllowed'),
'layoutId' => $parsedQueryParams->getInt('layoutId'),
'includeLayoutBackgroundImage' => ($parsedQueryParams->getInt('layoutId') != null) ? 1 : 0,
'orientation' => $parsedQueryParams->getString('orientation', ['defaultOnEmptyString' => true]),
'logicalOperator' => $parsedQueryParams->getString('logicalOperator'),
'logicalOperatorName' => $parsedQueryParams->getString('logicalOperatorName'),
'unreleasedOnly' => $parsedQueryParams->getCheckbox('unreleasedOnly'),
'unusedOnly' => $parsedQueryParams->getCheckbox('unusedOnly'),
], $parsedQueryParams));
// Add some additional row content
foreach ($mediaList as $media) {
$media->setUnmatchedProperty('revised', ($media->parentId != 0) ? 1 : 0);
// Thumbnail
$media->setUnmatchedProperty('thumbnail', '');
try {
$module = $this->moduleFactory->getByType($media->mediaType);
if ($module->hasThumbnail) {
$renderThumbnail = true;
// for video, check if the cover image exists here.
if ($media->mediaType === 'video') {
$libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
$renderThumbnail = file_exists($libraryLocation . $media->mediaId . '_videocover.png');
}
if ($renderThumbnail) {
$thumbnailUrl = $this->urlFor($request, $thumbnailRouteName, [
'id' => $media->mediaId,
]);
if ($isReturnPublicUrls) {
// If we are coming from the API we should remove the /api part of the URL
if ($this->isApi($request)) {
$thumbnailUrl = str_replace('/api/', '/', $thumbnailUrl);
}
// Sign the link.
$thumbnailUrl = $rootUrl . $thumbnailUrl . '?' . LinkSigner::getSignature(
$rootUrl,
$thumbnailUrl,
time() + 3600,
$encryptionKey,
);
}
$media->setUnmatchedProperty('thumbnail', $thumbnailUrl);
}
}
} catch (NotFoundException) {
$this->getLog()->error('Module ' . $media->mediaType . ' not found');
}
$media->setUnmatchedProperty('fileSizeFormatted', ByteFormatter::format($media->fileSize));
// Media expiry
$media->setUnmatchedProperty('mediaExpiresIn', __('Expires %s'));
$media->setUnmatchedProperty('mediaExpiryFailed', __('Expired '));
$media->setUnmatchedProperty('mediaNoExpiryDate', __('Never'));
if ($this->isApi($request)) {
$media->excludeProperty('mediaExpiresIn');
$media->excludeProperty('mediaExpiryFailed');
$media->excludeProperty('mediaNoExpiryDate');
$media->expires = ($media->expires == 0)
? 0
: Carbon::createFromTimestamp($media->expires)->format(DateFormatHelper::getSystemFormat());
continue;
}
$media->includeProperty('buttons');
switch ($media->released) {
case 1:
$media->setUnmatchedProperty('releasedDescription', '');
break;
case 2:
$media->setUnmatchedProperty(
'releasedDescription',
__('The uploaded image is too large and cannot be processed, please use another image.')
);
break;
default:
$media->setUnmatchedProperty(
'releasedDescription',
__('This image will be resized according to set thresholds and limits.')
);
}
switch ($media->enableStat) {
case 'On':
$media->setUnmatchedProperty(
'enableStatDescription',
__('This Media has enable stat collection set to ON')
);
break;
case 'Off':
$media->setUnmatchedProperty(
'enableStatDescription',
__('This Media has enable stat collection set to OFF')
);
break;
default:
$media->setUnmatchedProperty(
'enableStatDescription',
__('This Media has enable stat collection set to INHERIT')
);
}
if ($parsedQueryParams->getCheckbox('fullScreenScheduleCheck')) {
$fullScreenCampaignId = $this->hasFullScreenLayout($media);
$media->setUnmatchedProperty('hasFullScreenLayout', (!empty($fullScreenCampaignId)));
$media->setUnmatchedProperty('fullScreenCampaignId', $fullScreenCampaignId);
}
$media->buttons = [];
// Buttons
if ($this->getUser()->featureEnabled('library.modify')
&& $user->checkEditable($media)
) {
// Edit
$media->buttons[] = array(
'id' => 'content_button_edit',
'url' => $this->urlFor($request, 'library.edit.form', ['id' => $media->mediaId]),
'text' => __('Edit')
);
// Copy Button
$media->buttons[] = array(
'id' => 'media_button_copy',
'url' => $this->urlFor($request, 'library.copy.form', ['id' => $media->mediaId]),
'text' => __('Copy')
);
// Select Folder
if ($this->getUser()->featureEnabled('folder.view')) {
$media->buttons[] = [
'id' => 'library_button_selectfolder',
'url' => $this->urlFor($request, 'library.selectfolder.form', ['id' => $media->mediaId]),
'text' => __('Select Folder'),
'multi-select' => true,
'dataAttributes' => [
[
'name' => 'commit-url', 'value' => $this->urlFor($request, 'library.selectfolder', [
'id' => $media->mediaId
])
],
['name' => 'commit-method', 'value' => 'put'],
['name' => 'id', 'value' => 'library_button_selectfolder'],
['name' => 'text', 'value' => __('Move to Folder')],
['name' => 'rowtitle', 'value' => $media->name],
['name' => 'form-callback', 'value' => 'moveFolderMultiSelectFormOpen']
]
];
}
}
if ($this->getUser()->featureEnabled('library.modify')
&& $user->checkDeleteable($media)
) {
// Delete Button
$media->buttons[] = [
'id' => 'content_button_delete',
'url' => $this->urlFor($request,'library.delete.form', ['id' => $media->mediaId]),
'text' => __('Delete'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request,'library.delete', ['id' => $media->mediaId])],
['name' => 'commit-method', 'value' => 'delete'],
['name' => 'id', 'value' => 'content_button_delete'],
['name' => 'text', 'value' => __('Delete')],
['name' => 'sort-group', 'value' => 1],
['name' => 'rowtitle', 'value' => $media->name],
['name' => 'form-callback', 'value' => 'setDefaultMultiSelectFormOpen']
]
];
}
if ($this->getUser()->featureEnabled('library.modify')
&& $user->checkPermissionsModifyable($media)
) {
// Permissions
$media->buttons[] = [
'id' => 'content_button_permissions',
'url' => $this->urlFor($request,'user.permissions.form', ['entity' => 'Media', 'id' => $media->mediaId]),
'text' => __('Share'),
'multi-select' => true,
'dataAttributes' => [
['name' => 'commit-url', 'value' => $this->urlFor($request,'user.permissions.multi', ['entity' => 'Media', 'id' => $media->mediaId])],
['name' => 'commit-method', 'value' => 'post'],
['name' => 'id', 'value' => 'content_button_permissions'],
['name' => 'text', 'value' => __('Share')],
['name' => 'rowtitle', 'value' => $media->name],
['name' => 'sort-group', 'value' => 2],
['name' => 'custom-handler', 'value' => 'XiboMultiSelectPermissionsFormOpen'],
['name' => 'custom-handler-url', 'value' => $this->urlFor($request,'user.permissions.multi.form', ['entity' => 'Media'])],
['name' => 'content-id-name', 'value' => 'mediaId']
]
];
}
// Download
// No feature permissions here, anyone can get a file based on sharing.
$media->buttons[] = ['divider' => true];
$media->buttons[] = array(
'id' => 'content_button_download',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request, 'library.download', ['id' => $media->mediaId]) . '?attachment=' . urlencode($media->fileName),
'text' => __('Download')
);
// Set Enable Stat
if ($this->getUser()->featureEnabled('library.modify')
&& $this->getUser()->checkEditable($media)
) {
$media->buttons[] = ['divider' => true];
$media->buttons[] = array(
'id' => 'library_button_setenablestat',
'url' => $this->urlFor($request,'library.setenablestat.form', ['id' => $media->mediaId]),
'text' => __('Enable stats collection?'),
'multi-select' => true,
'dataAttributes' => array(
array('name' => 'commit-url', 'value' => $this->urlFor($request,'library.setenablestat', ['id' => $media->mediaId])),
array('name' => 'commit-method', 'value' => 'put'),
array('name' => 'id', 'value' => 'library_button_setenablestat'),
array('name' => 'text', 'value' => __('Enable stats collection?')),
array('name' => 'rowtitle', 'value' => $media->name),
['name' => 'form-callback', 'value' => 'setEnableStatMultiSelectFormOpen']
)
);
}
if ($this->getUser()->featureEnabled(['schedule.view', 'layout.view'])) {
$media->buttons[] = ['divider' => true];
$media->buttons[] = array(
'id' => 'usage_report_button',
'url' => $this->urlFor($request, 'library.usage.form', ['id' => $media->mediaId]),
'text' => __('Usage Report')
);
}
// Schedule
if ($this->getUser()->featureEnabled('schedule.add')
&& in_array($media->mediaType, ['image', 'video'])
&& ($this->getUser()->checkEditable($media)
|| $this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 1)
) {
$media->buttons[] = [
'id' => 'library_button_schedule',
'url' => $this->urlFor(
$request,
'schedule.add.form',
['id' => $media->mediaId, 'from' => 'Library']
),
'text' => __('Schedule')
];
}
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->mediaFactory->countLast();
$this->getState()->setData($mediaList);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/library/search",
* operationId="librarySearchAll",
* tags={"library"},
* summary="Library Search All",
* description="Search all library files from local and connectors",
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(
* type="array",
* @SWG\Items(ref="#/definitions/SearchResult")
* )
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws NotFoundException
*/
public function search(Request $request, Response $response): Response
{
$parsedQueryParams = $this->getSanitizer($request->getQueryParams());
$provider = $parsedQueryParams->getString('provider', ['default' => 'local']);
$searchResults = new SearchResults();
if ($provider === 'local') {
// Sorting options.
// only allow from a preset list
$sortCol = match ($parsedQueryParams->getString('sortCol')) {
'mediaId' => '`media`.`mediaId`',
'orientation' => '`media`.`orientation`',
'width' => '`media`.`width`',
'height' => '`media`.`height`',
'duration' => '`media`.`duration`',
'fileSize' => '`media`.`fileSize`',
'createdDt' => '`media`.`createdDt`',
'modifiedDt' => '`media`.`modifiedDt`',
default => '`media`.`name`',
};
$sortDir = match ($parsedQueryParams->getString('sortDir')) {
'DESC' => ' DESC',
default => ' ASC'
};
$mediaList = $this->mediaFactory->query([$sortCol . $sortDir], $this->gridRenderFilter([
'name' => $parsedQueryParams->getString('media'),
'useRegexForName' => $parsedQueryParams->getCheckbox('useRegexForName'),
'nameExact' => $parsedQueryParams->getString('nameExact'),
'type' => $parsedQueryParams->getString('type'),
'types' => $parsedQueryParams->getArray('types'),
'tags' => $parsedQueryParams->getString('tags'),
'exactTags' => $parsedQueryParams->getCheckbox('exactTags'),
'ownerId' => $parsedQueryParams->getInt('ownerId'),
'folderId' => $parsedQueryParams->getInt('folderId'),
'assignable' => 1,
'retired' => 0,
'orientation' => $parsedQueryParams->getString('orientation', ['defaultOnEmptyString' => true])
], $parsedQueryParams));
// Add some additional row content
foreach ($mediaList as $media) {
$searchResult = new SearchResult();
$searchResult->id = $media->mediaId;
$searchResult->source = 'local';
$searchResult->type = $media->mediaType;
$searchResult->title = $media->name;
$searchResult->width = $media->width;
$searchResult->height = $media->height;
$searchResult->description = '';
$searchResult->duration = $media->duration;
// Thumbnail
$module = $this->moduleFactory->getByType($media->mediaType);
if ($module->hasThumbnail) {
$searchResult->thumbnail = $this->urlFor($request, 'library.download', [
'id' => $media->mediaId
], [
'preview' => 1,
'isThumb' => 1
]);
}
// Add the result
$searchResults->data[] = $searchResult;
}
} else {
$this->getLog()->debug('Dispatching event, for provider ' . $provider);
// Do we have a type filter
$types = $parsedQueryParams->getArray('types');
$type = $parsedQueryParams->getString('type');
if ($type !== null) {
$types[] = $type;
}
// Hand off to any other providers that may want to provide results.
$event = new LibraryProviderEvent(
$searchResults,
$parsedQueryParams->getInt('start', ['default' => 0]),
$parsedQueryParams->getInt('length', ['default' => 10]),
$parsedQueryParams->getString('media'),
$types,
$parsedQueryParams->getString('orientation'),
$provider
);
try {
$this->getDispatcher()->dispatch($event, $event->getName());
} catch (\Exception $exception) {
$this->getLog()->error('Library search: Exception in dispatched event: ' . $exception->getMessage());
$this->getLog()->debug($exception->getTraceAsString());
}
}
return $response->withJson($searchResults);
}
/**
* Get list of Library providers with their details.
*
* @param Request $request
* @param Response $response
* @return Response|ResponseInterface
*/
public function providersList(Request $request, Response $response): Response|\Psr\Http\Message\ResponseInterface
{
$event = new LibraryProviderListEvent();
$this->getDispatcher()->dispatch($event, $event->getName());
$providers = $event->getProviders();
return $response->withJson($providers);
}
/**
* Media Delete 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 deleteForm(Request $request, Response $response, $id)
{
$media = $this->mediaFactory->getById($id);
if (!$this->getUser()->checkDeleteable($media)) {
throw new AccessDeniedException();
}
$this->getDispatcher()->dispatch(MediaFullLoadEvent::$NAME, new MediaFullLoadEvent($media));
$media->load(['deleting' => true]);
$this->getState()->template = 'library-form-delete';
$this->getState()->setData([
'media' => $media,
]);
return $this->render($request, $response);
}
/**
* Delete Media
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
* @SWG\Delete(
* path="/library/{mediaId}",
* operationId="libraryDelete",
* tags={"library"},
* summary="Delete Media",
* description="Delete Media from the Library",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media ID to Delete",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="forceDelete",
* in="formData",
* description="If the media item has been used should it be force removed from items that uses it?",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="purge",
* in="formData",
* description="Should this Media be added to the Purge List for all Displays?",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=204,
* description="successful operation"
* )
* )
*/
public function delete(Request $request, Response $response, $id)
{
$media = $this->mediaFactory->getById($id);
$params = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkDeleteable($media)) {
throw new AccessDeniedException();
}
// Check
$this->getDispatcher()->dispatch(new MediaFullLoadEvent($media), MediaFullLoadEvent::$NAME);
$media->load(['deleting' => true]);
if ($media->isUsed() && $params->getCheckbox('forceDelete') == 0) {
throw new InvalidArgumentException(__('This library item is in use.'));
}
$this->getDispatcher()->dispatch(
new MediaDeleteEvent($media, null, $params->getCheckbox('purge')),
MediaDeleteEvent::$NAME
);
// Delete
$media->delete();
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Deleted %s'), $media->name)
]);
return $this->render($request, $response);
}
/**
* Add a file to the library
* expects to be fed by the blueimp file upload handler
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @SWG\Post(
* path="/library",
* operationId="libraryAdd",
* tags={"library"},
* summary="Add Media",
* description="Add Media to the Library, optionally replacing an existing media item, optionally adding to a playlist.",
* @SWG\Parameter(
* name="files",
* in="formData",
* description="The Uploaded File",
* type="file",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Optional Media Name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="oldMediaId",
* in="formData",
* description="Id of an existing media file which should be replaced with the new upload",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="updateInLayouts",
* in="formData",
* description="Flag (0, 1), set to 1 to update this media in all layouts (use with oldMediaId) ",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="deleteOldRevisions",
* in="formData",
* description="Flag (0 , 1), to either remove or leave the old file revisions (use with oldMediaId)",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="tags",
* in="formData",
* description="Comma separated string of Tags that should be assigned to uploaded Media",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="expires",
* in="formData",
* description="Date in Y-m-d H:i:s format, will set expiration date on the uploaded Media",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="playlistId",
* in="formData",
* description="A playlistId to add this uploaded media to",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="widgetFromDt",
* in="formData",
* description="Date in Y-m-d H:i:s format, will set widget start date. Requires a playlistId.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="widgetToDt",
* in="formData",
* description="Date in Y-m-d H:i:s format, will set widget end date. Requires a playlistId.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="deleteOnExpiry",
* in="formData",
* description="Flag (0, 1), set to 1 to remove the Widget from the Playlist when the widgetToDt has been reached",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="applyToMedia",
* in="formData",
* description="Flag (0, 1), set to 1 to apply the widgetFromDt as the expiry date on the Media",
* type="integer",
* 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"
* )
* )
*/
public function add(Request $request, Response $response)
{
$parsedBody = $this->getSanitizer($request->getParams());
$options = $parsedBody->getArray('options', ['default' => []]);
// Folders
$folderId = $parsedBody->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
if ($parsedBody->getInt('playlistId') !== null) {
$playlist = $this->playlistFactory->getById($parsedBody->getInt('playlistId'));
if ($playlist->isDynamic === 1) {
throw new InvalidArgumentException(__('This Playlist is dynamically managed so cannot accept manual assignments.'), 'isDynamic');
}
}
$options = array_merge([
'oldMediaId' => null,
'updateInLayouts' => 0,
'deleteOldRevisions' => 0,
'allowMediaTypeChange' => 0
], $options);
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Handle any expiry date provided.
// this can come from the API via `expires` or via a widgetToDt
$expires = $parsedBody->getDate('expires');
$widgetFromDt = $parsedBody->getDate('widgetFromDt');
$widgetToDt = $parsedBody->getDate('widgetToDt');
// If applyToMedia has been selected, and we have a widgetToDt, then use that as our expiry
if ($widgetToDt !== null && $parsedBody->getCheckbox('applyToMedia', ['checkboxReturnInteger' => false])) {
$expires = $widgetToDt;
}
// Validate that this date is in the future.
if ($expires !== null && $expires->isBefore(Carbon::now())) {
throw new InvalidArgumentException(__('Cannot set Expiry date in the past'), 'expires');
}
// Make sure the library exists
MediaService::ensureLibraryExists($libraryFolder);
// Get Valid Extensions
if ($parsedBody->getInt('oldMediaId', ['default' => $options['oldMediaId']]) !== null) {
$media = $this->mediaFactory->getById($parsedBody->getInt('oldMediaId', ['default' => $options['oldMediaId']]));
$folderId = $media->folderId;
$validExt = $this->moduleFactory->getValidExtensions(['type' => $media->mediaType, 'allowMediaTypeChange' => $options['allowMediaTypeChange']]);
} else {
$validExt = $this->moduleFactory->getValidExtensions();
}
// Make sure there is room in the library
$libraryLimit = $this->getConfig()->getSetting('LIBRARY_SIZE_LIMIT_KB') * 1024;
$options = [
'userId' => $this->getUser()->userId,
'controller' => $this,
'oldMediaId' => $parsedBody->getInt('oldMediaId', ['default' => $options['oldMediaId']]),
'widgetId' => $parsedBody->getInt('widgetId'),
'updateInLayouts' => $parsedBody->getCheckbox('updateInLayouts', ['default' => $options['updateInLayouts']]),
'deleteOldRevisions' => $parsedBody->getCheckbox('deleteOldRevisions', ['default' => $options['deleteOldRevisions']]),
'allowMediaTypeChange' => $options['allowMediaTypeChange'],
'displayOrder' => $parsedBody->getInt('displayOrder'),
'playlistId' => $parsedBody->getInt('playlistId'),
'accept_file_types' => '/\.' . implode('|', $validExt) . '$/i',
'libraryLimit' => $libraryLimit,
'libraryQuotaFull' => ($libraryLimit > 0 && $this->getMediaService()->libraryUsage() > $libraryLimit),
'expires' => $expires === null ? null : $expires->format('U'),
'widgetFromDt' => $widgetFromDt === null ? null : $widgetFromDt->format('U'),
'widgetToDt' => $widgetToDt === null ? null : $widgetToDt->format('U'),
'deleteOnExpiry' => $parsedBody->getCheckbox('deleteOnExpiry', ['checkboxReturnInteger' => true]),
'oldFolderId' => $folderId,
];
// Output handled by UploadHandler
$this->setNoOutput(true);
$this->getLog()->debug('Hand off to Upload Handler with options: ' . json_encode($options));
// Hand off to the Upload Handler provided by jquery-file-upload
new XiboUploadHandler($libraryFolder . 'temp/', $this->getLog()->getLoggerInterface(), $options);
// Explicitly set the Content-Type header to application/json
$response = $response->withHeader('Content-Type', 'application/json');
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)
{
$media = $this->mediaFactory->getById($id);
if (!$this->getUser()->checkEditable($media)) {
throw new AccessDeniedException();
}
$media->enableStat = ($media->enableStat == null) ? $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT') : $media->enableStat;
$this->getState()->template = 'library-form-edit';
$this->getState()->setData([
'media' => $media,
'validExtensions' => implode('|', $this->moduleFactory->getValidExtensions(['type' => $media->mediaType])),
'expiryDate' => ($media->expires == 0 ) ? null : Carbon::createFromTimestamp($media->expires)->format(DateFormatHelper::getSystemFormat(), $media->expires)
]);
return $this->render($request, $response);
}
/**
* Edit Media
*
* @SWG\Put(
* path="/library/{mediaId}",
* operationId="libraryEdit",
* tags={"library"},
* summary="Edit Media",
* description="Edit a Media Item in the Library",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media ID to Edit",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="Media Item Name",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="duration",
* in="formData",
* description="The duration in seconds for this Media Item",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="retired",
* in="formData",
* description="Flag indicating if this media is retired",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="tags",
* in="formData",
* description="Comma separated list of Tags",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="updateInLayouts",
* in="formData",
* description="Flag indicating whether to update the duration in all Layouts the Media is assigned to",
* type="integer",
* required=false
* ),
* @SWG\Parameter(
* name="expires",
* in="formData",
* description="Date in Y-m-d H:i:s format, will set expiration date on the Media item",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this media should be assigned to",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Media")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function edit(Request $request, Response $response, $id)
{
$media = $this->mediaFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkEditable($media)) {
throw new AccessDeniedException();
}
if ($media->mediaType == 'font') {
throw new InvalidArgumentException(__('Sorry, Fonts do not have any editable properties.'));
}
$media->name = $sanitizedParams->getString('name');
$media->duration = $sanitizedParams->getInt('duration');
$media->retired = $sanitizedParams->getCheckbox('retired');
if ($this->getUser()->featureEnabled('tag.tagging')) {
if (is_array($sanitizedParams->getParam('tags'))) {
$tags = $this->tagFactory->tagsFromJson($sanitizedParams->getArray('tags'));
} else {
$tags = $this->tagFactory->tagsFromString($sanitizedParams->getString('tags'));
}
$media->updateTagLinks($tags);
}
$media->enableStat = $sanitizedParams->getString('enableStat');
$media->folderId = $sanitizedParams->getInt('folderId', ['default' => $media->folderId]);
$media->orientation = $sanitizedParams->getString('orientation', ['default' => $media->orientation]);
if ($media->hasPropertyChanged('folderId')) {
if ($media->folderId === 1) {
$this->checkRootFolderAllowSave();
}
$folder = $this->folderFactory->getById($media->folderId);
$media->permissionsFolderId = ($folder->getPermissionFolderId() == null)
? $folder->id
: $folder->getPermissionFolderId();
}
if ($sanitizedParams->getDate('expires') != null) {
if ($sanitizedParams->getDate('expires')->format('U') > Carbon::now()->format('U')) {
$media->expires = $sanitizedParams->getDate('expires')->format('U');
} else {
throw new InvalidArgumentException(__('Cannot set Expiry date in the past'), 'expires');
}
} else {
$media->expires = 0;
}
// Should we update the media in all layouts?
if ($sanitizedParams->getCheckbox('updateInLayouts') == 1
|| $media->hasPropertyChanged('enableStat')
) {
foreach ($this->widgetFactory->getByMediaId($media->mediaId, 0) as $widget) {
if ($widget->useDuration == 1) {
$widget->calculateDuration($this->moduleFactory->getByType($widget->type));
} else {
$widget->calculatedDuration = $media->duration;
}
$widget->save();
}
}
$media->save();
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Edited %s'), $media->name),
'id' => $media->mediaId,
'data' => $media
]);
return $this->render($request, $response);
}
/**
* Tidy Library
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ConfigurationException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function tidyForm(Request $request, Response $response)
{
if ($this->getConfig()->getSetting('SETTING_LIBRARY_TIDY_ENABLED') != 1) {
throw new ConfigurationException(__('Sorry this function is disabled.'));
}
// Work out how many files there are
$media = $this->mediaFactory->query(null, ['unusedOnly' => 1, 'ownerId' => $this->getUser()->userId]);
$sumExcludingGeneric = 0;
$countExcludingGeneric = 0;
$sumGeneric = 0;
$countGeneric = 0;
foreach ($media as $item) {
if ($item->mediaType == 'genericfile') {
$countGeneric++;
$sumGeneric = $sumGeneric + $item->fileSize;
}
else {
$countExcludingGeneric++;
$sumExcludingGeneric = $sumExcludingGeneric + $item->fileSize;
}
}
$this->getState()->template = 'library-form-tidy';
$this->getState()->setData([
'sumExcludingGeneric' => ByteFormatter::format($sumExcludingGeneric),
'sumGeneric' => ByteFormatter::format($sumGeneric),
'countExcludingGeneric' => $countExcludingGeneric,
'countGeneric' => $countGeneric,
]);
return $this->render($request, $response);
}
/**
* Tidies up the library
*
* @SWG\Delete(
* path="/library/tidy",
* operationId="libraryTidy",
* tags={"library"},
* summary="Tidy Library",
* description="Routine tidy of the library, removing unused files.",
* @SWG\Parameter(
* name="tidyGenericFiles",
* in="formData",
* description="Also delete generic files?",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws ConfigurationException
* @throws GeneralException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function tidy(Request $request, Response $response)
{
if ($this->getConfig()->getSetting('SETTING_LIBRARY_TIDY_ENABLED') != 1) {
throw new ConfigurationException(__('Sorry this function is disabled.'));
}
$tidyGenericFiles = $this->getSanitizer($request->getParams())->getCheckbox('tidyGenericFiles');
$this->getLog()->audit('Media', 0, 'Tidy library started', [
'tidyGenericFiles' => $tidyGenericFiles,
'initiator' => $this->getUser()->userId
]);
// Get a list of media that is not in use (for this user)
$media = $this->mediaFactory->query(null, ['unusedOnly' => 1, 'ownerId' => $this->getUser()->userId]);
$i = 0;
foreach ($media as $item) {
if ($tidyGenericFiles != 1 && $item->mediaType == 'genericfile') {
continue;
}
// Eligible for delete
$i++;
$this->getDispatcher()->dispatch(new MediaDeleteEvent($item), MediaDeleteEvent::$NAME);
$item->delete();
}
$this->getLog()->audit('Media', 0, 'Tidy library complete', [
'countDeleted' => $i,
'initiator' => $this->getUser()->userId
]);
// Return
$this->getState()->hydrate([
'message' => __('Library Tidy Complete'),
'countDeleted' => $i
]);
return $this->render($request, $response);
}
/**
* @return string
*/
public function getLibraryCacheUri()
{
return $this->getConfig()->getSetting('LIBRARY_LOCATION') . '/cache';
}
/**
* @SWG\Get(
* path="/library/download/{mediaId}/{type}",
* operationId="libraryDownload",
* tags={"library"},
* summary="Download Media",
* description="Download a Media file from the Library",
* produces={"application/octet-stream"},
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media ID to Download",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="type",
* in="path",
* description="The Module Type of the Download",
* type="string",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(type="file"),
* @SWG\Header(
* header="X-Sendfile",
* description="Apache Send file header - if enabled.",
* type="string"
* ),
* @SWG\Header(
* header="X-Accel-Redirect",
* description="nginx send file header - if enabled.",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function download(Request $request, Response $response, $id)
{
$this->setNoOutput();
// We can download by mediaId or by mediaName.
if (is_numeric($id)) {
$media = $this->mediaFactory->getById($id);
} else {
$media = $this->mediaFactory->getByName($id);
}
$this->getLog()->debug('download: Download request for mediaId ' . $id
. '. Media is a ' . $media->mediaType . ', is system file:' . $media->moduleSystemFile);
// Create the appropriate module
if ($media->mediaType === 'module') {
$module = $this->moduleFactory->getByType('image');
} else {
$module = $this->moduleFactory->getByType($media->mediaType);
}
// We are not able to download region specific modules
if ($module->regionSpecific == 1) {
throw new NotFoundException(__('Cannot download region specific module'));
}
// 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());
$params = $this->getSanitizer($request->getParams());
// Check if preview is allowed for the module
if ($params->getCheckbox('preview') == 1 && $module->allowPreview === 1) {
$this->getLog()->debug('download: preview mode, seeing if we can output an image/video');
// Output a 1px image if we're not allowed to see the media.
if (!$this->getUser()->checkViewable($media)) {
echo Img::make($this->getConfig()->uri('img/1x1.png', true))->encode();
return $this->render($request, $response->withHeader('Content-Type', 'image/png'));
}
// Various different behaviours for the different types of file.
if ($module->type === 'image') {
$response = $downloader->imagePreview(
$params,
$media->storedAs,
$response,
$this->getConfig()->uri('img/error.png', true),
);
} else if ($module->type === 'video') {
$response = $downloader->imagePreview(
$params,
$media->mediaId . '_videocover.png',
$response,
$this->getConfig()->uri('img/1x1.png', true),
);
} else {
$response = $downloader->download($media, $request, $response, $media->getMimeType());
}
} else {
$this->getLog()->debug('download: not preview mode, expect a full download');
// We are not a preview, and therefore we ought to check sharing before we download
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
$response = $downloader->download($media, $request, $response, null, $params->getString('attachment'));
}
return $this->render($request, $response);
}
/**
* Thumbnail for the libary page
* this is called by library-page datatable
*
* @SWG\Get(
* path="/library/thumbnail/{mediaId}",
* operationId="libraryThumbnail",
* tags={"library"},
* summary="Download Thumbnail",
* description="Download thumbnail for a Media file from the Library",
* produces={"application/octet-stream"},
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media ID to Download",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation",
* @SWG\Schema(type="file"),
* @SWG\Header(
* header="X-Sendfile",
* description="Apache Send file header - if enabled.",
* type="string"
* ),
* @SWG\Header(
* header="X-Accel-Redirect",
* description="nginx send file header - if enabled.",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\GeneralException
*/
public function thumbnail(Request $request, Response $response, $id, bool $isForceGrantAccess = false)
{
$this->setNoOutput();
// We can download by mediaId or by mediaName.
if (is_numeric($id)) {
$media = $this->mediaFactory->getById($id);
} else {
$media = $this->mediaFactory->getByName($id);
}
$this->getLog()->debug('thumbnail: Thumbnail request for mediaId ' . $id
. '. Media is a ' . $media->mediaType);
// Permissions.
if (!$this->getUser()->checkViewable($media) && !$isForceGrantAccess) {
// Output a 1px image if we're not allowed to see the media.
echo Img::make($this->getConfig()->uri('img/1x1.png', true))->encode();
return $this->render($request, $response);
}
// 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->thumbnail(
$media,
$response,
$this->getConfig()->uri('img/error.png', true)
);
return $this->render($request, $response);
}
/**
* Public Thumbnail
* this is an unauthenticated route (publicRoutes)
* we need to authenticate using the S3 link signing
* @param Request $request
* @param Response $response
* @param $id
* @return \Slim\Http\Response
* @throws \Xibo\Support\Exception\AccessDeniedException
* @throws \Xibo\Support\Exception\GeneralException
*/
public function thumbnailPublic(Request $request, Response $response, $id): Response
{
// Authenticate.
$params = $this->getSanitizer($request->getParams());
// Has the URL expired
if (time() > $params->getInt('X-Amz-Expires')) {
throw new AccessDeniedException(__('Expired'));
}
// Validate the URL.
$encryptionKey = $this->getConfig()->getApiKeyDetails()['encryptionKey'];
$signature = $params->getString('X-Amz-Signature');
$calculatedSignature = \Xibo\Helper\LinkSigner::getSignature(
(new HttpsDetect())->getUrl(),
$request->getUri()->getPath(),
$params->getInt('X-Amz-Expires'),
$encryptionKey,
$params->getString('X-Amz-Date'),
true,
);
if ($signature !== $calculatedSignature) {
throw new AccessDeniedException(__('Invalid URL'));
}
$this->getLog()->debug('thumbnailPublic: authorised for ' . $id);
$res = $this->thumbnail($request, $response, $id, true);
// Pass to the thumbnail route
return $res->withHeader('Access-Control-Allow-Origin', '*');
}
/**
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function mcaas(Request $request, Response $response, $id)
{
// This is only available through the API
if (!$this->isApi($request)) {
throw new AccessDeniedException(__('Route is available through the API'));
}
$options = [
'oldMediaId' => $id,
'updateInLayouts' => 1,
'deleteOldRevisions' => 1,
'allowMediaTypeChange' => 1
];
// Call Add with the oldMediaId
return $this->add($request->withParsedBody(['options' => $options]), $response);
}
/**
* @SWG\Post(
* path="/library/{mediaId}/tag",
* operationId="mediaTag",
* tags={"library"},
* summary="Tag Media",
* description="Tag a Media with one or more tags",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media 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/Media")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function tag(Request $request, Response $response, $id)
{
// Edit permission
// Get the media
$media = $this->mediaFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($media)) {
throw new AccessDeniedException();
}
$tags = $this->getSanitizer($request->getParams())->getArray('tag');
if (count($tags) <= 0) {
throw new InvalidArgumentException(__('No tags to assign'));
}
foreach ($tags as $tag) {
$media->assignTag($this->tagFactory->tagFromString($tag));
}
$media->save(['validate' => false]);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Tagged %s'), $media->name),
'id' => $media->mediaId,
'data' => $media
]);
return $this->render($request, $response);
}
/**
* @SWG\Post(
* path="/library/{mediaId}/untag",
* operationId="mediaUntag",
* tags={"library"},
* summary="Untag Media",
* description="Untag a Media with one or more tags",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media 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/Media")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function untag(Request $request, Response $response, $id)
{
// Edit permission
// Get the media
$media = $this->mediaFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($media)) {
throw new AccessDeniedException();
}
$tags = $this->getSanitizer($request->getParams())->getArray('tag');
if (count($tags) <= 0) {
throw new InvalidArgumentException(__('No tags to unassign'), 'tag');
}
foreach ($tags as $tag) {
$media->unassignTag($this->tagFactory->tagFromString($tag));
}
$media->save(['validate' => false]);
// Return
$this->getState()->hydrate([
'message' => sprintf(__('Untagged %s'), $media->name),
'id' => $media->mediaId,
'data' => $media
]);
return $this->render($request, $response);
}
/**
* Library Usage Report 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 usageForm(Request $request, Response $response, $id)
{
$media = $this->mediaFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
// Get a list of displays that this mediaId is used on
$displays = $this->displayFactory->query($this->gridRenderSort($sanitizedParams), $this->gridRenderFilter(['disableUserCheck' => 1, 'mediaId' => $id], $sanitizedParams));
$this->getState()->template = 'library-form-usage';
$this->getState()->setData([
'media' => $media,
'countDisplays' => count($displays)
]);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/library/usage/{mediaId}",
* operationId="libraryUsageReport",
* tags={"library"},
* summary="Get Library Item Usage Report",
* description="Get the records for the library item usage report",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media Id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*
* @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 usage(Request $request, Response $response, $id)
{
$media = $this->mediaFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
// Get a list of displays that this mediaId is used on by direct assignment
$displays = $this->displayFactory->query($this->gridRenderSort($sanitizedParams), $this->gridRenderFilter(['mediaId' => $id], $sanitizedParams));
// have we been provided with a date/time to restrict the scheduled events to?
$mediaFromDate = $sanitizedParams->getDate('mediaEventFromDate');
$mediaToDate = $sanitizedParams->getDate('mediaEventToDate');
// Media query array
$mediaQuery = [
'mediaId' => $id
];
if ($mediaFromDate !== null) {
$mediaQuery['futureSchedulesFrom'] = $mediaFromDate->format('U');
}
if ($mediaToDate !== null) {
$mediaQuery['futureSchedulesTo'] = $mediaToDate->format('U');
}
// Query for events
$events = $this->scheduleFactory->query(null, $mediaQuery);
// Total records returned from the schedules query
$totalRecords = $this->scheduleFactory->countLast();
foreach ($events as $row) {
/* @var \Xibo\Entity\Schedule $row */
// Generate this event
// Assess the date?
if ($mediaFromDate !== null && $mediaToDate !== null) {
try {
$scheduleEvents = $row->getEvents($mediaFromDate, $mediaToDate);
} catch (GeneralException $e) {
$this->getLog()->error('Unable to getEvents for ' . $row->eventId);
continue;
}
// Skip events that do not fall within the specified days
if (count($scheduleEvents) <= 0)
continue;
$this->getLog()->debug('EventId ' . $row->eventId . ' as events: ' . json_encode($scheduleEvents));
}
// Load the display groups
$row->load();
foreach ($row->displayGroups as $displayGroup) {
foreach ($this->displayFactory->getByDisplayGroupId($displayGroup->displayGroupId) as $display) {
$found = false;
// Check to see if our ID is already in our list
foreach ($displays as $existing) {
if ($existing->displayId === $display->displayId) {
$found = true;
break;
}
}
if (!$found)
$displays[] = $display;
}
}
}
if ($this->isApi($request) && $displays == []) {
$displays = [
'data' =>__('Specified Media item is not in use.')];
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $totalRecords;
$this->getState()->setData($displays);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/library/usage/layouts/{mediaId}",
* operationId="libraryUsageLayoutsReport",
* tags={"library"},
* summary="Get Library Item Usage Report for Layouts",
* description="Get the records for the library item usage report for Layouts",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media Id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*
* @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 usageLayouts(Request $request, Response $response, $id)
{
$media = $this->mediaFactory->getById($id);
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
$sanitizedParams = $this->getSanitizer($request->getParams());
$layouts = $this->layoutFactory->query(
$this->gridRenderSort($sanitizedParams),
$this->gridRenderFilter([
'mediaId' => $id,
'showDrafts' => 1
], $sanitizedParams)
);
if (!$this->isApi($request)) {
foreach ($layouts as $layout) {
$layout->includeProperty('buttons');
// Add some buttons for this row
if ($this->getUser()->checkEditable($layout)) {
// Design Button
$layout->buttons[] = array(
'id' => 'layout_button_design',
'linkType' => '_self', 'external' => true,
'url' => $this->urlFor($request,'layout.designer', ['id' => $layout->layoutId]),
'text' => __('Design')
);
}
// Preview
$layout->buttons[] = array(
'id' => 'layout_button_preview',
'external' => true,
'url' => '#',
'onclick' => 'createMiniLayoutPreview',
'onclickParam' => $this->urlFor($request, 'layout.preview', ['id' => $layout->layoutId]),
'text' => __('Preview Layout')
);
}
}
if ($this->isApi($request) && $layouts == []) {
$layouts = [
'data' =>__('Specified Media item is not in use.')
];
}
$this->getState()->template = 'grid';
$this->getState()->recordsTotal = $this->layoutFactory->countLast();
$this->getState()->setData($layouts);
return $this->render($request, $response);
}
/**
* Copy Media 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 Media
$media = $this->mediaFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
$this->getState()->template = 'library-form-copy';
$this->getState()->setData([
'media' => $media,
]);
return $this->render($request, $response);
}
/**
* Copies a Media
*
* @SWG\Post(
* path="/library/copy/{mediaId}",
* operationId="mediaCopy",
* tags={"library"},
* summary="Copy Media",
* description="Copy a Media, providing a new name and tags if applicable",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The media ID to Copy",
* type="integer",
* required=true
* ),
* @SWG\Parameter(
* name="name",
* in="formData",
* description="The name for the new Media",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="tags",
* in="formData",
* description="The Optional tags for new Media",
* type="string",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Media"),
* @SWG\Header(
* header="Location",
* description="Location of the new record",
* type="string"
* )
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function copy(Request $request, Response $response, $id)
{
// Get the Media
$media = $this->mediaFactory->getById($id);
$sanitizedParams = $this->getSanitizer($request->getParams());
// Check Permissions
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
// Load the media for Copy
$media = clone $media;
// Set new Name and tags
$media->name = $sanitizedParams->getString('name');
if ($this->getUser()->featureEnabled('tag.tagging')) {
if (is_array($sanitizedParams->getParam('tags'))) {
$tags = $this->tagFactory->tagsFromJson($sanitizedParams->getArray('tags'));
} else {
$tags = $this->tagFactory->tagsFromString($sanitizedParams->getString('tags'));
}
$media->updateTagLinks($tags);
}
// Set the Owner to user making the Copy
$media->setOwner($this->getUser()->userId);
// Set from global setting
if ($media->enableStat == null) {
$media->enableStat = $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT');
}
// Save the new Media
$media->save();
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => sprintf(__('Copied as %s'), $media->name),
'id' => $media->mediaId,
'data' => $media
]);
return $this->render($request, $response);
}
/**
* @SWG\Get(
* path="/library/{mediaId}/isused/",
* operationId="mediaIsUsed",
* tags={"library"},
* summary="Media usage check",
* description="Checks if a Media is being used",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media Id",
* type="integer",
* required=true
* ),
* @SWG\Response(
* response=200,
* description="successful operation"
* )
* )
*
* @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 isUsed(Request $request, Response $response, $id)
{
// Get the Media
$media = $this->mediaFactory->getById($id);
$this->getDispatcher()->dispatch(new MediaFullLoadEvent($media), MediaFullLoadEvent::$NAME);
// Check Permissions
if (!$this->getUser()->checkViewable($media)) {
throw new AccessDeniedException();
}
// Get count, being the number of times the media needs to appear to be true ( or use the default 0)
$count = $this->getSanitizer($request->getParams())->getInt('count', ['default' => 0]);
// Check and return result
$this->getState()->setData([
'isUsed' => $media->isUsed($count)
]);
return $this->render($request, $response);
}
/**
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws GeneralException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
*/
public function uploadFromUrlForm(Request $request, Response $response)
{
$this->getState()->template = 'library-form-uploadFromUrl';
$this->getState()->setData([
'uploadSizeMessage' => sprintf(__('This form accepts files up to a maximum size of %s'), Environment::getMaxUploadSize())
]);
return $this->render($request, $response);
}
/**
* Upload Media via URL
*
* @SWG\Post(
* path="/library/uploadUrl",
* operationId="uploadFromUrl",
* tags={"library"},
* summary="Upload Media from URL",
* description="Upload Media to CMS library from an external URL",
* @SWG\Parameter(
* name="url",
* in="formData",
* description="The URL to the media",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="type",
* in="formData",
* description="The type of the media, image, video etc",
* type="string",
* required=true
* ),
* @SWG\Parameter(
* name="extension",
* in="formData",
* description="Optional extension of the media, jpg, png etc. If not set in the request it will be retrieved from the headers",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="enableStat",
* in="formData",
* description="The option to enable the collection of Media Proof of Play statistics, On, Off or Inherit.",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="optionalName",
* in="formData",
* description="An optional name for this media file, if left empty it will default to the file name",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="expires",
* in="formData",
* description="Date in Y-m-d H:i:s format, will set expiration date on the Media item",
* type="string",
* required=false
* ),
* @SWG\Parameter(
* name="folderId",
* in="formData",
* description="Folder ID to which this media should be assigned to",
* type="integer",
* required=false
* ),
* @SWG\Response(
* response=201,
* description="successful operation",
* @SWG\Schema(ref="#/definitions/Media"),
* @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 ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws LibraryFullException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function uploadFromUrl(Request $request, Response $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
// Params
$url = $sanitizedParams->getString('url');
$type = $sanitizedParams->getString('type');
$optionalName = $sanitizedParams->getString('optionalName');
$extension = $sanitizedParams->getString('extension');
$enableStat = $sanitizedParams->getString('enableStat', [
'default' => $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT')
]);
// Folders
$folderId = $sanitizedParams->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
$folder = $this->folderFactory->getById($folderId, 0);
if ($sanitizedParams->hasParam('expires')) {
if ($sanitizedParams->getDate('expires')->format('U') > Carbon::now()->format('U')) {
$expires = $sanitizedParams->getDate('expires')->format('U');
} else {
throw new InvalidArgumentException(__('Cannot set Expiry date in the past'), 'expires');
}
} else {
$expires = 0;
}
// Validate the URL
if (!v::url()->notEmpty()->validate($url) || !filter_var($url, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException(__('Provided URL is invalid'), 'url');
}
// remote file size
$downloadInfo = $this->getMediaService()->getDownloadInfo($url);
// check if we have extension provided in the request (available via API)
// if not get it from the headers
if (!empty($extension)) {
$ext = $extension;
} else {
$ext = $downloadInfo['extension'];
}
// Unsupported links (ie Youtube links, etc) will return a null extension, thus, throw an error
if (is_null($ext)) {
throw new NotFoundException(sprintf(__('Extension %s is not supported.'), $ext));
}
// Initialise the library and do some checks
$this->getMediaService()
->initLibrary()
->checkLibraryOrQuotaFull(true)
->checkMaxUploadSize($downloadInfo['size']);
// check if we have type provided in the request (available via API), if not get the module type from
// the extension
if (!empty($type)) {
$module = $this->getModuleFactory()->getByType($type);
} else {
$module = $this->getModuleFactory()->getByExtension($ext);
$module = $this->getModuleFactory()->getByType($module->type);
}
// if we were provided with optional Media name set it here, otherwise get it from download info
$name = empty($optionalName) ? htmlspecialchars($downloadInfo['filename']) : $optionalName;
// double check that provided Module Type and Extension are valid
if (!Str::contains($module->getSetting('validExtensions'), $ext)) {
throw new NotFoundException(
sprintf(
__('Invalid Module type or extension. Module type %s does not allow for %s extension'),
$module->type,
$ext
)
);
}
// add our media to queueDownload and process the downloads
$media = $this->mediaFactory->queueDownload(
$name,
str_replace(' ', '%20', htmlspecialchars_decode($url)),
$expires,
[
'fileType' => strtolower($module->type),
'duration' => $module->defaultDuration,
'extension' => $ext,
'enableStat' => $enableStat,
'folderId' => $folder->getId(),
'permissionsFolderId' => $folder->getPermissionFolderIdOrThis()
]
);
$this->mediaFactory->processDownloads(
function (Media $media) use ($module) {
// Success
$this->getLog()->debug('Successfully uploaded Media from URL, Media Id is ' . $media->mediaId);
$libraryFolder = $this->getConfig()->getSetting('LIBRARY_LOCATION');
$realDuration = $module->fetchDurationOrDefaultFromFile($libraryFolder . $media->storedAs);
if ($realDuration !== $media->duration) {
$media->updateDuration($realDuration);
}
},
function (Media $media) {
throw new InvalidArgumentException(__('Download rejected for an unknown reason.'));
},
function ($message) {
// Download rejected.
throw new InvalidArgumentException(sprintf(__('Download rejected due to %s'), $message));
}
);
// Return
$this->getState()->hydrate([
'httpStatus' => 201,
'message' => __('Media upload from URL was successful'),
'id' => $media->mediaId,
'data' => $media
]);
return $this->render($request, $response);
}
/**
* This is called when video finishes uploading.
* Saves provided base64 image as an actual image to the library
*
* @param Request $request
* @param Response $response
*
* @return Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function addThumbnail($request, $response)
{
$sanitizedParams = $this->getSanitizer($request->getParams());
$libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
MediaService::ensureLibraryExists($libraryLocation);
$imageData = $request->getParam('image');
$mediaId = $sanitizedParams->getInt('mediaId');
$media = $this->mediaFactory->getById($mediaId);
if (!$this->getUser()->checkEditable($media)) {
throw new AccessDeniedException();
}
try {
Img::configure(array('driver' => 'gd'));
// Load the image
$image = Img::make($imageData);
$image->save($libraryLocation . $mediaId . '_' . $media->mediaType . 'cover.png');
} catch (\Exception $exception) {
$this->getLog()->error('Exception adding Video cover image. e = ' . $exception->getMessage());
throw new InvalidArgumentException(__('Invalid image data'));
}
$media->width = $image->getWidth();
$media->height = $image->getHeight();
$media->orientation = ($media->width >= $media->height) ? 'landscape' : 'portrait';
$media->save(['saveTags' => false, 'validate' => false]);
return $response->withStatus(204);
}
/**
* Select Folder 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 selectFolderForm(Request $request, Response $response, $id)
{
// Get the Media
$media = $this->mediaFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($media)) {
throw new AccessDeniedException();
}
$data = [
'media' => $media
];
$this->getState()->template = 'library-form-selectfolder';
$this->getState()->setData($data);
return $this->render($request, $response);
}
/**
* @SWG\Put(
* path="/library/{id}/selectfolder",
* operationId="librarySelectFolder",
* tags={"library"},
* summary="Media Select folder",
* description="Select Folder for Media",
* @SWG\Parameter(
* name="mediaId",
* in="path",
* description="The Media ID",
* type="integer",
* required=true
* ),
* @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/Campaign")
* )
* )
*
* @param Request $request
* @param Response $response
* @param $id
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws AccessDeniedException
* @throws ConfigurationException
* @throws GeneralException
* @throws InvalidArgumentException
* @throws NotFoundException
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\DuplicateEntityException
*/
public function selectFolder(Request $request, Response $response, $id)
{
// Get the Media
$media = $this->mediaFactory->getById($id);
// Check Permissions
if (!$this->getUser()->checkEditable($media)) {
throw new AccessDeniedException();
}
$folderId = $this->getSanitizer($request->getParams())->getInt('folderId');
if ($folderId === 1) {
$this->checkRootFolderAllowSave();
}
$media->folderId = $folderId;
$folder = $this->folderFactory->getById($media->folderId);
$media->permissionsFolderId = ($folder->getPermissionFolderId() == null) ? $folder->id : $folder->getPermissionFolderId();
$media->save(['saveTags' => false]);
if ($media->parentId != 0) {
$this->updateMediaRevision($media, $folderId);
}
// Return
$this->getState()->hydrate([
'httpStatus' => 204,
'message' => sprintf(__('Media %s moved to Folder %s'), $media->name, $folder->text)
]);
return $this->render($request, $response);
}
/**
* Connector import.
*
* Note: this doesn't have a Swagger document because it is only available via the web UI.
*
* @param Request $request
* @param Response $response
* @return \Psr\Http\Message\ResponseInterface|Response
* @throws \Xibo\Support\Exception\ControllerNotImplemented
* @throws \Xibo\Support\Exception\GeneralException
*/
public function connectorImport(Request $request, Response $response)
{
$params = $this->getSanitizer($request->getParams());
$items = $params->getArray('items');
// Folders
$folderId = $params->getInt('folderId');
if (empty($folderId) || !$this->getUser()->featureEnabled('folder.view')) {
$folderId = $this->getUser()->homeFolderId;
}
$folder = $this->folderFactory->getById($folderId, 0);
// Stats
$enableStat = $params->getString('enableStat', [
'default' => $this->getConfig()->getSetting('MEDIA_STATS_ENABLED_DEFAULT')
]);
// Initialise the library.
$this->getMediaService()
->initLibrary()
->checkLibraryOrQuotaFull(true);
$libraryLocation = $this->getConfig()->getSetting('LIBRARY_LOCATION');
// Hand these off to the connector to format into a downloadable response.
$importQueue = [];
foreach ($items as $item) {
$import = new ProviderImport();
$import->searchResult = new SearchResult();
$import->searchResult->provider = new ProviderDetails();
$import->searchResult->provider->id = $item['provider']['id'];
$import->searchResult->title = $item['title'];
$import->searchResult->id = $item['id'];
$import->searchResult->type = $item['type'];
$import->searchResult->download = $item['download'];
$import->searchResult->duration = (int)$item['duration'];
$import->searchResult->videoThumbnailUrl = $item['videoThumbnailUrl'];
$importQueue[] = $import;
}
$event = new LibraryProviderImportEvent($importQueue);
$this->getDispatcher()->dispatch($event, $event->getName());
// Pull out our events and upload
foreach ($importQueue as $import) {
try {
// Has this been configured for upload?
if ($import->isConfigured) {
// Make sure we have a URL
if (empty($import->url)) {
throw new InvalidArgumentException('Missing or invalid URL', 'url');
}
// This ensures that apiRef will be unique for each provider and resource id
$apiRef = $import->searchResult->provider->id . '_' . $import->searchResult->id;
// Queue this for upload.
// Use a module to make sure our type, etc is supported.
// make sure the name is not longer than 100 characters.
$name = $import->searchResult->title;
if (strlen($name) >= 100) {
$name = trim(preg_replace('/\s+?(\S+)?$/', '', substr($name, 0, 95)), ', ');
}
$module = $this->getModuleFactory()->getByType($import->searchResult->type);
$import->media = $this->mediaFactory->queueDownload(
$name,
str_replace(' ', '%20', htmlspecialchars_decode($import->url)),
0,
[
'fileType' => strtolower($module->type),
'duration' => !(empty($import->searchResult->duration))
? $import->searchResult->duration
: $module->defaultDuration,
'enableStat' => $enableStat,
'folderId' => $folder->getId(),
'permissionsFolderId' => $folder->permissionsFolderId,
'apiRef' => $apiRef
]
);
} else {
throw new GeneralException(__('Not configured by any active connector.'));
}
} catch (\Exception $e) {
$import->setError($e->getMessage());
}
}
// Process all of those downloads
$this->mediaFactory->processDownloads(
function (Media $media) use ($importQueue, $libraryLocation) {
// Success
// if we have video thumbnail url from provider, download it now
foreach ($importQueue as $import) {
/** @var ProviderImport $import */
if ($import->media->getId() === $media->getId()
&& $media->mediaType === 'video'
&& !empty($import->searchResult->videoThumbnailUrl)
) {
try {
$filePath = $libraryLocation . $media->getId() . '_' . $media->mediaType . 'cover.png';
// Expect a quick download.
$client = new Client($this->getConfig()->getGuzzleProxy(['timeout' => 20]));
$client->request(
'GET',
$import->searchResult->videoThumbnailUrl,
['sink' => $filePath]
);
list($imgWidth, $imgHeight) = @getimagesize($filePath);
$media->updateOrientation($imgWidth, $imgHeight);
} catch (\Exception $exception) {
// if we failed, corrupted file might still be created, remove it here
unlink($libraryLocation . $media->getId() . '_' . $media->mediaType . 'cover.png');
$this->getLog()->error(sprintf(
'Downloading thumbnail for video %s, from url %s, failed with message %s',
$media->name,
$import->searchResult->videoThumbnailUrl,
$exception->getMessage()
));
}
}
}
},
function ($media) use ($importQueue) {
// Failure
// Pull out the import which failed.
foreach ($importQueue as $import) {
/** @var ProviderImport $import */
if ($import->media->getId() === $media->getId()) {
$import->setError(__('Download failed'));
}
}
}
);
// Return
$this->getState()->hydrate([
'httpStatus' => 200,
'message' => __('Imported'),
'data' => $event->getItems()
]);
return $this->render($request, $response);
}
/**
* Check if we already have a full screen Layout for this Media
* @param Media $media
* @return int|null
* @throws NotFoundException
*/
private function hasFullScreenLayout(Media $media): ?int
{
return $this->layoutFactory->getLinkedFullScreenLayout('media', $media->mediaId)?->campaignId;
}
/**
* Update media files with revisions
* @param Media $media
* @param $folderId
*/
private function updateMediaRevision(Media $media, $folderId)
{
$oldMedia = $this->mediaFactory->getParentById($media->mediaId);
$oldMedia->folderId = $folderId;
$folder = $this->folderFactory->getById($oldMedia->folderId);
$folder->permissionsFolderId = ($folder->getPermissionFolderId() == null) ? $folder->id : $folder->getPermissionFolderId();
$oldMedia->save(['saveTags' => false, 'validate' => false]);
}
}