diff --git a/.php_cs b/.php_cs index e68b9be..fab371b 100644 --- a/.php_cs +++ b/.php_cs @@ -10,4 +10,4 @@ return PhpCsFixer\Config::create() 'array_syntax' => ['syntax' => 'short'], 'align_multiline_comment' => ['comment_type' => 'phpdocs_like'], ]) - ->setFinder($finder); \ No newline at end of file + ->setFinder($finder); diff --git a/Classes/Backend/ImageManipulationElement.php b/Classes/Backend/ImageManipulationElement.php index e584202..989a419 100644 --- a/Classes/Backend/ImageManipulationElement.php +++ b/Classes/Backend/ImageManipulationElement.php @@ -16,6 +16,10 @@ */ class ImageManipulationElement extends \TYPO3\CMS\Backend\Form\Element\ImageManipulationElement { + /** + * @return array + * @throws \TYPO3\CMS\Core\Imaging\ImageManipulation\InvalidConfigurationException + */ public function render() { $resultArray = $this->initializeResultArray(); diff --git a/Classes/Backend/InlineControlContainer.php b/Classes/Backend/InlineControlContainer.php index e5b69a5..0c1e79d 100644 --- a/Classes/Backend/InlineControlContainer.php +++ b/Classes/Backend/InlineControlContainer.php @@ -8,6 +8,7 @@ * All code (c) Beech.it all rights reserved */ use BeechIt\Bynder\Resource\BynderDriver; +use BeechIt\Bynder\Utility\ConfigurationUtility; use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Resource\ResourceStorage; @@ -63,11 +64,15 @@ protected function renderBynderButton(array $inlineConfiguration): string $foreign_table = $inlineConfiguration['foreign_table']; $allowed = $groupFieldConfiguration['allowed']; + $allowedAssetTypes = ConfigurationUtility::getAssetTypesByAllowedElements($groupFieldConfiguration['appearance']['elementBrowserAllowed']); $currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']); $objectPrefix = $currentStructureDomObjectIdPrefix . '-' . $foreign_table; $nameObject = $currentStructureDomObjectIdPrefix; - $compactViewUrl = BackendUtility::getModuleUrl('bynder_compact_view', ['element' => 'bynder' . $this->inlineData['config'][$nameObject]['md5']]); + $compactViewUrl = BackendUtility::getModuleUrl('bynder_compact_view', [ + 'element' => 'bynder' . $this->inlineData['config'][$nameObject]['md5'], + 'assetTypes' => implode(',', $allowedAssetTypes) + ]); $this->requireJsModules[] = 'TYPO3/CMS/Bynder/CompactView'; $buttonText = htmlspecialchars($languageService->sL('LLL:EXT:bynder/Resources/Private/Language/locallang_be.xlf:compact_view.button')); @@ -105,4 +110,4 @@ protected function bynderStorageAvailable(): bool } return false; } -} \ No newline at end of file +} diff --git a/Classes/Controller/CompactViewController.php b/Classes/Controller/CompactViewController.php index ac488f4..cfdca96 100644 --- a/Classes/Controller/CompactViewController.php +++ b/Classes/Controller/CompactViewController.php @@ -8,10 +8,11 @@ * All code (c) Beech.it all rights reserved */ +use BeechIt\Bynder\Utility\ConfigurationUtility; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use BeechIt\Bynder\Service\BynderService; use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; +use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\Index\Indexer; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -22,6 +23,7 @@ */ class CompactViewController { + /** * Fluid Standalone View * @@ -50,15 +52,12 @@ class CompactViewController */ protected $layoutRootPaths = ['EXT:bynder/Resources/Private/Layouts/CompactView']; + /** - * @var BynderService + * CompactViewController constructor. */ - protected $bynderService; - public function __construct() { - $this->bynderService = GeneralUtility::makeInstance(BynderService::class); - $this->view = GeneralUtility::makeInstance(StandaloneView::class); $this->view->setPartialRootPaths($this->partialRootPaths); $this->view->setTemplateRootPaths($this->templateRootPaths); @@ -66,6 +65,8 @@ public function __construct() } /** + * Action: Display compact view + * * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface @@ -76,8 +77,9 @@ public function indexAction(ServerRequestInterface $request, ResponseInterface $ $this->view->assignMultiple([ 'language' => $this->getBackendUserAuthentication()->uc['lang'] ?: ($this->getBackendUserAuthentication()->user['lang'] ?: 'en_EN'), - 'apiBaseUrl' => $this->bynderService->getApiBaseUrl(), + 'apiBaseUrl' => ConfigurationUtility::getApiBaseUrl(), 'element' => $request->getQueryParams()['element'], + 'assetTypes' => $request->getQueryParams()['assetTypes'] ]); $response->getBody()->write($this->view->render()); @@ -85,6 +87,13 @@ public function indexAction(ServerRequestInterface $request, ResponseInterface $ return $response; } + /** + * Action: Retrieve file from storage + * + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return ResponseInterface + */ public function getFilesAction(ServerRequestInterface $request, ResponseInterface $response) { $files = []; @@ -92,9 +101,9 @@ public function getFilesAction(ServerRequestInterface $request, ResponseInterfac $fileStorage = $this->getBynderStorage(); foreach ($request->getParsedBody()['files'] ?? [] as $fileIdentifier) { $file = $fileStorage->getFile($fileIdentifier); - if ($file) { + if ($file instanceof File) { // (Re)Fetch metadata - $this->getIndexer($file->getStorage())->extractMetaData($file); + $this->getIndexer($fileStorage)->extractMetaData($file); $files[] = $file->getUid(); } } @@ -107,6 +116,9 @@ public function getFilesAction(ServerRequestInterface $request, ResponseInterfac return $response; } + /** + * @return ResourceStorage + */ protected function getBynderStorage(): ResourceStorage { /** @var ResourceStorage $fileStorage */ @@ -137,4 +149,4 @@ protected function getIndexer(ResourceStorage $storage) { return GeneralUtility::makeInstance(Indexer::class, $storage); } -} \ No newline at end of file +} diff --git a/Classes/Exception/BynderException.php b/Classes/Exception/BynderException.php index abcdcee..8b45e05 100644 --- a/Classes/Exception/BynderException.php +++ b/Classes/Exception/BynderException.php @@ -10,4 +10,4 @@ abstract class BynderException extends \Exception { -} \ No newline at end of file +} diff --git a/Classes/Exception/InvalidExtensionConfigurationException.php b/Classes/Exception/InvalidExtensionConfigurationException.php index 20aa69d..d68e1bf 100644 --- a/Classes/Exception/InvalidExtensionConfigurationException.php +++ b/Classes/Exception/InvalidExtensionConfigurationException.php @@ -10,4 +10,4 @@ class InvalidExtensionConfigurationException extends BynderException { -} \ No newline at end of file +} diff --git a/Classes/Exception/NotImplementedException.php b/Classes/Exception/NotImplementedException.php index d28aaa7..e89fe57 100644 --- a/Classes/Exception/NotImplementedException.php +++ b/Classes/Exception/NotImplementedException.php @@ -10,4 +10,4 @@ class NotImplementedException extends BynderException { -} \ No newline at end of file +} diff --git a/Classes/Hook/DataHandlerHook.php b/Classes/Hook/DataHandlerHook.php index f35e21c..25819bd 100644 --- a/Classes/Hook/DataHandlerHook.php +++ b/Classes/Hook/DataHandlerHook.php @@ -134,4 +134,4 @@ protected function getUsageReference(string $table, int $uid): string return $table; } } -} \ No newline at end of file +} diff --git a/Classes/Resource/AssetProcessing.php b/Classes/Resource/AssetProcessing.php deleted file mode 100644 index 41c457c..0000000 --- a/Classes/Resource/AssetProcessing.php +++ /dev/null @@ -1,269 +0,0 @@ -isNew() - || (!$processedFile->usesOriginalFile() && !$processedFile->exists()) - || $processedFile->isOutdated(); - } - - /** - * Create url for scalled/cropped versions of Bynder assets - * - * @param FileProcessingService $fileProcessingService - * @param DriverInterface $driver - * @param ProcessedFile $processedFile - * @param File $file - * @param $taskType - * @param array $configuration - * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException - */ - public function processFile(FileProcessingService $fileProcessingService, DriverInterface $driver, ProcessedFile $processedFile, File $file, $taskType, array $configuration) - { - if ($file->getStorage()->getDriverType() !== BynderDriver::KEY) { - return; - } - - if (!$this->needsReprocessing($processedFile)) { - return; - } - - try { - $mediaInfo = $this->getBynderService()->getMediaInfo($processedFile->getOriginalFile()->getIdentifier()); - } catch (ClientException $e) { - $mediaInfo = [ - 'isPublic' => false, - 'thumbnails' => [ - 'thul' => '', - 'webimage' => '', - 'mini' => '', - ] - ]; - } - - $processingConfiguration = $processedFile->getProcessingConfiguration(); - // The CONTEXT_IMAGEPREVIEW task only gives max dimensions - if ($taskType === ProcessedFile::CONTEXT_IMAGEPREVIEW) { - if (!empty($processingConfiguration['width'])) { - $processingConfiguration['width'] .= 'm'; - } - if (!empty($processingConfiguration['height'])) { - $processingConfiguration['height'] .= 'm'; - } - } - - $fileInfo = $this->getThumbnailInfo( - $processingConfiguration, - (int)$processedFile->getOriginalFile()->getProperty('width'), - (int)$processedFile->getOriginalFile()->getProperty('height'), - $mediaInfo['isPublic'] ? $this->getBynderService()->getOTFBaseUrl() . $processedFile->getOriginalFile()->getIdentifier() : '', - $mediaInfo['thumbnails'] - ); - - // Fetch OTF url to trigger generation of the derivative - if ($fileInfo['type'] === 'otf') { - GeneralUtility::getUrl($fileInfo['url']); - self::$lastRequestedOtfAsset = microtime(true); - } - - $processedFile->setUsesOriginalFile(); - $checksum = $processedFile->getTask()->getConfigurationChecksum(); - $processedFile->setIdentifier('processed_' . $file->getIdentifier() . '_' . $fileInfo['type'] . '_' . $checksum); - - // Update existing processed file - $processedFile->updateProperties( - [ - 'bynder' => true, - 'width' => $fileInfo['width'], - 'height' => $fileInfo['height'], - 'checksum' => $checksum, - 'bynder_url' => $fileInfo['url'], - ] - ); - - // Persist processed file like done in FileProcessingService::process() - $processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class); - $processedFileRepository->add($processedFile); - } - - /** - * Calculate the best suitable/available dimensions for the requested file configuration - * - * @param array $configuration - * @param int $orgWidth - * @param int $orgHeight - * @param string $otfBaseUrl - * @param array $derivatives - * @return array - */ - protected function getThumbnailInfo(array $configuration, int $orgWidth, int $orgHeight, string $otfBaseUrl, array $derivatives): array - { - $rawWidth = $configuration['width'] ?? $configuration['maxWidth'] ?? 0; - $rawHeight = $configuration['height'] ?? $configuration['maxHeight'] ?? 0; - - $keepRatio = true; - $crop = false; - $otf = $otfBaseUrl !== ''; - - // When width and height are set and non of them have a 'm' suffix we don't keep existing ratio - if ($rawWidth && $rawHeight && strpos($rawWidth . $rawWidth, 'm') < 0) { - $keepRatio = false; - } - - // When width and height are set and one of then have a 'c' suffix we don't keep existing ratio and allow cropping - if ($rawWidth && $rawHeight && strpos($rawWidth . $rawWidth, 'c') >= 0) { - $keepRatio = false; - $crop = true; - } - - $width = (int)$rawWidth; - $height = (int)$rawHeight; - - if (!$keepRatio && $width > $orgWidth) { - $height = $this->calculateRelativeDimension($width, $height, $orgWidth); - $width = $orgWidth; - } elseif (!$keepRatio && $height > $orgHeight) { - $width = $this->calculateRelativeDimension($height, $width, $orgHeight); - $height = $orgHeight; - } elseif ($keepRatio && $width > $orgWidth) { - $height = $orgWidth / $width * $height; - } elseif ($keepRatio && $height > $orgHeight) { - $height = $orgHeight / $height * $width; - } elseif ($width === 0 && $height > 0) { - $height = $this->calculateRelativeDimension($orgWidth, $orgHeight, $width); - } elseif ($width === 0 && $height > 0) { - $width = $this->calculateRelativeDimension($orgHeight, $orgWidth, $height); - } - - if ($otf) { - return [ - 'type' => 'otf', - 'width' => $width ?: $orgWidth, - 'height' => $height ?: $orgHeight, - 'url' => $otfBaseUrl . '?' . http_build_query([ - 'w' => $width ?: '', - 'h' => $height ?: '', - 'crop' => $crop ? 'true' : 'false' - ]), - ]; - } - - $default = [ - 'type' => 'webimage', - 'width' => $width, - 'height' => $height, - 'url' => $derivatives['webimage'], - ]; - if ($height === 0 && $width === 0) { - return $default; - } - if ($width <= 80) { - return [ - 'type' => 'mini', - 'width' => $width, - 'height' => $width, // derivative/image is square - 'url' => $derivatives['mini'], - ]; - } elseif ($width <= 250) { - return [ - 'type' => 'thul', - 'width' => $width, - 'height' => $height, - 'url' => $derivatives['thul'], - ]; - } - - return $default; - } - - /** - * Calculate relative dimension - * - * For instance you have the original width, height and new width. - * And want to calculate the new height with the same ratio as the original dimensions - * - * @param int $orgA - * @param int $orgB - * @param int $newA - * @return int - */ - protected function calculateRelativeDimension(int $orgA, int $orgB, int $newA): int - { - if ($newA === 0) { - return $orgB; - } - - return (int)($orgB / ($orgA / $newA)); - } - - /** - * @return BynderService - * @throws \InvalidArgumentException - */ - protected function getBynderService(): BynderService - { - if ($this->bynderService === null) { - $this->bynderService = GeneralUtility::makeInstance(BynderService::class); - } - - return $this->bynderService; - } - - /** - * Bynder needs some time to process the OTF images - */ - public function __destruct() - { - if (!self::$lastRequestedOtfAsset) { - return; - } - $difference = microtime(true) - (float)self::$lastRequestedOtfAsset; - if ($difference < 3) { - sleep(ceil(3 - $difference)); - } - } -} diff --git a/Classes/Resource/BynderDriver.php b/Classes/Resource/BynderDriver.php index 11ce6e4..76f0398 100644 --- a/Classes/Resource/BynderDriver.php +++ b/Classes/Resource/BynderDriver.php @@ -8,25 +8,25 @@ * Date: 19-2-18 * All code (c) Beech.it all rights reserved */ -use BeechIt\Bynder\Service\BynderService; -use Bynder\Api\Impl\AssetBankManager; +use BeechIt\Bynder\Exception\NotImplementedException; +use BeechIt\Bynder\Resource\Helper\BynderHelper; use TYPO3\CMS\Core\Resource\Driver\DriverInterface; use TYPO3\CMS\Core\Resource\Exception; use TYPO3\CMS\Core\Resource\ResourceStorage; use TYPO3\CMS\Core\Utility\GeneralUtility; -use BeechIt\Bynder\Exception\NotImplementedException; +use TYPO3\CMS\Extbase\Utility\DebuggerUtility; /** * Class BynderDriver */ class BynderDriver implements DriverInterface { + use \BeechIt\Bynder\Traits\BynderService; + const KEY = 'bynder'; - /** - * @var array - */ - protected static $tempFiles = []; + const ASSET_TYPE_VIDEO = 'video'; + const ASSET_TYPE_IMAGE = 'image'; /** * @var string @@ -55,28 +55,8 @@ class BynderDriver implements DriverInterface protected $configuration = []; /** - * @var AssetBankManager - */ - protected $assetBankManager; - - /** - * @var BynderService + * Processes the configuration for this driver. */ - protected $bynderService; - - /** - * Creates this object. - * - * @param array $configuration - */ - public function __construct(array $configuration = []) - { - $this->capabilities = - ResourceStorage::CAPABILITY_BROWSABLE - | ResourceStorage::CAPABILITY_PUBLIC - | ResourceStorage::CAPABILITY_WRITABLE; - } - public function processConfiguration() { } @@ -120,8 +100,16 @@ public function getCapabilities() return $this->capabilities; } + /** + * Initializes this object. This is called by the storage after the driver + * has been attached. + */ public function initialize() { + $this->capabilities = + ResourceStorage::CAPABILITY_BROWSABLE + | ResourceStorage::CAPABILITY_PUBLIC + | ResourceStorage::CAPABILITY_WRITABLE; } /** @@ -152,9 +140,11 @@ public function isCaseSensitiveFileSystem() } /** + * Cleans a fileName from not allowed characters + * * @param string $fileName - * @param string $charset - * @return string + * @param string $charset Charset of the a fileName (defaults to current charset; depending on context) + * @return string the cleaned filename */ public function sanitizeFileName($fileName, $charset = '') { @@ -163,15 +153,21 @@ public function sanitizeFileName($fileName, $charset = '') } /** + * Hashes a file identifier, taking the case sensitivity of the file system + * into account. This helps mitigating problems with case-insensitive + * databases. + * * @param string $identifier * @return string */ public function hashIdentifier($identifier) { - return sha1($identifier); + return $this->hash($identifier, 'sha1'); } /** + * Returns the identifier of the root level folder of the storage. + * * @return string */ public function getRootLevelFolder() @@ -180,6 +176,8 @@ public function getRootLevelFolder() } /** + * Returns the identifier of the default folder new files should be put into. + * * @return string */ public function getDefaultFolder() @@ -207,63 +205,101 @@ public function getParentFolderIdentifierOfIdentifier($fileIdentifier) */ public function getPublicUrl($identifier) { + DebuggerUtility::var_dump(__METHOD__); + return $identifier; + $format = ''; if (preg_match('/^processed_([0-9A-Z\-]{35})_([a-z]+)/', $identifier, $matches)) { $identifier = $matches[1]; $format = $matches[2]; } - if (!in_array($format, ['mini', 'thul', 'webimage'])) { - $format = 'webimage'; - } - try { $fileInfo = $this->getBynderService()->getMediaInfo($identifier); - return $fileInfo['thumbnails'][$format]; + switch ($fileInfo['type']) { + case BynderDriver::ASSET_TYPE_IMAGE: + if (!in_array($format, ['mini', 'thul', 'webimage'])) { + $format = 'webimage'; + } + return $fileInfo['thumbnails'][$format]; + case BynderDriver::ASSET_TYPE_VIDEO: + $urls = array_filter($fileInfo['videoPreviewURLs'], function ($url) { + return preg_match('/mp4$/i', $url); + }); + if (empty($urls)) { + throw new Exception\FileDoesNotExistException( + 'mp4 not found in video URL\'s', + 1530626116454 + ); + } + return reset($urls); + } } catch (\Exception $exception) { throw new Exception\FileDoesNotExistException( - sprintf('Requested file "%s" coudn\'t be found', $identifier), + sprintf('Requested file "%s" couldn\'t be found', $identifier), 1519115242, $exception ); } } + /** + * Creates a folder, within a parent folder. + * If no parent folder is given, a root level folder will be created + * + * @param string $newFolderName + * @param string $parentFolderIdentifier + * @param bool $recursive + * @return string the Identifier of the new folder + * @throws NotImplementedException + */ public function createFolder($newFolderName, $parentFolderIdentifier = '', $recursive = false) { throw new NotImplementedException(sprintf('Method %s::%s() is not implemented', __CLASS__, __METHOD__), 1519045381); } + /** + * Renames a folder in this storage. + * + * @param string $folderIdentifier + * @param string $newName + * @return array A map of old to new file identifiers of all affected resources + * @throws NotImplementedException + */ public function renameFolder($folderIdentifier, $newName) { throw new NotImplementedException(sprintf('Method %s::%s() is not implemented', __CLASS__, __METHOD__), 1519045382); } + /** + * Removes a folder in filesystem. + * + * @param string $folderIdentifier + * @param bool $deleteRecursively + * @return bool + * @throws NotImplementedException + */ public function deleteFolder($folderIdentifier, $deleteRecursively = false) { throw new NotImplementedException(sprintf('Method %s::%s() is not implemented', __CLASS__, __METHOD__), 1519045383); } + /** + * Checks if a file exists. + * * @param string $fileIdentifier * @return bool */ public function fileExists($fileIdentifier) { // We just assume that the processed file exists as this is just a CDN link - if ($this->isProcessedFile($fileIdentifier)) { - return true; - } - - try { - $asset = $this->getBynderService()->getMediaInfo($fileIdentifier); - return true; - } catch (\Exception $exception) { - return false; - } + return (!empty($fileIdentifier)); } /** + * Checks if a folder exists. + * * @param string $folderIdentifier * @return bool */ @@ -274,23 +310,29 @@ public function folderExists($folderIdentifier) } /** + * Checks if a folder contains files and (if supported) other folders. + * * @param string $folderIdentifier - * @return bool + * @return bool TRUE if there are no files and folders within $folder */ public function isFolderEmpty($folderIdentifier) { - // We just say that every folder has some content as - // Bynder doesn't know the concept of folders and we don't want - // any call like deleteFolder() - return false; + // We only know the root folder + return $folderIdentifier === $this->rootFolder; } /** - * @param string $localFilePath + * Adds a file from the local server hard disk to a given path in TYPO3s + * virtual file system. This assumes that the local file exists, so no + * further check is done here! After a successful the original file must + * not exist anymore. + * + * @param string $localFilePath (within PATH_site) * @param string $targetFolderIdentifier - * @param string $newFileName - * @param bool $removeOriginal - * @return string|void + * @param string $newFileName optional, if not given original name is used + * @param bool $removeOriginal if set the original file will be removed + * after successful operation + * @return string the identifier of the new file * @throws NotImplementedException */ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = '', $removeOriginal = true) @@ -299,9 +341,11 @@ public function addFile($localFilePath, $targetFolderIdentifier, $newFileName = } /** + * Creates a new (empty) file and returns the identifier. + * * @param string $fileName * @param string $parentFolderIdentifier - * @return string|void + * @return string * @throws NotImplementedException */ public function createFile($fileName, $parentFolderIdentifier) @@ -310,10 +354,14 @@ public function createFile($fileName, $parentFolderIdentifier) } /** + * Copies a file *within* the current storage. + * Note that this is only about an inner storage copy action, + * where a file is just copied to another folder in the same storage. + * * @param string $fileIdentifier * @param string $targetFolderIdentifier * @param string $fileName - * @return string|void + * @return string the Identifier of the new file * @throws NotImplementedException */ public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $fileName) @@ -322,9 +370,11 @@ public function copyFileWithinStorage($fileIdentifier, $targetFolderIdentifier, } /** + * Renames a file in this storage. + * * @param string $fileIdentifier - * @param string $newName - * @return string|void + * @param string $newName The target path (including the file name!) + * @return string The identifier of the file after renaming * @throws NotImplementedException */ public function renameFile($fileIdentifier, $newName) @@ -333,9 +383,11 @@ public function renameFile($fileIdentifier, $newName) } /** + * Replaces a file with file in local file system. + * * @param string $fileIdentifier * @param string $localFilePath - * @return bool|void + * @return bool TRUE if the operation succeeded * @throws NotImplementedException */ public function replaceFile($fileIdentifier, $localFilePath) @@ -344,8 +396,12 @@ public function replaceFile($fileIdentifier, $localFilePath) } /** + * Removes a file from the filesystem. This does not check if the file is + * still used or if it is a bad idea to delete it for some other reason + * this has to be taken care of in the upper layers (e.g. the Storage)! + * * @param string $fileIdentifier - * @return bool + * @return bool TRUE if deleting the file succeeded * @throws NotImplementedException */ public function deleteFile($fileIdentifier) @@ -385,10 +441,14 @@ public function hash($fileIdentifier, $hashAlgorithm) } /** + * Moves a file *within* the current storage. + * Note that this is only about an inner-storage move action, + * where a file is just moved to another folder in the same storage. + * * @param string $fileIdentifier * @param string $targetFolderIdentifier * @param string $newFileName - * @return string|void + * @return string * @throws NotImplementedException */ public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, $newFileName) @@ -397,10 +457,12 @@ public function moveFileWithinStorage($fileIdentifier, $targetFolderIdentifier, } /** + * Folder equivalent to moveFileWithinStorage(). + * * @param string $sourceFolderIdentifier * @param string $targetFolderIdentifier * @param string $newFolderName - * @return array|void + * @return array All files which are affected, map of old => new file identifiers * @throws NotImplementedException */ public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) @@ -409,10 +471,12 @@ public function moveFolderWithinStorage($sourceFolderIdentifier, $targetFolderId } /** + * Folder equivalent to copyFileWithinStorage(). + * * @param string $sourceFolderIdentifier * @param string $targetFolderIdentifier * @param string $newFolderName - * @return bool|void + * @return void * @throws NotImplementedException */ public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderIdentifier, $newFolderName) @@ -421,28 +485,26 @@ public function copyFolderWithinStorage($sourceFolderIdentifier, $targetFolderId } /** + * Returns the contents of a file. Beware that this requires to load the + * complete file into memory and also may require fetching the file from an + * external location. So this might be an expensive operation (both in terms + * of processing resources and money) for large files. + * * @param string $fileIdentifier - * @return string - * @throws Exception\FileDoesNotExistException + * @return string The file contents + * @throws NotImplementedException */ public function getFileContents($fileIdentifier) { - try { - $downloadLocation = $this->getAssetBankManager()->getMediaDownloadLocation($fileIdentifier)->wait(); - return file_get_contents($downloadLocation['s3_file']); - } catch (\Exception $exception) { - throw new Exception\FileDoesNotExistException( - sprintf('Requested file "%s" coudn\'t be found', $fileIdentifier), - 1519115242, - $exception - ); - } + throw new NotImplementedException(sprintf('Method %s::%s() is not implemented', __CLASS__, __METHOD__), 1530716278); } /** + * Sets the contents of a file to the specified value. + * * @param string $fileIdentifier * @param string $contents - * @return int|void + * @return int The number of bytes written to the file * @throws NotImplementedException */ public function setFileContents($fileIdentifier, $contents) @@ -451,21 +513,20 @@ public function setFileContents($fileIdentifier, $contents) } /** + * Checks if a file inside a folder exists + * * @param string $fileName * @param string $folderIdentifier * @return bool */ public function fileExistsInFolder($fileName, $folderIdentifier) { - if ($folderIdentifier !== $this->rootFolder) { - return false; - } - - // @todo: find a file by name instead of identifier - return false; + return !empty($fileName) && ($this->rootFolder === $folderIdentifier); } /** + * Checks if a folder inside a folder exists. + * * @param string $folderName * @param string $folderIdentifier * @return bool @@ -476,12 +537,27 @@ public function folderExistsInFolder($folderName, $folderIdentifier) return false; } + /** + * Returns a path to a local copy of a file for processing it. When changing the + * file, you have to take care of replacing the current version yourself! + * + * @param string $fileIdentifier + * @param bool $writable Set this to FALSE if you only need the file for read + * operations. This might speed up things, e.g. by using + * a cached local version. Never modify the file if you + * have set this flag! + * @return string The path to the file on the local disk + * @throws NotImplementedException + */ public function getFileForLocalProcessing($fileIdentifier, $writable = true) { - return $this->saveFileToTemporaryPath($fileIdentifier); + throw new NotImplementedException(sprintf('Method %s::%s() is not implemented', __CLASS__, __METHOD__), 1530778712); } /** + * Returns the permissions of a file/folder as an array + * (keys r, w) of boolean flags + * * @param string $identifier * @return array */ @@ -499,41 +575,38 @@ public function getPermissions($identifier) * buffer before. Will be taken care of by the Storage. * * @param string $identifier - * @throws Exception\FileDoesNotExistException + * @throws NotImplementedException */ public function dumpFileContents($identifier) { - try { - $downloadLocation = $this->getAssetBankManager()->getMediaDownloadLocation($identifier)->wait(); - readfile($downloadLocation['s3_file'], 0); - } catch (\Exception $exception) { - throw new Exception\FileDoesNotExistException( - sprintf('Requested file "%s" coudn\'t be found', $identifier), - 1519115242, - $exception - ); - } + throw new NotImplementedException(sprintf('Method %s::%s() is not implemented', __CLASS__, __METHOD__), 1530716441); } /** + * Checks if a given identifier is within a container, e.g. if + * a file or folder is within another folder. + * This can e.g. be used to check for web-mounts. + * + * Hint: this also needs to return TRUE if the given identifier + * matches the container identifier to allow access to the root + * folder of a filemount. + * * @param string $folderIdentifier - * @param string $identifier - * @return bool + * @param string $identifier identifier to be checked against $folderIdentifier + * @return bool TRUE if $content is within or matches $folderIdentifier */ public function isWithin($folderIdentifier, $identifier) { - if ($folderIdentifier === $this->rootFolder) { - return true; - } else { - return false; - } + return ($folderIdentifier === $this->rootFolder); } /** + * Returns information about a file. + * * @param string $fileIdentifier - * @param array $propertiesToExtract + * @param array $propertiesToExtract Array of properties which are be extracted + * If empty all will be extracted * @return array - * @throws Exception\FileDoesNotExistException */ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtract = []) { @@ -541,20 +614,14 @@ public function getFileInfoByIdentifier($fileIdentifier, array $propertiesToExtr $fileIdentifier = $matches[1]; } - try { - $mediaInfo = $this->getBynderService()->getMediaInfo($fileIdentifier); - } catch (\Exception $exception) { - throw new Exception\FileDoesNotExistException( - sprintf('Requested file "%s" coudn\'t be found', $fileIdentifier), - 1519115242, - $exception - ); - } - - return $this->extractFileInformation($mediaInfo, $propertiesToExtract); + $bynderHelper = GeneralUtility::makeInstance(BynderHelper::class, 'bynder'); + $fileInfo = $bynderHelper->extractMetaData($fileIdentifier, $propertiesToExtract); + return $fileInfo; } /** + * Returns information about a folder. + * * @param string $folderIdentifier * @return array */ @@ -570,9 +637,11 @@ public function getFolderInfoByIdentifier($folderIdentifier) } /** + * Returns the identifier of a file inside the folder + * * @param string $fileName * @param string $folderIdentifier - * @return string + * @return string file identifier */ public function getFileInFolder($fileName, $folderIdentifier) { @@ -580,14 +649,20 @@ public function getFileInFolder($fileName, $folderIdentifier) } /** + * Returns a list of files inside the specified path + * * @param string $folderIdentifier * @param int $start * @param int $numberOfItems * @param bool $recursive - * @param array $filenameFilterCallbacks - * @param string $sort - * @param bool $sortRev - * @return array + * @param array $filenameFilterCallbacks callbacks for filtering the items + * @param string $sort Property name used to sort the items. + * Among them may be: '' (empty, no sorting), name, + * fileext, size, tstamp and rw. + * If a driver does not support the given property, it + * should fall back to "name". + * @param bool $sortRev TRUE to indicate reverse sorting (last to first) + * @return array of FileIdentifiers */ public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $filenameFilterCallbacks = [], $sort = '', $sortRev = false) { @@ -595,9 +670,11 @@ public function getFilesInFolder($folderIdentifier, $start = 0, $numberOfItems = } /** - * @param string $folderName + * Returns the identifier of a folder inside the folder + * + * @param string $folderName The name of the target folder * @param string $folderIdentifier - * @return string + * @return string folder identifier */ public function getFolderInFolder($folderName, $folderIdentifier) { @@ -605,25 +682,34 @@ public function getFolderInFolder($folderName, $folderIdentifier) } /** + * Returns a list of folders inside the specified path + * * @param string $folderIdentifier * @param int $start * @param int $numberOfItems * @param bool $recursive - * @param array $folderNameFilterCallbacks - * @param string $sort - * @param bool $sortRev - * @return array + * @param array $folderNameFilterCallbacks callbacks for filtering the items + * @param string $sort Property name used to sort the items. + * Among them may be: '' (empty, no sorting), name, + * fileext, size, tstamp and rw. + * If a driver does not support the given property, it + * should fall back to "name". + * @param bool $sortRev TRUE to indicate reverse sorting (last to first) + * @return array of Folder Identifier */ public function getFoldersInFolder($folderIdentifier, $start = 0, $numberOfItems = 0, $recursive = false, array $folderNameFilterCallbacks = [], $sort = '', $sortRev = false) { return []; } + /** - * @param string $folderIdentifier + * Returns the number of files inside the specified path + * + * @param string $folderIdentifier * @param bool $recursive - * @param array $filenameFilterCallbacks - * @return int + * @param array $filenameFilterCallbacks callbacks for filtering the items + * @return int Number of files in folder */ public function countFilesInFolder($folderIdentifier, $recursive = false, array $filenameFilterCallbacks = []) { @@ -631,10 +717,12 @@ public function countFilesInFolder($folderIdentifier, $recursive = false, array } /** - * @param string $folderIdentifier + * Returns the number of folders inside the specified path + * + * @param string $folderIdentifier * @param bool $recursive - * @param array $folderNameFilterCallbacks - * @return int + * @param array $folderNameFilterCallbacks callbacks for filtering the items + * @return int Number of folders in folder */ public function countFoldersInFolder($folderIdentifier, $recursive = false, array $folderNameFilterCallbacks = []) { @@ -649,154 +737,4 @@ protected function isProcessedFile(string $fileIdentifier): bool { return (bool)preg_match('/^processed_([0-9A-Z\-]{35})_([a-z]+)/', $fileIdentifier); } - - /** - * @return AssetBankManager - * @throws \InvalidArgumentException - */ - protected function getAssetBankManager(): AssetBankManager - { - if ($this->assetBankManager === null) { - $this->assetBankManager = $this->getBynderService() - ->getBynderApi() - ->getAssetBankManager(); - } - - return $this->assetBankManager; - } - - /** - * @return BynderService - * @throws \InvalidArgumentException - */ - protected function getBynderService(): BynderService - { - if ($this->bynderService === null) { - $this->bynderService = GeneralUtility::makeInstance(BynderService::class); - } - - return $this->bynderService; - } - - /** - * Extracts information about a file from the filesystem. - * - * @param array $mediaInfo as returned from getAssetBankManager()->getMediaInfo() - * @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted - * @return array - */ - protected function extractFileInformation(array $mediaInfo, array $propertiesToExtract = []): array - { - if (empty($propertiesToExtract)) { - $propertiesToExtract = [ - 'size', 'atime', 'mtime', 'ctime', 'mimetype', 'name', 'extension', - 'identifier', 'identifier_hash', 'storage', 'folder_hash' - ]; - } - $fileInformation = []; - foreach ($propertiesToExtract as $property) { - $fileInformation[$property] = $this->getSpecificFileInformation($mediaInfo, $property); - } - return $fileInformation; - } - - /** - * Extracts a specific FileInformation from the FileSystems. - * - * @param array $mediaInfo - * @param string $property - * - * @return bool|int|string - * @throws \InvalidArgumentException - */ - protected function getSpecificFileInformation($mediaInfo, $property) - { - switch ($property) { - case 'size': - return $mediaInfo['fileSize']; - case 'atime': - return strtotime($mediaInfo['dateModified']); - case 'mtime': - return strtotime($mediaInfo['dateModified']); - case 'ctime': - return strtotime($mediaInfo['dateCreated']); - case 'name': - return $mediaInfo['name'] . '.' . $mediaInfo['extension'][0]; - case 'mimetype': - // @todo: find beter way to determine mimetype - return $mediaInfo['type'] . '/' . $mediaInfo['extension'][0]; - case 'identifier': - return $mediaInfo['id']; - case 'extension': - return $mediaInfo['extension'][0]; - case 'storage': - return $this->storageUid; - case 'identifier_hash': - return $this->hashIdentifier($mediaInfo['id']); - case 'folder_hash': - return $this->hashIdentifier(''); - - // Metadata - case 'title': - return $mediaInfo['name']; - case 'description': - return $mediaInfo['description']; - case 'width': - return $mediaInfo['width']; - case 'height': - return $mediaInfo['height']; - case 'copyright': - return $mediaInfo['copyright']; - case 'keywords': - return implode(', ', $mediaInfo['tags'] ?? []); - default: - throw new \InvalidArgumentException(sprintf('The information "%s" is not available.', $property), 1519130380); - } - } - - /** - * Save a file to a temporary path and returns that path. - * - * @param string $fileIdentifier - * @return string The temporary path - * @throws \RuntimeException - * @throws Exception\FileDoesNotExistException - */ - protected function saveFileToTemporaryPath($fileIdentifier): string - { - $temporaryPath = $this->getTemporaryPathForFile($fileIdentifier); - $result = file_put_contents($temporaryPath, $this->getFileContents($fileIdentifier)); - if ($result === false) { - throw new \RuntimeException( - 'Copying file "' . $fileIdentifier . '" to temporary path "' . $temporaryPath . '" failed.', - 1519208427 - ); - } - return $temporaryPath; - } - - /** - * Returns a temporary path for a given file, including the file extension. - * - * @param string $fileIdentifier - * @return string - * @throws Exception\FileDoesNotExistException - */ - protected function getTemporaryPathForFile($fileIdentifier): string - { - list($fileExtension) = $this->getFileInfoByIdentifier($fileIdentifier, ['extension']); - $tempFile = GeneralUtility::tempnam('fal-tempfile-', '.' . $fileExtension); - - return self::$tempFiles[] = $tempFile; - } - - /** - * Cleanup temp files that a still present - */ - public function __destruct() - { - foreach (self::$tempFiles as $tempFile) { - GeneralUtility::unlink_tempfile($tempFile); - } - } -} \ No newline at end of file +} diff --git a/Classes/Resource/Helper/BynderHelper.php b/Classes/Resource/Helper/BynderHelper.php new file mode 100644 index 0000000..a7ed0d0 --- /dev/null +++ b/Classes/Resource/Helper/BynderHelper.php @@ -0,0 +1,324 @@ +derivative = $derivative; + } else { + throw new NotImplementedException('Invalid derivative retrieved', 1531746896); + } + return $this; + } + + /** + * Try to transform given URL to a File + * + * @param string $url + * @param Folder $targetFolder + * @return File + * @throws NotImplementedException + */ + public function transformUrlToFile($url, Folder $targetFolder): File + { + throw new NotImplementedException(sprintf('Method %s::%s() is not implemented', __CLASS__, __METHOD__), 1531749725); + } + + /** + * @param string $identifier + * @return array + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + */ + public function getBynderMediaInfo(string $identifier): array + { + return $this->getBynderService()->getMediaInfo($identifier); + } + + /** + * Get public url + * + * Return NULL if you want to use core default behaviour + * + * @param File $file + * @param bool $relativeToCurrentScript + * @return string|null + */ + public function getPublicUrl(File $file, $relativeToCurrentScript = false): string + { + $identifier = $this->getBynderFileIdentifier($file); + if (!empty($identifier)) { + switch ($file->getMimeType()) { + case 'bynder/' . BynderDriver::ASSET_TYPE_IMAGE: + try { + $mediaInfo = $this->getBynderMediaInfo($identifier); + return $mediaInfo['thumbnails'][$this->derivative]; + } catch (\Exception $e) { + return ConfigurationUtility::getUnavailableImage($relativeToCurrentScript); + } + break; + // @TODO WHEN IMPLEMENTED BY BYNDER COMPACT VIEW + // case 'bynder' . BynderDriver::ASSET_TYPE_DOCUMENT: + } + } + + return ''; + } + + /** + * Get local absolute file path to preview image + * + * Return an empty string when no preview image is available + * + * @param File $file + * @return string + * @throws FileDoesNotExistException + */ + public function getPreviewImage(File $file): string + { + try { + return $this->downloadThumbnailToTemporaryFilePath($file, $this->derivative); + } catch (\Exception $e) { + return ''; + } + } + + /** + * Get meta data for OnlineMedia item + * + * See $GLOBALS[TCA][sys_file_metadata][columns] for possible fields to fill/use + * + * @param File $file + * @return array with metadata + */ + public function getMetaData(File $file): array + { + return $this->extractMetaData($this->getBynderFileIdentifier($file), ['title', 'description', 'width', 'height', 'copyright', 'keywords']); + } + + /** + * @param string $identifier + * @param array $propertiesToExtract + * @return array + */ + public function extractMetaData($identifier, $propertiesToExtract = []) + { + try { + $mediaInfo = $mediaInfo = $this->getBynderMediaInfo($identifier); + return $this->extractFileInformation($mediaInfo, $propertiesToExtract); + } catch (\Exception $e) { + } + return []; + } + + /** + * @param FileInterface $file + * @return string + */ + protected function getBynderFileIdentifier(FileInterface $file): string + { + return $file->getProperty('identifier'); + } + + /** + * Extracts information about a file from the filesystem. + * + * @param array $mediaInfo as returned from getAssetBankManager()->getMediaInfo() + * @param array $propertiesToExtract array of properties which should be returned, if empty all will be extracted + * @return array + */ + protected function extractFileInformation(array $mediaInfo, array $propertiesToExtract = []): array + { + if (empty($propertiesToExtract)) { + $propertiesToExtract = [ + 'size', + 'atime', + 'mtime', + 'ctime', + 'mimetype', + 'name', + 'extension', + 'identifier', + 'identifier_hash', + 'storage', + 'folder_hash' + ]; + } + $fileInformation = []; + foreach ($propertiesToExtract as $property) { + $fileInformation[$property] = $this->getSpecificFileInformation($mediaInfo, $property); + } + return $fileInformation; + } + + /** + * Extracts a specific FileInformation from the FileSystems. + * + * @param array $mediaInfo + * @param string $property + * + * @return bool|int|string + * @throws \InvalidArgumentException + */ + protected function getSpecificFileInformation($mediaInfo, $property) + { + switch ($property) { + case 'size': + return $mediaInfo['fileSize']; + case 'atime': + return strtotime($mediaInfo['dateModified']); + case 'mtime': + return strtotime($mediaInfo['dateModified']); + case 'ctime': + return strtotime($mediaInfo['dateCreated']); + case 'name': + return $mediaInfo['name'] . '.bynder'; + case 'mimetype': + return 'bynder/' . $mediaInfo['type']; + case 'identifier': + return $mediaInfo['id']; + case 'extension': + return 'bynder'; + case 'identifier_hash': + return sha1($mediaInfo['id']); + case 'storage': + return $this->getBynderStorage()->getUid(); + case 'folder_hash': + return sha1('bynder' . $this->getBynderStorage()->getUid()); + + // Metadata + case 'title': + return $mediaInfo['name']; + case 'description': + return $mediaInfo['description']; + case 'width': + return $mediaInfo['width']; + case 'height': + return $mediaInfo['height']; + case 'copyright': + return $mediaInfo['copyright']; + case 'keywords': + return implode(', ', $mediaInfo['tags'] ?? []); + default: + throw new \InvalidArgumentException(sprintf('The information "%s" is not available.', $property), 1519130380); + } + } + + /** + * Save a file to a temporary path and returns that path. + * + * @param File $file + * @param string $type + * @return string|null The temporary path + * @throws FileDoesNotExistException + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + */ + protected function downloadThumbnailToTemporaryFilePath(File $file, $type = self::DERIVATIVES_THUMBNAIL): string + { + try { + $mediaInfo = $this->getBynderService()->getMediaInfo($this->getBynderFileIdentifier($file)); + $url = $mediaInfo['thumbnails'][$type]; + if (!empty($url)) { + $temporaryPath = $this->getTemporaryPathForFile($url); + if (!is_file($temporaryPath)) { + $report = []; + $data = GeneralUtility::getUrl($url, 0, false, $report); + if (!empty($data)) { + $result = GeneralUtility::writeFile($temporaryPath, $data); + if ($result === false) { + throw new \RuntimeException( + 'Copying file "' . $file->getIdentifier() . '" to temporary path "' . $temporaryPath . '" failed.', + 1519208427 + ); + } + } + } + } + } catch (\GuzzleHttp\Exception\RequestException $exception) { + throw new FileDoesNotExistException( + sprintf('Requested file " % s" coudn\'t be found', $file->getIdentifier()), + 1519115242, + $exception + ); + } + + return $temporaryPath ?? null; + } + + /** + * @param File $file + * @param string $width + * @param string $height + * @return string + * @throws \BeechIt\Bynder\Exception\InvalidExtensionConfigurationException + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + */ + public function getOnTheFlyPublicUrl(File $file, $width, $height): string + { + $mediaInfo = $this->getBynderMediaInfo($file->getIdentifier()); + if (filter_var($mediaInfo['isPublic'], FILTER_VALIDATE_BOOLEAN) === true) { + return ConfigurationUtility::getOnTheFlyBaseUrl() . $file->getIdentifier() . '?' . http_build_query([ + 'w' => (int)$width, + 'h' => (int)$height, + 'crop' => (bool)strpos($width . $height, 'c') + ]); + } + return ''; + } + + /** + * Returns a temporary path for a given file, including the file extension. + * + * @param string $url + * @return string + */ + protected function getTemporaryPathForFile($url): string + { + $temporaryPath = PATH_site . 'typo3temp/assets/' . BynderDriver::KEY . '/'; + if (!is_dir($temporaryPath)) { + GeneralUtility::mkdir_deep($temporaryPath); + } + $info = pathinfo($url); + return $temporaryPath . $info['filename'] . '.' . $info['extension']; + } +} diff --git a/Classes/Metadata/Extractor.php b/Classes/Resource/Index/Extractor.php similarity index 64% rename from Classes/Metadata/Extractor.php rename to Classes/Resource/Index/Extractor.php index ee70898..3ec7f89 100644 --- a/Classes/Metadata/Extractor.php +++ b/Classes/Resource/Index/Extractor.php @@ -1,6 +1,6 @@ getOnlineMediaHelper($file) !== false; } /** @@ -65,17 +67,10 @@ public function canProcess(Resource\File $file) */ public function extractMetaData(Resource\File $file, array $previousExtractedData = []) { - $fileInfo = $file->getStorage()->getFileInfoByIdentifier( - $file->getIdentifier(), - [ - 'title', - 'description', - 'width', - 'height', - 'copyright', - 'keywords', - ] - ); - return array_merge($previousExtractedData, $fileInfo); + /** @var Resource\OnlineMedia\Helpers\OnlineMediaHelperInterface $helper */ + $helper = Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry::getInstance()->getOnlineMediaHelper($file); + $output = $previousExtractedData; + ArrayUtility::mergeRecursiveWithOverrule($output, ($helper !== false ? $helper->getMetaData($file) : [])); + return $output; } -} \ No newline at end of file +} diff --git a/Classes/Resource/Rendering/BynderImageRenderer.php b/Classes/Resource/Rendering/BynderImageRenderer.php new file mode 100644 index 0000000..439bd6f --- /dev/null +++ b/Classes/Resource/Rendering/BynderImageRenderer.php @@ -0,0 +1,272 @@ +getExtension() === 'bynder' && ($file->getMimeType() === 'bynder/image')); + } + + /** + * Render for given File(Reference) HTML output + * + * @param FileInterface $file + * @param int|string $width TYPO3 known format; examples: 220, 200m or 200c + * @param int|string $height TYPO3 known format; examples: 220, 200m or 200c + * @param array $options + * @param bool $usedPathsRelativeToCurrentScript See $file->getPublicUrl() + * @return string + * @throws \BeechIt\Bynder\Exception\InvalidExtensionConfigurationException + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + * @throws \TYPO3\CMS\Extbase\Object\InvalidObjectException + */ + public function render(FileInterface $file, $width, $height, array $options = [], $usedPathsRelativeToCurrentScript = false): string + { + return $this->renderImageTag( + $file, + $this->getProcessedPublicImageLocation($file, $width, $height, $options), + $width, + $height, + $options + ); + } + + /** + * @param FileInterface $file + * @param $width + * @param $height + * @param $info + * @return string + * @throws \BeechIt\Bynder\Exception\InvalidExtensionConfigurationException + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + * @throws \TYPO3\CMS\Extbase\Object\InvalidObjectException + */ + protected function getProcessedPublicImageLocation(FileInterface $file, $width, $height, $info) + { + if (!($file instanceof File) && is_callable([$file, 'getOriginalFile'])) { + $originalFile = $file->getOriginalFile(); + } else { + $originalFile = $file; + } + + list($width, $height) = $this->getCalculatedSizes($originalFile->getProperty('width'), $originalFile->getProperty('height'), $width, $height); + + try { + if (ConfigurationUtility::isOnTheFlyConfigured() && $url = $this->getBynderHelper($originalFile)->getOnTheFlyPublicUrl($originalFile, $width, $height)) { + return $url; + } else { + return $this->processPublicImageLocationLocally($originalFile, $width, $height, $info); + } + } catch (BynderException $e) { + // Never throw on own exceptions, just return unavailable image + return ConfigurationUtility::getUnavailableImage(); + } + } + + /** + * @param File $file + * @param integer $width + * @param integer $height + * @param array $info + * @return string + * @throws \TYPO3\CMS\Extbase\Object\InvalidObjectException + * @throws \BeechIt\Bynder\Exception\NotImplementedException + */ + protected function processPublicImageLocationLocally(File $file, $width, $height, $info) + { + // When width and height are set and non of them have a 'm' suffix we don't keep existing ratio + $derivative = BynderHelper::DERIVATIVES_WEB_IMAGE; + if ($width && $height) { + if ($width <= 80) { + $derivative = BynderHelper::DERIVATIVES_MINI; + } elseif ($width <= 250) { + $derivative = BynderHelper::DERIVATIVES_THUMBNAIL; + } + } + $bynderHelper = $this->getBynderHelper($file); + // Set required derivative + $bynderHelper->setDerivative($derivative); + + /** + * Now do the same logic as MediaViewHelper. + */ + $cropVariant = $info['cropVariant'] ?: 'default'; + $cropString = $file instanceof FileReference ? $file->getProperty('crop') : ''; + $cropVariantCollection = CropVariantCollection::create((string)$cropString); + $cropArea = $cropVariantCollection->getCropArea($cropVariant); + $processingInstructions = [ + 'width' => $width, + 'height' => $height, + 'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($file), + ]; + $imageService = $this->getImageService(); + $processedImage = $imageService->applyProcessingInstructions($file, $processingInstructions); + $imageUri = $imageService->getImageUri($processedImage); + /** + * The logic from MediaViewHelper is ended here. + */ + + // Restore derivative to thumbnail + $bynderHelper->setDerivative(BynderHelper::DERIVATIVES_THUMBNAIL); + return $imageUri; + } + + /** + * @param integer $originalWidth + * @param integer $originalHeight + * @param integer|string $width + * @param integer|string $height + * @return array + */ + protected function getCalculatedSizes($originalWidth, $originalHeight, $width, $height) + { + $keepRatio = true; + $crop = false; + + // When width and height are set and non of them have a 'm' suffix we don't keep existing ratio + if ($width && $height && strpos($width . $height, 'm') === false) { + $keepRatio = false; + } + + // When width and height are set and one of then have a 'c' suffix we don't keep existing ratio and allow cropping + if ($width && $height && strpos($width . $height, 'c') !== false) { + $keepRatio = false; + $crop = true; + } + + $width = (int)$width; + $height = (int)$height; + + if (!$keepRatio && $width > $originalWidth) { + $height = $this->calculateRelativeDimension($width, $height, $originalWidth); + $width = $originalWidth; + } elseif (!$keepRatio && $height > $originalHeight) { + $width = $this->calculateRelativeDimension($height, $width, $originalHeight); + $height = $originalHeight; + } elseif ($keepRatio && $width > $originalWidth) { + $height = $originalWidth / $width * $height; + } elseif ($keepRatio && $height > $originalHeight) { + $height = $originalHeight / $height * $width; + } elseif ($width === 0 && $height > 0) { + $height = $this->calculateRelativeDimension($originalWidth, $originalHeight, $width); + } elseif ($width === 0 && $height > 0) { + $width = $this->calculateRelativeDimension($originalHeight, $originalWidth, $height); + } + if ($crop === true) { + return [$width . 'c', $height . 'c']; + } else { + return [$width, $height]; + } + } + + /** + * Render image for given File(Reference) HTML output + * + * @param FileInterface $file + * @param string $source + * @param int|string $width TYPO3 known format; examples: 220, 200m or 200c + * @param int|string $height TYPO3 known format; examples: 220, 200m or 200c + * @param array $options + * @param bool $usedPathsRelativeToCurrentScript See $file->getPublicUrl() + * @return string + */ + public function renderImageTag($file, $source, $width, $height, $options, $usedPathsRelativeToCurrentScript = false): string + { + $tagBuilderService = $this->getTagBuilderService(); + $tag = $tagBuilderService->getTagBuilder('img'); + $tagBuilderService->initializeAbstractTagBasedAttributes($tag, $options); + + $tag->addAttribute('src', ($usedPathsRelativeToCurrentScript ? $source : $source)); + $tag->addAttribute('width', $width); + $tag->addAttribute('height', $height); + + // The alt-attribute is mandatory to have valid html-code, therefore add it even if it is empty + if ($tag->hasAttribute('alt') === false) { + $tag->addAttribute('alt', $file->getProperty('alternative')); + } + if ($tag->hasAttribute('title') === false) { + $tag->addAttribute('title', $file->getProperty('title')); + } + + return $tag->render(); + } + + /** + * Calculate relative dimension + * For instance you have the original width, height and new width. + * And want to calculate the new height with the same ratio as the original dimensions + * + * @param int $orgA + * @param int $orgB + * @param int $newA + * @return int + */ + protected function calculateRelativeDimension(int $orgA, int $orgB, int $newA): int + { + return ($newA === 0) ? $orgB : (int)($orgB / ($orgA / $newA)); + } + + /** + * Return an instance of ImageService + * + * @return ImageService + */ + protected function getImageService() + { + $objectManager = GeneralUtility::makeInstance(ObjectManager::class); + return $objectManager->get(ImageService::class); + } + + /** + * Return an instance of TagBuilderService + * + * @return TagBuilderService + */ + protected function getTagBuilderService() + { + return GeneralUtility::makeInstance(TagBuilderService::class); + } +} diff --git a/Classes/Resource/Rendering/BynderVideoRenderer.php b/Classes/Resource/Rendering/BynderVideoRenderer.php new file mode 100644 index 0000000..5944dad --- /dev/null +++ b/Classes/Resource/Rendering/BynderVideoRenderer.php @@ -0,0 +1,119 @@ +getExtension() === 'bynder' && $file->getMimeType() === 'bynder/video'); + } + + /** + * Render for given File(Reference) HTML output + * + * @param FileInterface $source + * @param int|string $width TYPO3 known format; examples: 220, 200m or 200c + * @param int|string $height TYPO3 known format; examples: 220, 200m or 200c + * @param array $options + * @param bool $usedPathsRelativeToCurrentScript See $file->getPublicUrl() + * @return string + * @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException + * @throws \TYPO3\CMS\Extbase\Object\InvalidObjectException + */ + public function render(FileInterface $source, $width, $height, array $options = [], $usedPathsRelativeToCurrentScript = false): string + { + if ($source instanceof File) { + $file = $source; + } elseif (is_callable([$source, 'getOriginalFile'])) { + $file = $source->getOriginalFile(); + } + + return $this->getEmbedCode($file); + } + + /** + * Include Video.JS javascript libraries and configuration + * + * @return void + */ + protected function addVideoScripts() + { + $this->getPageRenderer()->addCssFile('EXT:bynder/Resources/Public/Styles/video-js.min.css'); + $this->getPageRenderer()->addJsInlineCode('video-js', 'window.VIDEOJS_NO_DYNAMIC_STYLE = true;'); + $this->getPageRenderer()->addJsFooterFile('EXT:bynder/Resources/Public/JavaScript/video-js.min.js'); + } + + /** + * Generate Video.JS embed code + * + * @param File $file + * @return string + */ + protected function getEmbedCode(File $file): string + { + $sources = []; + try { + $mediaInfo = $this->getBynderHelper($file)->getBynderMediaInfo($file->getIdentifier()); + foreach ((array)$mediaInfo['videoPreviewURLs'] as $url) { + $sources[] = ''; + } + } catch (\Exception $e) { + // Catch all exceptions as these should never crash the frontend website + } + if (!empty($sources)) { + $this->addVideoScripts(); + return ''; + } else { + return ''; + } + } + + /** + * @return PageRenderer + */ + protected function getPageRenderer(): PageRenderer + { + return GeneralUtility::makeInstance(PageRenderer::class); + } +} diff --git a/Classes/Service/BynderService.php b/Classes/Service/BynderService.php index 94abc95..d1bca72 100644 --- a/Classes/Service/BynderService.php +++ b/Classes/Service/BynderService.php @@ -7,11 +7,10 @@ * Date: 19-2-18 * All code (c) Beech.it all rights reserved */ -use BeechIt\Bynder\Exception\InvalidExtensionConfigurationException; +use BeechIt\Bynder\Utility\ConfigurationUtility; use Bynder\Api\BynderApiFactory; use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; -use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -25,36 +24,6 @@ class BynderService implements SingletonInterface */ protected $bynderIntegrationId = '8517905e-6c2f-47c3-96ca-0312027bbc95'; - /** - * @var string - */ - protected $apiBaseUrl; - - /** - * @var string - */ - protected $otfBaseUrl; - - /** - * @var string - */ - protected $oAuthConsumerKey; - - /** - * @var string - */ - protected $oAuthConsumerSecret; - - /** - * @var string - */ - protected $oAuthTokenKey; - - /** - * @var string - */ - protected $oAuthTokenSecret; - /** * @var \Bynder\Api\Impl\BynderApi */ @@ -65,85 +34,13 @@ class BynderService implements SingletonInterface */ protected $cache; - public function __construct() - { - $extensionConfiguration = \BeechIt\Bynder\Utility\ConfigurationUtility::getExtensionConfiguration(); - - $this->apiBaseUrl = $extensionConfiguration['url'] ?? ''; - $this->otfBaseUrl = $extensionConfiguration['otf_base_url'] ?? ''; - $this->oAuthConsumerKey = $extensionConfiguration['consumer_key'] ?? null; - $this->oAuthConsumerSecret = $extensionConfiguration['consumer_secret'] ?? null; - $this->oAuthTokenKey = $extensionConfiguration['token_key'] ?? null; - $this->oAuthTokenSecret = $extensionConfiguration['token_secret'] ?? null; - - if (empty($this->apiBaseUrl) || empty($this->oAuthConsumerKey) || empty($this->oAuthConsumerSecret) || empty($this->oAuthTokenKey) || empty($this->oAuthTokenSecret)) { - throw new InvalidExtensionConfigurationException('Make sure all Bynder oAuth settings are set in extension manager', 1519051718); - } - } - /** * @return \Bynder\Api\Impl\BynderApi * @throws \InvalidArgumentException */ public function getBynderApi() { - return BynderApiFactory::create( - [ - 'consumerKey' => $this->oAuthConsumerKey, - 'consumerSecret' => $this->oAuthConsumerSecret, - 'token' => $this->oAuthTokenKey, - 'tokenSecret' => $this->oAuthTokenSecret, - 'baseUrl' => $this->apiBaseUrl - ] - ); - } - - /** - * @return string - */ - public function getApiBaseUrl(): string - { - return $this->apiBaseUrl; - } - - /** - * @return string - */ - public function getOtfBaseUrl(): string - { - return $this->otfBaseUrl; - } - - /** - * @return string - */ - public function getOAuthConsumerKey(): string - { - return $this->oAuthConsumerKey; - } - - /** - * @return string - */ - public function getOAuthConsumerSecret(): string - { - return $this->oAuthConsumerSecret; - } - - /** - * @return string - */ - public function getOAuthTokenKey(): string - { - return $this->oAuthTokenKey; - } - - /** - * @return string - */ - public function getOAuthTokenSecret(): string - { - return $this->oAuthTokenSecret; + return BynderApiFactory::create(ConfigurationUtility::getBynderApiFactoryCredentials()); } /** diff --git a/Classes/Service/TagBuilderService.php b/Classes/Service/TagBuilderService.php new file mode 100644 index 0000000..13671f7 --- /dev/null +++ b/Classes/Service/TagBuilderService.php @@ -0,0 +1,63 @@ +addAttributes($arguments['additionalAttributes']); + } + + if ($arguments['data'] && is_array($arguments['data'])) { + foreach ($arguments['data'] as $dataAttributeKey => $dataAttributeValue) { + $tagBuilder->addAttribute('data-' . $dataAttributeKey, $dataAttributeValue); + } + } + $this->initializeUniversalTagAttributes($tagBuilder, $arguments); + return $tagBuilder; + } + + /** + * @param TagBuilder $tagBuilder + * @param array $arguments + * @param array $universalTagAttributes + * @return TagBuilder + */ + public function initializeUniversalTagAttributes( + TagBuilder $tagBuilder, + array $arguments, + array $universalTagAttributes = ['class', 'dir', 'id', 'lang', 'style', 'title', 'accesskey', 'tabindex', 'onclick'] + ): TagBuilder { + foreach ($universalTagAttributes as $attributeName) { + if ($arguments[$attributeName] && $arguments[$attributeName] !== '') { + $tagBuilder->addAttribute($attributeName, $arguments[$attributeName]); + } + } + return $tagBuilder; + } +} diff --git a/Classes/Slot/InstallSlot.php b/Classes/Slot/InstallSlot.php index a2b8c14..6896bd1 100644 --- a/Classes/Slot/InstallSlot.php +++ b/Classes/Slot/InstallSlot.php @@ -73,4 +73,4 @@ public function createBynderFileStorage(string $extensionKey, InstallUtility $in ->getConnectionForTable('sys_filemounts'); $dbConnection->insert('sys_filemounts', $field_values); } -} \ No newline at end of file +} diff --git a/Classes/Slot/PublicUrlSlot.php b/Classes/Slot/PublicUrlSlot.php deleted file mode 100644 index 18f1167..0000000 --- a/Classes/Slot/PublicUrlSlot.php +++ /dev/null @@ -1,76 +0,0 @@ -getProperty('bynder') === true) { - if ($resourceObject->getProperty('bynder_url')) { - $urlData['publicUrl'] = $resourceObject->getProperty('bynder_url'); - } else { - $urlData['publicUrl'] = $this->getUnavailableImage($relativeToCurrentScript); - } - } elseif ($driver instanceof BynderDriver) { - try { - $urlData['publicUrl'] = $driver->getPublicUrl($resourceObject->getIdentifier()); - } catch (FileDoesNotExistException $e) { - $urlData['publicUrl'] = $this->getUnavailableImage($relativeToCurrentScript); - } - } - } - - /** - * @param bool $relativeToCurrentScript - * @return string - */ - protected function getUnavailableImage($relativeToCurrentScript = false): string - { - $configuration = \BeechIt\Bynder\Utility\ConfigurationUtility::getExtensionConfiguration(); - $path = GeneralUtility::getFileAbsFileName( - $configuration['image_unavailable'] ?? - 'EXT:bynder/Resources/Public/Icons/ImageUnavailable.svg'); - - return ($relativeToCurrentScript) ? PathUtility::getAbsoluteWebPath($path) : str_replace(PATH_site, '', $path); - } - -} diff --git a/Classes/Traits/BynderHelper.php b/Classes/Traits/BynderHelper.php new file mode 100644 index 0000000..dcc4885 --- /dev/null +++ b/Classes/Traits/BynderHelper.php @@ -0,0 +1,37 @@ +getOnlineMediaHelper($file); + } else { + $helper = GeneralUtility::makeInstance(Helper\BynderHelper::class, 'bynder'); + } + + if ($helper instanceof Helper\BynderHelper) { + return $helper; + } + throw new InvalidObjectException('Bynder Helper cannot be initialized', 1530782854569); + } +} diff --git a/Classes/Traits/BynderService.php b/Classes/Traits/BynderService.php new file mode 100644 index 0000000..937073b --- /dev/null +++ b/Classes/Traits/BynderService.php @@ -0,0 +1,22 @@ +bynderStorage === null) { + /** @var \TYPO3\CMS\Core\Resource\ResourceStorage $fileStorage */ + $backendUserAuthentication = $GLOBALS['BE_USER']; + foreach ($backendUserAuthentication->getFileStorages() as $fileStorage) { + if ($fileStorage->getDriverType() === 'bynder') { + return $this->bynderStorage = $fileStorage; + } + } + throw new \InvalidArgumentException('Missing Bynder file storage'); + } + return $this->bynderStorage; + } +} diff --git a/Classes/Utility/ConfigurationUtility.php b/Classes/Utility/ConfigurationUtility.php index 4d6a52f..1d66f33 100644 --- a/Classes/Utility/ConfigurationUtility.php +++ b/Classes/Utility/ConfigurationUtility.php @@ -2,8 +2,11 @@ namespace BeechIt\Bynder\Utility; +use BeechIt\Bynder\Exception\InvalidExtensionConfigurationException; +use BeechIt\Bynder\Resource\BynderDriver; use TYPO3\CMS\Core\Configuration\ExtensionConfiguration; use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Core\Utility\PathUtility; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extensionmanager\Utility\ConfigurationUtility as CoreConfigurationUtility; @@ -16,29 +19,132 @@ class ConfigurationUtility const EXTENSION = 'bynder'; /** + * @param string $allowedElements * @return array + * @throws InvalidExtensionConfigurationException */ - public static function getExtensionConfiguration(): array + public static function getAssetTypesByAllowedElements($allowedElements): array { - $objectManager = GeneralUtility::makeInstance(ObjectManager::class); - if (class_exists(CoreConfigurationUtility::class)) { - $configuration = $objectManager->get(CoreConfigurationUtility::class)->getCurrentConfiguration('bynder'); - $extensionConfiguration = []; - foreach ($configuration as $key => $value) { - $extensionConfiguration[$key] = $value['value']; - } + $assetTypes = []; + if (empty($allowedElements)) { + $assetTypes = [BynderDriver::ASSET_TYPE_IMAGE, BynderDriver::ASSET_TYPE_VIDEO]; } else { - $extensionConfiguration = $objectManager->get(ExtensionConfiguration::class)->get('bynder'); - } + $allowedElements = GeneralUtility::trimExplode(',', strtolower($allowedElements), true); + $allowed = [ + BynderDriver::ASSET_TYPE_IMAGE => ((self::getExtensionConfiguration())['asset_type_image'] ?? 'jpg,png,gif'), + BynderDriver::ASSET_TYPE_VIDEO => ((self::getExtensionConfiguration())['asset_type_video'] ?? 'mp4,mov') + ]; - if (isset($extensionConfiguration['url'])) { - $extensionConfiguration['url'] = static::cleanUrl($extensionConfiguration['url']); + foreach (array_filter($allowed) as $key => $elements) { + foreach (GeneralUtility::trimExplode(',', $elements, true) as $element) { + if (in_array($element, $allowedElements)) { + $assetTypes[] = $key; + break; + } + } + } } - $extensionConfiguration['otf_base_url'] = $extensionConfiguration['otf_base_url'] ?? $extensionConfiguration['otfBaseUrl'] ?? null; - if (isset($extensionConfiguration['otf_base_url'])) { - $extensionConfiguration['otf_base_url'] = static::cleanUrl($extensionConfiguration['otf_base_url']); + + return $assetTypes; + } + + /** + * @param boolean $relativeToCurrentScript + * @return string + * @throws InvalidExtensionConfigurationException + */ + public static function getUnavailableImage($relativeToCurrentScript = false): string + { + $path = GeneralUtility::getFileAbsFileName( + (self::getExtensionConfiguration())['image_unavailable'] ?? + 'EXT:bynder/Resources/Public/Icons/ImageUnavailable.svg' + ); + + return ($relativeToCurrentScript) ? PathUtility::getAbsoluteWebPath($path) : str_replace(PATH_site, '', $path); + } + + /** + * @return array + * @throws InvalidExtensionConfigurationException + */ + public static function getBynderApiFactoryCredentials(): array + { + $credentials = [ + 'baseUrl' => static::getApiBaseUrl(), + 'consumerKey' => ((self::getExtensionConfiguration())['consumer_key'] ?? ''), + 'consumerSecret' => ((self::getExtensionConfiguration())['consumer_secret'] ?? ''), + 'token' => ((self::getExtensionConfiguration())['token_key'] ?? ''), + 'tokenSecret' => ((self::getExtensionConfiguration())['token_secret'] ?? ''), + ]; + return $credentials; + } + + /** + * @return string + * @throws InvalidExtensionConfigurationException + */ + public static function getApiBaseUrl(): string + { + return static::cleanUrl((self::getExtensionConfiguration())['url']); + } + + /** + * @return string + * @throws InvalidExtensionConfigurationException + */ + public static function getOnTheFlyBaseUrl(): string + { + return static::cleanUrl((self::getExtensionConfiguration())['otf_base_url']); + } + + /** + * @return boolean + * @throws InvalidExtensionConfigurationException + */ + public static function isOnTheFlyConfigured(): bool + { + return !empty((self::getExtensionConfiguration())['otf_base_url']); + } + + /** + * @return array + * @throws InvalidExtensionConfigurationException + */ + public static function getExtensionConfiguration(): array + { + static $configuration; + if ($configuration === null) { + $configuration = [ + 'url' => '', + 'otf_base_url' => '', + 'consumer_key' => '', + 'consumer_secret' => '', + 'token_key' => '', + 'token_secret' => '', + 'image_unavailable' => 'EXT:bynder/Resources/Public/Icons/ImageUnavailable.svg', + 'asset_type_image' => 'jpg,png,gif', + 'asset_type_video' => 'mp4' + ]; + + $objectManager = GeneralUtility::makeInstance(ObjectManager::class); + if (class_exists(CoreConfigurationUtility::class)) { + $currentConfiguration = $objectManager->get(CoreConfigurationUtility::class)->getCurrentConfiguration('bynder'); + $configuration = []; + foreach ($currentConfiguration as $key => $value) { + $configuration[$key] = $value['value']; + } + } else { + \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($configuration, (array)$objectManager->get(ExtensionConfiguration::class)->get('bynder')); + } + + if (empty($configuration['url']) || + empty($configuration['consumer_key']) || empty($configuration['consumer_secret']) || + empty($configuration['token_key']) || empty($configuration['token_secret']) + ) { + throw new InvalidExtensionConfigurationException('Make sure all Bynder oAuth settings are set in extension manager', 1519051718); + } } - return $extensionConfiguration; + return $configuration; } /** diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php index 1c5e0ac..964178a 100644 --- a/Configuration/Backend/AjaxRoutes.php +++ b/Configuration/Backend/AjaxRoutes.php @@ -9,4 +9,4 @@ 'path' => '/bynder/compactview/getfiles', 'target' => \BeechIt\Bynder\Controller\CompactViewController::class . '::getFilesAction' ], -]; \ No newline at end of file +]; diff --git a/Configuration/Backend/Routes.php b/Configuration/Backend/Routes.php index d47f03d..9067eb8 100644 --- a/Configuration/Backend/Routes.php +++ b/Configuration/Backend/Routes.php @@ -9,4 +9,4 @@ 'path' => '/bynder/compactview', 'target' => \BeechIt\Bynder\Controller\CompactViewController::class . '::indexAction' ], -]; \ No newline at end of file +]; diff --git a/README.md b/README.md index 2d3a013..9b2c138 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Next got the the extension configuration of EXT:bynder and fill in the needed ur | token_secret | Bynder OAuth token secret | Yes | | | image_unavailable | Displayed image when file is not retrievable like when the file status is deleted or unpublished | *No* | EXT:bynder/Resources/Public/Icons/ImageUnavailable.svg | -(1) See: https://help.bynder.com/Modules/Asset-Bank/Modify-public-derivatives-on-the-fly.htm?Highlight=on-the-fly#prereq +(1) See: https://help.bynder.com/Modules/Asset-Library/Modify-public-derivatives-on-the-fly.htm ### How to contribute diff --git a/Resources/Private/Language/locallang_be.xlf b/Resources/Private/Language/locallang_be.xlf index 049ca76..99ae9fa 100644 --- a/Resources/Private/Language/locallang_be.xlf +++ b/Resources/Private/Language/locallang_be.xlf @@ -12,6 +12,10 @@ Please make sure you have access to the Bynder file mount. This is needed to use the Bynder file picker + + supports HTML5 video]]> + - \ No newline at end of file + diff --git a/Resources/Private/Templates/CompactView/Index.html b/Resources/Private/Templates/CompactView/Index.html index 7930704..a4cadca 100644 --- a/Resources/Private/Templates/CompactView/Index.html +++ b/Resources/Private/Templates/CompactView/Index.html @@ -1,19 +1,17 @@ - -
- + data-folder="bynder-compactview" + data-fullScreen="true" + data-header="false" + data-language="{language}" + data-mode="multi" + data-zindex="300" +> + +
\ No newline at end of file + diff --git a/Resources/Public/JavaScript/video-js.min.js b/Resources/Public/JavaScript/video-js.min.js new file mode 100644 index 0000000..06f5f44 --- /dev/null +++ b/Resources/Public/JavaScript/video-js.min.js @@ -0,0 +1,12 @@ +/** + * @license + * Video.js 7.0.5 + * Copyright Brightcove, Inc. + * Available under Apache License Version 2.0 + * + * + * Includes vtt.js + * Available under Apache License Version 2.0 + * + */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.videojs=t()}(this,function(){var d="7.0.5",e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{};function t(e,t){return e(t={exports:{}},t.exports),t.exports}var i,g="undefined"!=typeof window?window:"undefined"!=typeof e?e:"undefined"!=typeof self?self:{},r={},n=Object.freeze({default:r}),a=n&&r||n,s="undefined"!=typeof e?e:"undefined"!=typeof window?window:{};"undefined"!=typeof document?i=document:(i=s["__GLOBAL_DOCUMENT_CACHE@4"])||(i=s["__GLOBAL_DOCUMENT_CACHE@4"]=a);var p=i,o=void 0,u="info",l=[],c=function(e,t){var i=o.levels[u],r=new RegExp("^("+i+")$");if("log"!==e&&t.unshift(e.toUpperCase()+":"),l&&l.push([].concat(t)),t.unshift("VIDEOJS:"),g.console){var n=g.console[e];n||"debug"!==e||(n=g.console.info||g.console.log),n&&i&&r.test(e)&&n[Array.isArray(t)?"apply":"call"](g.console,t)}};(o=function(){for(var e=arguments.length,t=Array(e),i=0;i',i=n.firstChild,n.setAttribute("style","display:none; position:absolute;"),p.body.appendChild(n));for(var a={},s=0;sx',e=t.firstChild.href}return e},ei=function(e){if("string"==typeof e){var t=/^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i.exec(e);if(t)return t.pop().toLowerCase()}return""},ti=function(e){var t=g.location,i=Jt(e);return(":"===i.protocol?t.protocol:i.protocol)+i.host!==t.protocol+t.host},ii=Object.freeze({parseUrl:Jt,getAbsoluteURL:Zt,getFileExtension:ei,isCrossOrigin:ti}),ri=function(e){var t=ni.call(e);return"[object Function]"===t||"function"==typeof e&&"[object RegExp]"!==t||"undefined"!=typeof window&&(e===window.setTimeout||e===window.alert||e===window.confirm||e===window.prompt)},ni=Object.prototype.toString;var ai=Object.freeze({default:ri,__moduleExports:ri}),si=t(function(e,t){(t=e.exports=function(e){return e.replace(/^\s*|\s*$/g,"")}).left=function(e){return e.replace(/^\s*/,"")},t.right=function(e){return e.replace(/\s*$/,"")}}),oi=si.left,ui=si.right,li=Object.freeze({default:si,__moduleExports:si,left:oi,right:ui}),ci=ai&&ri||ai,hi=function(e,t,i){if(!ci(t))throw new TypeError("iterator must be a function");arguments.length<3&&(i=this);"[object Array]"===di.call(e)?function(e,t,i){for(var r=0,n=e.length;r=e?t.push(n):n.startTime===n.endTime&&n.startTime<=e&&n.startTime+.5>=e&&t.push(n)}if(o=!1,t.length!==this.activeCues_.length)o=!0;else for(var a=0;a","‎":"‎","‏":"‏"," ":" "},Gi={c:"span",i:"i",b:"b",u:"u",ruby:"ruby",rt:"rt",v:"span",lang:"span"},Xi={v:"title",lang:"lang"},Yi={rt:"ruby"};function $i(a,i){function e(){if(!i)return null;var e,t=i.match(/^([^<]*)(<[^>]*>?)?/);return e=t[1]?t[1]:t[2],i=i.substr(e.length),e}function t(e){return zi[e]}function r(e){for(;f=e.match(/&(amp|lt|gt|lrm|rlm|nbsp);/);)e=e.replace(f[0],t);return e}function n(e,t){var i=Gi[e];if(!i)return null;var r=a.document.createElement(i);r.localName=i;var n=Xi[e];return n&&t&&(r[n]=t.trim()),r}for(var s,o,u,l=a.document.createElement("div"),c=l,h=[];null!==(s=e());)if("<"!==s[0])c.appendChild(a.document.createTextNode(r(s)));else{if("/"===s[1]){h.length&&h[h.length-1]===s.substr(2).replace(">","")&&(h.pop(),c=c.parentNode);continue}var d,p=Hi(s.substr(1,s.length-2));if(p){d=a.document.createProcessingInstruction("timestamp",p),c.appendChild(d);continue}var f=s.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);if(!f)continue;if(!(d=n(f[1],f[3])))continue;if(o=c,Yi[(u=d).localName]&&Yi[u.localName]!==o.localName)continue;f[2]&&(d.className=f[2].substr(1).replace("."," ")),h.push(f[1]),c.appendChild(d),c=d}return l}var Ki=[[1470,1470],[1472,1472],[1475,1475],[1478,1478],[1488,1514],[1520,1524],[1544,1544],[1547,1547],[1549,1549],[1563,1563],[1566,1610],[1645,1647],[1649,1749],[1765,1766],[1774,1775],[1786,1805],[1807,1808],[1810,1839],[1869,1957],[1969,1969],[1984,2026],[2036,2037],[2042,2042],[2048,2069],[2074,2074],[2084,2084],[2088,2088],[2096,2110],[2112,2136],[2142,2142],[2208,2208],[2210,2220],[8207,8207],[64285,64285],[64287,64296],[64298,64310],[64312,64316],[64318,64318],[64320,64321],[64323,64324],[64326,64449],[64467,64829],[64848,64911],[64914,64967],[65008,65020],[65136,65140],[65142,65276],[67584,67589],[67592,67592],[67594,67637],[67639,67640],[67644,67644],[67647,67669],[67671,67679],[67840,67867],[67872,67897],[67903,67903],[67968,68023],[68030,68031],[68096,68096],[68112,68115],[68117,68119],[68121,68147],[68160,68167],[68176,68184],[68192,68223],[68352,68405],[68416,68437],[68440,68466],[68472,68479],[68608,68680],[126464,126467],[126469,126495],[126497,126498],[126500,126500],[126503,126503],[126505,126514],[126516,126519],[126521,126521],[126523,126523],[126530,126530],[126535,126535],[126537,126537],[126539,126539],[126541,126543],[126545,126546],[126548,126548],[126551,126551],[126553,126553],[126555,126555],[126557,126557],[126559,126559],[126561,126562],[126564,126564],[126567,126570],[126572,126578],[126580,126583],[126585,126588],[126590,126590],[126592,126601],[126603,126619],[126625,126627],[126629,126633],[126635,126651],[1114109,1114109]];function Qi(e){for(var t=0;t=i[0]&&e<=i[1])return!0}return!1}function Ji(){}function Zi(e,t,i){Ji.call(this),this.cue=t,this.cueDiv=$i(e,t.text);var r={color:"rgba(255, 255, 255, 1)",backgroundColor:"rgba(0, 0, 0, 0.8)",position:"relative",left:0,right:0,top:0,bottom:0,display:"inline",writingMode:""===t.vertical?"horizontal-tb":"lr"===t.vertical?"vertical-lr":"vertical-rl",unicodeBidi:"plaintext"};this.applyStyles(r,this.cueDiv),this.div=e.document.createElement("div"),r={direction:function(e){var t=[],i="";if(!e||!e.childNodes)return"ltr";function n(e,t){for(var i=t.childNodes.length-1;0<=i;i--)e.push(t.childNodes[i])}function a(e){if(!e||!e.length)return null;var t=e.pop(),i=t.textContent||t.innerText;if(i){var r=i.match(/^.*(\n|\r)/);return r?r[e.length=0]:i}return"ruby"===t.tagName?a(e):t.childNodes?(n(e,t),a(e)):void 0}for(n(t,e);i=a(t);)for(var r=0;rh&&(c=c<0?-1:1,c*=Math.ceil(h/l)*l),n<0&&(c+=""===r.vertical?o.height:o.width,a=a.reverse()),i.move(d,c)}else{var p=i.lineHeight/o.height*100;switch(r.lineAlign){case"middle":n-=p/2;break;case"end":n-=p}switch(r.vertical){case"":t.applyStyles({top:t.formatStyle(n,"%")});break;case"rl":t.applyStyles({left:t.formatStyle(n,"%")});break;case"lr":t.applyStyles({right:t.formatStyle(n,"%")})}a=["+y","-x","+x","-y"],i=new er(t)}var f=function(e,t){for(var i,r=new er(e),n=1,a=0;ae.left&&this.tope.top},er.prototype.overlapsAny=function(e){for(var t=0;t=e.top&&this.bottom<=e.bottom&&this.left>=e.left&&this.right<=e.right},er.prototype.overlapsOppositeAxis=function(e,t){switch(t){case"+x":return this.lefte.right;case"+y":return this.tope.bottom}},er.prototype.intersectPercentage=function(e){return Math.max(0,Math.min(this.right,e.right)-Math.max(this.left,e.left))*Math.max(0,Math.min(this.bottom,e.bottom)-Math.max(this.top,e.top))/(this.height*this.width)},er.prototype.toCSSCompatValues=function(e){return{top:this.top-e.top,bottom:e.bottom-this.bottom,left:this.left-e.left,right:e.right-this.right,height:this.height,width:this.width}},er.getSimpleBoxPosition=function(e){var t=e.div?e.div.offsetHeight:e.tagName?e.offsetHeight:0,i=e.div?e.div.offsetWidth:e.tagName?e.offsetWidth:0,r=e.div?e.div.offsetTop:e.tagName?e.offsetTop:0;return{left:(e=e.div?e.div.getBoundingClientRect():e.tagName?e.getBoundingClientRect():e).left,right:e.right,top:e.top||r,height:e.height||t,bottom:e.bottom||r+(e.height||t),width:e.width||i}},ir.StringDecoder=function(){return{decode:function(e){if(!e)return"";if("string"!=typeof e)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(e))}}},ir.convertCueToDOMTree=function(e,t){return e&&t?$i(e,t):null};ir.processCues=function(r,n,e){if(!r||!n||!e)return null;for(;e.firstChild;)e.removeChild(e.firstChild);var a=r.document.createElement("div");if(a.style.position="absolute",a.style.left="0",a.style.right="0",a.style.top="0",a.style.bottom="0",a.style.margin="1.5%",e.appendChild(a),function(e){for(var t=0;t