1041 lines
33 KiB
PHP
1041 lines
33 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\Entity;
|
||
|
|
|
||
|
|
use Carbon\Carbon;
|
||
|
|
use Mimey\MimeTypes;
|
||
|
|
use Respect\Validation\Validator as v;
|
||
|
|
use Xibo\Factory\MediaFactory;
|
||
|
|
use Xibo\Factory\PermissionFactory;
|
||
|
|
use Xibo\Helper\DateFormatHelper;
|
||
|
|
use Xibo\Service\ConfigServiceInterface;
|
||
|
|
use Xibo\Service\LogServiceInterface;
|
||
|
|
use Xibo\Storage\StorageServiceInterface;
|
||
|
|
use Xibo\Support\Exception\ConfigurationException;
|
||
|
|
use Xibo\Support\Exception\DuplicateEntityException;
|
||
|
|
use Xibo\Support\Exception\GeneralException;
|
||
|
|
use Xibo\Support\Exception\InvalidArgumentException;
|
||
|
|
use Xibo\Support\Exception\NotFoundException;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Class Media
|
||
|
|
* @package Xibo\Entity
|
||
|
|
*
|
||
|
|
* @SWG\Definition()
|
||
|
|
*/
|
||
|
|
class Media implements \JsonSerializable
|
||
|
|
{
|
||
|
|
use EntityTrait;
|
||
|
|
use TagLinkTrait;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The Media ID")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $mediaId;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The ID of the User that owns this Media")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $ownerId;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The Parent ID of this Media if it has been revised")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $parentId;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The Name of this Media")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $name;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The module type of this Media")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $mediaType;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The file name of the media as stored in the library")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $storedAs;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The original file name as it was uploaded")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $fileName;
|
||
|
|
|
||
|
|
// Thing that might be referred to
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="Tags associated with this Media, array of TagLink objects")
|
||
|
|
* @var TagLink[]
|
||
|
|
*/
|
||
|
|
public $tags = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The file size in bytes")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $fileSize;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The duration to use when assigning this media to a Layout widget")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $duration = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="Flag indicating whether this media is valid.")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $valid = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="DEPRECATED: Flag indicating whether this media is a system file or not")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $moduleSystemFile = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="Timestamp indicating when this media should expire")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $expires = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="Flag indicating whether this media is retired")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $retired = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="Flag indicating whether this media has been edited and replaced with a newer file")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $isEdited = 0;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="A MD5 checksum of the stored media file")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $md5;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The username of the User that owns this media")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $owner;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="A comma separated list of groups/users with permissions to this Media")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $groupsWithPermissions;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="A flag indicating whether this media has been released")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $released = 1;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="An API reference")
|
||
|
|
* @var string
|
||
|
|
*/
|
||
|
|
public $apiRef;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var string
|
||
|
|
* @SWG\Property(
|
||
|
|
* description="The datetime the Media was created"
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public $createdDt;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var string
|
||
|
|
* @SWG\Property(
|
||
|
|
* description="The datetime the Media was last modified"
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public $modifiedDt;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var string
|
||
|
|
* @SWG\Property(
|
||
|
|
* description="The option to enable the collection of Media Proof of Play statistics"
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public $enableStat;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var string
|
||
|
|
* @SWG\Property(
|
||
|
|
* description="The orientation of the Media file"
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public $orientation;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var int
|
||
|
|
* @SWG\Property(description="The width of the Media file")
|
||
|
|
*/
|
||
|
|
public $width;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var int
|
||
|
|
* @SWG\Property(description="The height of the Media file")
|
||
|
|
*/
|
||
|
|
public $height;
|
||
|
|
|
||
|
|
// Private
|
||
|
|
/** @var TagLink[] */
|
||
|
|
private $unlinkTags = [];
|
||
|
|
/** @var TagLink[] */
|
||
|
|
private $linkTags = [];
|
||
|
|
private $requestOptions = [];
|
||
|
|
private $datesToFormat = ['expires'];
|
||
|
|
// New file revision
|
||
|
|
public $isSaveRequired;
|
||
|
|
public $isRemote;
|
||
|
|
|
||
|
|
public $cloned = false;
|
||
|
|
public $newExpiry;
|
||
|
|
public $alwaysCopy = false;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The id of the Folder this Media belongs to")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $folderId;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Property(description="The id of the Folder responsible for providing permissions for this Media")
|
||
|
|
* @var int
|
||
|
|
*/
|
||
|
|
public $permissionsFolderId;
|
||
|
|
|
||
|
|
public $widgets = [];
|
||
|
|
public $displayGroups = [];
|
||
|
|
public $layoutBackgroundImages = [];
|
||
|
|
private $permissions = [];
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var ConfigServiceInterface
|
||
|
|
*/
|
||
|
|
private $config;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var MediaFactory
|
||
|
|
*/
|
||
|
|
private $mediaFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var PermissionFactory
|
||
|
|
*/
|
||
|
|
private $permissionFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Entity constructor.
|
||
|
|
* @param StorageServiceInterface $store
|
||
|
|
* @param LogServiceInterface $log
|
||
|
|
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
|
||
|
|
* @param ConfigServiceInterface $config
|
||
|
|
* @param MediaFactory $mediaFactory
|
||
|
|
* @param PermissionFactory $permissionFactory
|
||
|
|
*/
|
||
|
|
public function __construct($store, $log, $dispatcher, $config, $mediaFactory, $permissionFactory)
|
||
|
|
{
|
||
|
|
$this->setCommonDependencies($store, $log, $dispatcher);
|
||
|
|
|
||
|
|
$this->config = $config;
|
||
|
|
$this->mediaFactory = $mediaFactory;
|
||
|
|
$this->permissionFactory = $permissionFactory;
|
||
|
|
}
|
||
|
|
|
||
|
|
public function __clone()
|
||
|
|
{
|
||
|
|
// Clear the ID's and all widget/displayGroup assignments
|
||
|
|
$this->mediaId = null;
|
||
|
|
$this->widgets = [];
|
||
|
|
$this->displayGroups = [];
|
||
|
|
$this->layoutBackgroundImages = [];
|
||
|
|
$this->permissions = [];
|
||
|
|
$this->tags = [];
|
||
|
|
|
||
|
|
// We need to do something with the name
|
||
|
|
$this->name = sprintf(
|
||
|
|
__('Copy of %s on %s'),
|
||
|
|
$this->name,
|
||
|
|
Carbon::now()->format(DateFormatHelper::getSystemFormat())
|
||
|
|
);
|
||
|
|
|
||
|
|
// Set so that when we add, we copy the existing file in the library
|
||
|
|
$this->fileName = $this->storedAs;
|
||
|
|
$this->storedAs = null;
|
||
|
|
$this->cloned = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Id
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
public function getId(): int
|
||
|
|
{
|
||
|
|
return $this->mediaId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
public function getPermissionFolderId(): int
|
||
|
|
{
|
||
|
|
return $this->permissionsFolderId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get Owner Id
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
public function getOwnerId(): int
|
||
|
|
{
|
||
|
|
return $this->ownerId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get the MIME type for this media
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public function getMimeType(): string
|
||
|
|
{
|
||
|
|
$mimeTypes = new MimeTypes();
|
||
|
|
$ext = explode('.', $this->storedAs);
|
||
|
|
return $mimeTypes->getMimeType($ext[count($ext) - 1]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Sets the Owner
|
||
|
|
* @param int $ownerId
|
||
|
|
*/
|
||
|
|
public function setOwner(int $ownerId)
|
||
|
|
{
|
||
|
|
$this->ownerId = $ownerId;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @return int
|
||
|
|
* @throws GeneralException
|
||
|
|
*/
|
||
|
|
private function countUsages()
|
||
|
|
{
|
||
|
|
$this->load(['fullInfo' => true]);
|
||
|
|
|
||
|
|
return count($this->widgets) + count($this->displayGroups) + count($this->layoutBackgroundImages);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Is this media used
|
||
|
|
* @param int $usages threshold
|
||
|
|
* @return bool
|
||
|
|
* @throws GeneralException
|
||
|
|
*/
|
||
|
|
public function isUsed($usages = 0)
|
||
|
|
{
|
||
|
|
return $this->countUsages() > $usages;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Validate
|
||
|
|
* @param array $options
|
||
|
|
* @throws GeneralException
|
||
|
|
*/
|
||
|
|
public function validate($options)
|
||
|
|
{
|
||
|
|
if (!v::stringType()->notEmpty()->validate($this->mediaType)) {
|
||
|
|
throw new InvalidArgumentException(__('Unknown Media Type'), 'type');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!v::stringType()->notEmpty()->length(1, 100)->validate($this->name)) {
|
||
|
|
throw new InvalidArgumentException(__('The name must be between 1 and 100 characters'), 'name');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check the naming of this item to ensure it doesn't conflict
|
||
|
|
$params = [];
|
||
|
|
$checkSQL = 'SELECT `name`, `mediaId`, `apiRef` FROM `media` WHERE `name` = :name AND userid = :userId';
|
||
|
|
|
||
|
|
if ($this->mediaId != 0) {
|
||
|
|
$checkSQL .= ' AND mediaId <> :mediaId AND IsEdited = 0 ';
|
||
|
|
$params['mediaId'] = $this->mediaId;
|
||
|
|
}
|
||
|
|
else if ($options['oldMedia'] != null && $this->name == $options['oldMedia']->name) {
|
||
|
|
$checkSQL .= ' AND IsEdited = 0 ';
|
||
|
|
}
|
||
|
|
|
||
|
|
$params['name'] = $this->name;
|
||
|
|
$params['userId'] = $this->ownerId;
|
||
|
|
|
||
|
|
$result = $this->getStore()->select($checkSQL, $params);
|
||
|
|
|
||
|
|
if (count($result) > 0) {
|
||
|
|
// If the media is imported from a provider (ie Pixabay, etc), use it instead of importing again.
|
||
|
|
if (isset($this->apiRef) && $this->apiRef === $result[0]['apiRef']) {
|
||
|
|
$this->mediaId = intval($result[0]['mediaId']);
|
||
|
|
} else {
|
||
|
|
throw new DuplicateEntityException(__('Media you own already has this name. Please choose another.'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Load
|
||
|
|
* @param array $options
|
||
|
|
* @throws GeneralException
|
||
|
|
*/
|
||
|
|
public function load($options = [])
|
||
|
|
{
|
||
|
|
if ($this->loaded || $this->mediaId == null) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$options = array_merge([
|
||
|
|
'deleting' => false,
|
||
|
|
'fullInfo' => false
|
||
|
|
], $options);
|
||
|
|
|
||
|
|
$this->getLog()->debug(sprintf('Loading Media. Options = %s', json_encode($options)));
|
||
|
|
|
||
|
|
// Are we loading for a delete? If so load the child models, unless we're a module file in which case
|
||
|
|
// we've no need.
|
||
|
|
if ($this->mediaType !== 'module' && ($options['deleting'] || $options['fullInfo'])) {
|
||
|
|
// Permissions
|
||
|
|
$this->permissions = $this->permissionFactory->getByObjectId(get_class($this), $this->mediaId);
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->loaded = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Save this media
|
||
|
|
* @param array $options
|
||
|
|
* @throws ConfigurationException
|
||
|
|
* @throws DuplicateEntityException
|
||
|
|
* @throws InvalidArgumentException
|
||
|
|
* @throws GeneralException
|
||
|
|
*/
|
||
|
|
public function save($options = [])
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('Save for mediaId: ' . $this->mediaId);
|
||
|
|
|
||
|
|
$options = array_merge([
|
||
|
|
'validate' => true,
|
||
|
|
'oldMedia' => null,
|
||
|
|
'deferred' => false,
|
||
|
|
'saveTags' => true,
|
||
|
|
'audit' => true,
|
||
|
|
], $options);
|
||
|
|
|
||
|
|
if ($options['validate'] && $this->mediaType !== 'module') {
|
||
|
|
$this->validate($options);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add or edit
|
||
|
|
if ($this->mediaId == null || $this->mediaId == 0) {
|
||
|
|
$this->add();
|
||
|
|
|
||
|
|
// Always set force to true as we always want to save new files
|
||
|
|
$this->isSaveRequired = true;
|
||
|
|
|
||
|
|
if ($options['audit']) {
|
||
|
|
$this->audit($this->mediaId, 'Added', [
|
||
|
|
'mediaId' => $this->mediaId,
|
||
|
|
'name' => $this->name,
|
||
|
|
'mediaType' => $this->mediaType,
|
||
|
|
'fileName' => $this->fileName,
|
||
|
|
'folderId' => $this->folderId
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$this->edit();
|
||
|
|
|
||
|
|
// If the media file is invalid, then force an update (only applies to module files)
|
||
|
|
$expires = $this->getOriginalValue('expires');
|
||
|
|
$this->isSaveRequired = $this->isSaveRequired
|
||
|
|
|| $this->valid == 0
|
||
|
|
|| ($expires > 0 && $expires < Carbon::now()->format('U'))
|
||
|
|
|| ($this->mediaType === 'module' && !file_exists($this->downloadSink(false)));
|
||
|
|
|
||
|
|
if ($options['audit']) {
|
||
|
|
$this->audit($this->mediaId, 'Updated', $this->getChangedProperties());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($options['deferred']) {
|
||
|
|
$this->getLog()->debug('Media Update deferred until later');
|
||
|
|
} else {
|
||
|
|
$this->getLog()->debug('Media Update happening now');
|
||
|
|
|
||
|
|
// Call save file
|
||
|
|
if ($this->isSaveRequired) {
|
||
|
|
$this->saveFile();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($options['saveTags']) {
|
||
|
|
// Remove unwanted ones
|
||
|
|
if (is_array($this->unlinkTags)) {
|
||
|
|
foreach ($this->unlinkTags as $tag) {
|
||
|
|
$this->unlinkTagFromEntity('lktagmedia', 'mediaId', $this->mediaId, $tag->tagId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save the tags
|
||
|
|
if (is_array($this->linkTags)) {
|
||
|
|
foreach ($this->linkTags as $tag) {
|
||
|
|
$this->linkTagToEntity('lktagmedia', 'mediaId', $this->mediaId, $tag->tagId, $tag->value);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Save Async
|
||
|
|
* @param array $options
|
||
|
|
* @return $this
|
||
|
|
* @throws ConfigurationException
|
||
|
|
* @throws DuplicateEntityException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws InvalidArgumentException
|
||
|
|
*/
|
||
|
|
public function saveAsync($options = [])
|
||
|
|
{
|
||
|
|
$options = array_merge([
|
||
|
|
'deferred' => true,
|
||
|
|
'requestOptions' => []
|
||
|
|
], $options);
|
||
|
|
$this->requestOptions = $options['requestOptions'];
|
||
|
|
$this->save($options);
|
||
|
|
|
||
|
|
return $this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete
|
||
|
|
* @param array $options
|
||
|
|
* @throws GeneralException
|
||
|
|
*/
|
||
|
|
public function delete($options = [])
|
||
|
|
{
|
||
|
|
$options = array_merge([
|
||
|
|
'rollback' => false
|
||
|
|
], $options);
|
||
|
|
|
||
|
|
if ($options['rollback']) {
|
||
|
|
$this->deleteRecord();
|
||
|
|
$this->deleteFile();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->load(['deleting' => true]);
|
||
|
|
|
||
|
|
// Prepare some contexts for auditing
|
||
|
|
$auditMessage = 'Deleted';
|
||
|
|
$auditContext = [
|
||
|
|
'mediaId' => $this->mediaId,
|
||
|
|
'name' => $this->name,
|
||
|
|
'mediaType' => $this->mediaType,
|
||
|
|
'fileName' => $this->fileName,
|
||
|
|
];
|
||
|
|
|
||
|
|
// Should we bring back this item's parent?
|
||
|
|
try {
|
||
|
|
$parentMedia = $this->mediaFactory->getParentById($this->mediaId);
|
||
|
|
|
||
|
|
// If the parent media is expired, delete it directly
|
||
|
|
// Or if the current media is expired, delete the parent regardless of its own expiration
|
||
|
|
if ((!empty($parentMedia->expires) && $parentMedia->expires < Carbon::now()->timestamp) ||
|
||
|
|
(!empty($this->expires) && $this->expires < Carbon::now()->timestamp)
|
||
|
|
) {
|
||
|
|
$parentMedia->delete();
|
||
|
|
|
||
|
|
$auditMessage .= ' and deleted old revision';
|
||
|
|
$auditContext['deletedParentMediaId'] = $parentMedia->mediaId;
|
||
|
|
} else {
|
||
|
|
// Otherwise, revert the parent media
|
||
|
|
$parentMedia->isEdited = 0;
|
||
|
|
$parentMedia->parentId = null;
|
||
|
|
$parentMedia->save(['validate' => false, 'audit' => false]);
|
||
|
|
|
||
|
|
$auditMessage .= ' and reverted old revision';
|
||
|
|
$auditContext['revertedMediaId'] = $parentMedia->mediaId;
|
||
|
|
}
|
||
|
|
} catch (NotFoundException) {
|
||
|
|
// No parent, this is fine.
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($this->permissions as $permission) {
|
||
|
|
/* @var Permission $permission */
|
||
|
|
$permission->delete();
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->unlinkAllTagsFromEntity('lktagmedia', 'mediaId', $this->mediaId);
|
||
|
|
|
||
|
|
$this->deleteRecord();
|
||
|
|
$this->deleteFile();
|
||
|
|
|
||
|
|
$this->audit($this->mediaId, $auditMessage, $auditContext);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add
|
||
|
|
*/
|
||
|
|
private function add()
|
||
|
|
{
|
||
|
|
// The originalFileName column has limit of 254 characters
|
||
|
|
// if the filename basename that we are about to save is still over the limit, attempt to strip query string
|
||
|
|
// we cannot make any operations directly on $this->fileName, as that might be still needed to processDownloads
|
||
|
|
$fileName = basename($this->fileName);
|
||
|
|
if (strpos(basename($fileName), '?')) {
|
||
|
|
$fileName = substr(basename($fileName), 0, strpos(basename($fileName), '?'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sanitize what we have left and make it fit in the space we have.
|
||
|
|
$fileName = substr(htmlspecialchars($fileName), 0, 254);
|
||
|
|
|
||
|
|
$this->mediaId = $this->getStore()->insert('
|
||
|
|
INSERT INTO `media` (`name`, `type`, duration, originalFilename, userID, retired, moduleSystemFile, released, apiRef, valid, `createdDt`, `modifiedDt`, `enableStat`, `folderId`, `permissionsFolderId`, `orientation`, `width`, `height`)
|
||
|
|
VALUES (:name, :type, :duration, :originalFileName, :userId, :retired, :moduleSystemFile, :released, :apiRef, :valid, :createdDt, :modifiedDt, :enableStat, :folderId, :permissionsFolderId, :orientation, :width, :height)
|
||
|
|
', [
|
||
|
|
'name' => $this->name,
|
||
|
|
'type' => $this->mediaType,
|
||
|
|
'duration' => $this->duration,
|
||
|
|
'originalFileName' => $fileName,
|
||
|
|
'userId' => $this->ownerId,
|
||
|
|
'retired' => $this->retired,
|
||
|
|
'moduleSystemFile' => (($this->moduleSystemFile) ? 1 : 0),
|
||
|
|
'released' => $this->released,
|
||
|
|
'apiRef' => $this->apiRef,
|
||
|
|
'valid' => $this->valid,
|
||
|
|
'createdDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
|
||
|
|
'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
|
||
|
|
'enableStat' => $this->enableStat,
|
||
|
|
'folderId' => ($this->folderId === null) ? 1 : $this->folderId,
|
||
|
|
'permissionsFolderId' => ($this->permissionsFolderId == null) ? 1 : $this->permissionsFolderId,
|
||
|
|
'orientation' => $this->orientation,
|
||
|
|
'width' => ($this->width === null) ? null : $this->width,
|
||
|
|
'height' => ($this->height === null) ? null : $this->height,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Edit
|
||
|
|
*/
|
||
|
|
private function edit()
|
||
|
|
{
|
||
|
|
$sql = '
|
||
|
|
UPDATE `media`
|
||
|
|
SET `name` = :name,
|
||
|
|
duration = :duration,
|
||
|
|
retired = :retired,
|
||
|
|
moduleSystemFile = :moduleSystemFile,
|
||
|
|
editedMediaId = :editedMediaId,
|
||
|
|
isEdited = :isEdited,
|
||
|
|
userId = :userId,
|
||
|
|
released = :released,
|
||
|
|
apiRef = :apiRef,
|
||
|
|
modifiedDt = :modifiedDt,
|
||
|
|
`enableStat` = :enableStat,
|
||
|
|
expires = :expires,
|
||
|
|
folderId = :folderId,
|
||
|
|
permissionsFolderId = :permissionsFolderId,
|
||
|
|
orientation = :orientation,
|
||
|
|
width = :width,
|
||
|
|
height = :height
|
||
|
|
WHERE mediaId = :mediaId
|
||
|
|
';
|
||
|
|
|
||
|
|
$params = [
|
||
|
|
'name' => $this->name,
|
||
|
|
'duration' => $this->duration,
|
||
|
|
'retired' => $this->retired,
|
||
|
|
'moduleSystemFile' => $this->moduleSystemFile,
|
||
|
|
'editedMediaId' => $this->parentId,
|
||
|
|
'isEdited' => $this->isEdited,
|
||
|
|
'userId' => $this->ownerId,
|
||
|
|
'released' => $this->released,
|
||
|
|
'apiRef' => $this->apiRef,
|
||
|
|
'mediaId' => $this->mediaId,
|
||
|
|
'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()),
|
||
|
|
'enableStat' => $this->enableStat,
|
||
|
|
'expires' => $this->expires,
|
||
|
|
'folderId' => $this->folderId,
|
||
|
|
'permissionsFolderId' => $this->permissionsFolderId,
|
||
|
|
'orientation' => $this->orientation,
|
||
|
|
'width' => ($this->width === null) ? null : $this->width,
|
||
|
|
'height' => ($this->height === null) ? null : $this->height,
|
||
|
|
];
|
||
|
|
|
||
|
|
$this->getStore()->update($sql, $params);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete record
|
||
|
|
*/
|
||
|
|
private function deleteRecord()
|
||
|
|
{
|
||
|
|
// Delete direct assignments to displays. This will be module files assigned by widgets
|
||
|
|
// no need to notify the display as the next time it collects is sufficient to delete these
|
||
|
|
$this->getStore()->update('DELETE FROM `display_media` WHERE mediaID = :mediaId', [
|
||
|
|
'mediaId' => $this->mediaId
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Delete the media entry itself
|
||
|
|
$this->getStore()->update('DELETE FROM `media` WHERE mediaID = :mediaId', ['mediaId' => $this->mediaId]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Save File to Library
|
||
|
|
* works on files that are already in the File system
|
||
|
|
* @throws ConfigurationException
|
||
|
|
*/
|
||
|
|
public function saveFile()
|
||
|
|
{
|
||
|
|
$libraryFolder = $this->config->getSetting('LIBRARY_LOCATION');
|
||
|
|
|
||
|
|
// Work out the extension
|
||
|
|
$lastPeriod = strrchr($this->fileName, '.');
|
||
|
|
|
||
|
|
// Determine the save name
|
||
|
|
if ($lastPeriod === false) {
|
||
|
|
$saveName = $this->mediaId;
|
||
|
|
} else {
|
||
|
|
$saveName = $this->mediaId . '.' . strtolower(substr($lastPeriod, 1));
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($this->getUnmatchedProperty('urlDownload', false) === true) {
|
||
|
|
// for upload via URL, handle cases where URL do not have specified extension in url
|
||
|
|
// we either have a long string after lastPeriod or nothing
|
||
|
|
$extension = $this->getUnmatchedProperty('extension');
|
||
|
|
if (isset($extension) && (strlen($lastPeriod) > 3 || $lastPeriod === false)) {
|
||
|
|
$saveName = $this->mediaId . '.' . $extension;
|
||
|
|
}
|
||
|
|
|
||
|
|
// if needed strip any not needed characters from the storedAs, this should be just <mediaId>.<extension>
|
||
|
|
if (strpos(basename($saveName), '?')) {
|
||
|
|
$saveName = substr(basename($saveName), 0, strpos(basename($saveName), '?'));
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->storedAs = $saveName;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug('saveFile for "' . $this->name . '" [' . $this->mediaId . '] with storedAs = "'
|
||
|
|
. $this->storedAs . '", fileName = "' . $this->fileName . '" to "' . $saveName . '". Always Copy = "'
|
||
|
|
. $this->alwaysCopy . '", Cloned = "' . $this->cloned . '"');
|
||
|
|
|
||
|
|
// If the storesAs is empty, then set it to be the moved file name
|
||
|
|
if (empty($this->storedAs) && !$this->alwaysCopy) {
|
||
|
|
// We could be a fresh file entirely, or we could be a clone
|
||
|
|
if ($this->cloned) {
|
||
|
|
$this->getLog()->debug('Copying cloned file: ' . $libraryFolder . $this->fileName);
|
||
|
|
// Copy the file into the library
|
||
|
|
if (!@copy($libraryFolder . $this->fileName, $libraryFolder . $saveName)) {
|
||
|
|
throw new ConfigurationException(__('Problem copying file in the Library Folder'));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$this->getLog()->debug('Moving temporary file: ' . $libraryFolder . 'temp/' . $this->fileName);
|
||
|
|
// Move the file into the library
|
||
|
|
if (!$this->moveFile($libraryFolder . 'temp/' . $this->fileName, $libraryFolder . $saveName)) {
|
||
|
|
throw new ConfigurationException(__('Problem moving uploaded file into the Library Folder'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set the storedAs
|
||
|
|
$this->storedAs = $saveName;
|
||
|
|
} else {
|
||
|
|
// We have pre-defined where we want this to be stored
|
||
|
|
if (empty($this->storedAs)) {
|
||
|
|
// Assume we want to set this automatically (i.e. we are set to always copy)
|
||
|
|
$this->storedAs = $saveName;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($this->isRemote) {
|
||
|
|
$this->getLog()->debug('Moving temporary file: ' . $libraryFolder . 'temp/' . $this->name);
|
||
|
|
|
||
|
|
// Move the file into the library
|
||
|
|
if (!$this->moveFile($libraryFolder . 'temp/' . $this->name, $libraryFolder . $this->storedAs)) {
|
||
|
|
throw new ConfigurationException(__('Problem moving downloaded file into the Library Folder'));
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$this->getLog()->debug('Copying specified file: ' . $this->fileName);
|
||
|
|
|
||
|
|
if (!@copy($this->fileName, $libraryFolder . $this->storedAs)) {
|
||
|
|
$this->getLog()->error(sprintf('Cannot copy %s to %s', $this->fileName, $libraryFolder . $this->storedAs));
|
||
|
|
throw new ConfigurationException(__('This media has expired and cannot be replaced.'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Work out the MD5
|
||
|
|
$this->md5 = md5_file($libraryFolder . $this->storedAs);
|
||
|
|
$this->fileSize = filesize($libraryFolder . $this->storedAs);
|
||
|
|
|
||
|
|
// Set to valid
|
||
|
|
$this->valid = 1;
|
||
|
|
|
||
|
|
// Resize image dimensions if threshold exceeds
|
||
|
|
// This also sets orientation
|
||
|
|
$this->assessDimensions();
|
||
|
|
|
||
|
|
// Update the MD5 and storedAs to suit
|
||
|
|
$this->getStore()->update('
|
||
|
|
UPDATE `media`
|
||
|
|
SET md5 = :md5,
|
||
|
|
fileSize = :fileSize,
|
||
|
|
storedAs = :storedAs,
|
||
|
|
expires = :expires,
|
||
|
|
released = :released,
|
||
|
|
orientation = :orientation,
|
||
|
|
width = :width,
|
||
|
|
height = :height,
|
||
|
|
valid = :valid
|
||
|
|
WHERE mediaId = :mediaId
|
||
|
|
', [
|
||
|
|
'fileSize' => $this->fileSize,
|
||
|
|
'md5' => $this->md5,
|
||
|
|
'storedAs' => $this->storedAs,
|
||
|
|
'expires' => $this->expires,
|
||
|
|
'released' => $this->released,
|
||
|
|
'orientation' => $this->orientation,
|
||
|
|
'valid' => $this->valid,
|
||
|
|
'width' => ($this->width === null) ? null : $this->width,
|
||
|
|
'height' => ($this->height === null) ? null : $this->height,
|
||
|
|
'mediaId' => $this->mediaId,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
private function assessDimensions(): void
|
||
|
|
{
|
||
|
|
if ($this->mediaType === 'image' || ($this->mediaType === 'module' && $this->moduleSystemFile === 0)) {
|
||
|
|
$libraryFolder = $this->config->getSetting('LIBRARY_LOCATION');
|
||
|
|
$filePath = $libraryFolder . $this->storedAs;
|
||
|
|
list($imgWidth, $imgHeight) = @getimagesize($filePath);
|
||
|
|
|
||
|
|
$resizeThreshold = $this->config->getSetting('DEFAULT_RESIZE_THRESHOLD');
|
||
|
|
$resizeLimit = $this->config->getSetting('DEFAULT_RESIZE_LIMIT');
|
||
|
|
|
||
|
|
$this->width = $imgWidth;
|
||
|
|
$this->height = $imgHeight;
|
||
|
|
$this->orientation = ($imgWidth >= $imgHeight) ? 'landscape' : 'portrait';
|
||
|
|
|
||
|
|
// Media released set to 0 for large size images
|
||
|
|
// if image size is greater than Resize Limit then we flag that image as too big
|
||
|
|
if ($resizeLimit > 0 && ($imgWidth > $resizeLimit || $imgHeight > $resizeLimit)) {
|
||
|
|
$this->released = 2;
|
||
|
|
$this->getLog()->debug('Image size is too big. MediaId '. $this->mediaId);
|
||
|
|
} elseif ($resizeThreshold > 0) {
|
||
|
|
if ($imgWidth > $imgHeight) { // 'landscape';
|
||
|
|
if ($imgWidth <= $resizeThreshold) {
|
||
|
|
$this->released = 1;
|
||
|
|
} else {
|
||
|
|
if ($resizeThreshold > 0) {
|
||
|
|
$this->released = 0;
|
||
|
|
$this->getLog()->debug('Image exceeded threshold, released set to 0. MediaId '. $this->mediaId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else { // 'portrait';
|
||
|
|
if ($imgHeight <= $resizeThreshold) {
|
||
|
|
$this->released = 1;
|
||
|
|
} else {
|
||
|
|
if ($resizeThreshold > 0) {
|
||
|
|
$this->released = 0;
|
||
|
|
$this->getLog()->debug('Image exceeded threshold, released set to 0. MediaId '. $this->mediaId);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Release an image from image processing
|
||
|
|
* @param $md5
|
||
|
|
* @param $fileSize
|
||
|
|
* @param $height
|
||
|
|
* @param $width
|
||
|
|
*/
|
||
|
|
public function release($md5, $fileSize, $height, $width)
|
||
|
|
{
|
||
|
|
// Update the img record
|
||
|
|
$this->getStore()->update('UPDATE `media` SET md5 = :md5, fileSize = :fileSize, released = :released, height = :height, width = :width, modifiedDt = :modifiedDt WHERE mediaId = :mediaId', [
|
||
|
|
'fileSize' => $fileSize,
|
||
|
|
'md5' => $md5,
|
||
|
|
'released' => 1,
|
||
|
|
'mediaId' => $this->mediaId,
|
||
|
|
'height' => $height,
|
||
|
|
'width' => $width,
|
||
|
|
'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat())
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete a Library File
|
||
|
|
*/
|
||
|
|
private function deleteFile()
|
||
|
|
{
|
||
|
|
// Make sure storedAs isn't null
|
||
|
|
if ($this->storedAs == null) {
|
||
|
|
$this->getLog()->error(sprintf('Deleting media [%s] with empty stored as. Skipping library file delete.', $this->name));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Library location
|
||
|
|
$libraryLocation = $this->config->getSetting("LIBRARY_LOCATION");
|
||
|
|
|
||
|
|
// 3 things to check for..
|
||
|
|
// the actual file, the thumbnail, the background
|
||
|
|
// video cover image and its thumbnail
|
||
|
|
if (file_exists($libraryLocation . $this->storedAs)) {
|
||
|
|
unlink($libraryLocation . $this->storedAs);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (file_exists($libraryLocation . 'tn_' . $this->storedAs)) {
|
||
|
|
unlink($libraryLocation . 'tn_' . $this->storedAs);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (file_exists($libraryLocation . 'tn_' . $this->mediaId . '_videocover.png')) {
|
||
|
|
unlink($libraryLocation . 'tn_' . $this->mediaId . '_videocover.png');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (file_exists($libraryLocation . $this->mediaId . '_videocover.png')) {
|
||
|
|
unlink($libraryLocation . $this->mediaId . '_videocover.png');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Workaround for moving files across file systems
|
||
|
|
* @param $from
|
||
|
|
* @param $to
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
private function moveFile($from, $to)
|
||
|
|
{
|
||
|
|
// Try to move the file first
|
||
|
|
$moved = rename($from, $to);
|
||
|
|
|
||
|
|
if (!$moved) {
|
||
|
|
$this->getLog()->info('Cannot move file: ' . $from . ' to ' . $to . ', will try and copy/delete instead.');
|
||
|
|
|
||
|
|
// Copy
|
||
|
|
$moved = copy($from, $to);
|
||
|
|
|
||
|
|
// Delete
|
||
|
|
if (!@unlink($from)) {
|
||
|
|
$this->getLog()->error('Cannot delete file: ' . $from . ' after copying to ' . $to);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $moved;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Download URL
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public function downloadUrl()
|
||
|
|
{
|
||
|
|
return $this->fileName;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Download Sink
|
||
|
|
* @return string
|
||
|
|
*/
|
||
|
|
public function downloadSink($temp = true)
|
||
|
|
{
|
||
|
|
return $this->config->getSetting('LIBRARY_LOCATION')
|
||
|
|
. ($temp ? 'temp' . DIRECTORY_SEPARATOR : '')
|
||
|
|
. $this->name;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get optional options for downloading media files
|
||
|
|
* @return array
|
||
|
|
*/
|
||
|
|
public function downloadRequestOptions()
|
||
|
|
{
|
||
|
|
return $this->requestOptions;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update Media duration.
|
||
|
|
* This is called on processDownloads when uploading video/audio from url
|
||
|
|
* Real duration can be determined in determineRealDuration function in MediaFactory
|
||
|
|
* @param int $realDuration
|
||
|
|
* @return Media
|
||
|
|
*/
|
||
|
|
public function updateDuration(int $realDuration): Media
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('Updating duration for MediaId '. $this->mediaId);
|
||
|
|
|
||
|
|
$this->getStore()->update('UPDATE `media` SET duration = :duration WHERE mediaId = :mediaId', [
|
||
|
|
'duration' => $realDuration,
|
||
|
|
'mediaId' => $this->mediaId
|
||
|
|
]);
|
||
|
|
|
||
|
|
$this->duration = $realDuration;
|
||
|
|
|
||
|
|
return $this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Update Media orientation.
|
||
|
|
* For videos from Library connectors, update the orientation once we have the cover image saved.
|
||
|
|
* @param int $width
|
||
|
|
* @param int $height
|
||
|
|
* @return Media
|
||
|
|
*/
|
||
|
|
public function updateOrientation(int $width, int $height): Media
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('Updating orientation and resolution for MediaId '. $this->mediaId);
|
||
|
|
|
||
|
|
$this->width = $width;
|
||
|
|
$this->height = $height;
|
||
|
|
$this->orientation = ($width >= $height) ? 'landscape' : 'portrait';
|
||
|
|
|
||
|
|
$this->getStore()->update('
|
||
|
|
UPDATE `media` SET orientation = :orientation, width = :width, height = :height
|
||
|
|
WHERE mediaId = :mediaId
|
||
|
|
', [
|
||
|
|
'orientation' => $this->orientation,
|
||
|
|
'width' => $this->width,
|
||
|
|
'height' => $this->height,
|
||
|
|
'mediaId' => $this->mediaId
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this;
|
||
|
|
}
|
||
|
|
}
|