diff --git a/Classes/Aspects/PublicUrlAspect.php b/Classes/Aspects/PublicUrlAspect.php index c1f43a5..1313e0e 100644 --- a/Classes/Aspects/PublicUrlAspect.php +++ b/Classes/Aspects/PublicUrlAspect.php @@ -25,7 +25,7 @@ ***************************************************************/ use TYPO3\CMS\Core\Resource; -use TYPO3\CMS\Backend\Utility\BackendUtility; +use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -90,17 +90,17 @@ public function generatePublicUrl( $queryParameterArray['p'] = $resourceObject->getUid(); $queryParameterArray['t'] = 'p'; } - $queryParameterArray['token'] = GeneralUtility::hmac( + $queryParameterArray['fal_token'] = GeneralUtility::hmac( implode('|', $queryParameterArray), 'BeResourceStorageDumpFile' ); // $urlData['publicUrl'] is passed by reference, so we can change that here and the value will be taken into account - $urlData['publicUrl'] = BackendUtility::getAjaxUrl( - 'FalSecuredownload::publicUrl', + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + $urlData['publicUrl'] = (string) $uriBuilder->buildUriFromRoute( + 'ajax_dump_file', $queryParameterArray, - false, - true + UriBuilder::ABSOLUTE_URL ); } } diff --git a/Classes/Configuration/ExtensionConfiguration.php b/Classes/Configuration/ExtensionConfiguration.php index 364eb63..83f6b80 100644 --- a/Classes/Configuration/ExtensionConfiguration.php +++ b/Classes/Configuration/ExtensionConfiguration.php @@ -25,6 +25,9 @@ namespace BeechIt\FalSecuredownload\Configuration; +use TYPO3\CMS\Core\Configuration\ExtensionConfiguration as ExtensionConfigurationCore; +use TYPO3\CMS\Core\Utility\GeneralUtility; + /** * Wrapper for the extension configuration */ @@ -42,14 +45,18 @@ private static function init() { if (!self::$isInitialized) { self::$isInitialized = true; - - $extensionConfig = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['fal_securedownload']); + if (class_exists(ExtensionConfigurationCore::class)) { + $extensionConfig = GeneralUtility::makeInstance(ExtensionConfigurationCore::class)->get('fal_securedownload'); + } else { + // Fallback for 8LTS + $extensionConfig = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['fal_securedownload']); + } self::$loginRedirectUrl = $extensionConfig['login_redirect_url']; self::$noAccessRedirectUrl = $extensionConfig['no_access_redirect_url']; self::$forceDownload = (bool)$extensionConfig['force_download']; self::$forceDownloadForExt = $extensionConfig['force_download_for_ext']; self::$trackDownloads = (bool)$extensionConfig['track_downloads']; - self::$resumableDownload = (bool)(isset($extensionConfig['resumable_download']) ? $extensionConfig['resumable_download'] : false); + self::$resumableDownload = (isset($extensionConfig['resumable_download']) ? (bool)$extensionConfig['resumable_download'] : false); } } diff --git a/Classes/ContextMenu/ItemProvider.php b/Classes/ContextMenu/ItemProvider.php new file mode 100644 index 0000000..8cb5684 --- /dev/null +++ b/Classes/ContextMenu/ItemProvider.php @@ -0,0 +1,106 @@ +table === 'sys_file' || $this->table === 'sys_file_storage'; + } + + /** + * Initialize file object + */ + protected function initialize() + { + parent::initialize(); + $resource = ResourceFactory::getInstance() + ->retrieveFileOrFolderObject($this->identifier); + + if ($resource instanceof Folder + && !$resource->getStorage()->isPublic() + && in_array( + $resource->getRole(), + [Folder::ROLE_DEFAULT, Folder::ROLE_USERUPLOAD], + true + ) + ) { + $this->folder = $resource; + } + } + + /** + * Adds the folder permission menu item for folder of a non-public storage + * + * @param array $items + * @return array + */ + public function addItems(array $items): array + { + $this->initialize(); + if ($this->folder instanceof Folder) { + + $items += $this->prepareItems([ + 'permissions_divider' => [ + 'type' => 'divider', + ], + 'permissions' => [ + 'label' => 'LLL:EXT:fal_securedownload/Resources/Private/Language/locallang_be.xlf:clickmenu.folderpermissions', + 'iconIdentifier' => 'action-folder', + 'callbackAction' => 'folderPermissions' + ] + ]); + } + + return $items; + } + + /** + * @param string $itemName + * @return array + */ + protected function getAdditionalAttributes(string $itemName): array + { + /** @var Utility $utility */ + $utility = GeneralUtility::makeInstance(Utility::class); + $folderRecord = $utility->getFolderRecord($this->folder); + + return [ + 'data-callback-module' => 'TYPO3/CMS/FalSecuredownload/ContextMenuActions', + 'data-folder-record-uid' => $folderRecord['uid'] ?? 0, + 'data-storage' => $this->folder->getStorage()->getUid(), + 'data-folder' => $this->folder->getIdentifier(), + 'data-folder-hash' => $this->folder->getHashedIdentifier(), + ]; + } +} \ No newline at end of file diff --git a/Classes/Controller/BePublicUrlController.php b/Classes/Controller/BePublicUrlController.php index d416143..6982cc7 100644 --- a/Classes/Controller/BePublicUrlController.php +++ b/Classes/Controller/BePublicUrlController.php @@ -7,7 +7,8 @@ * All code (c) Beech Applications B.V. all rights reserved */ -use TYPO3\CMS\Core\Http\AjaxRequestHandler; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use TYPO3\CMS\Core\Resource\ProcessedFileRepository; use TYPO3\CMS\Core\Resource\ResourceFactory; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -18,15 +19,14 @@ */ class BePublicUrlController { - /** * Dump file content * Copy from /sysext/core/Resources/PHP/FileDumpEID.php - * - * @param array $params - * @param AjaxRequestHandler $ajaxObj + * @param ServerRequestInterface $request + * @param ResponseInterface $response + * @return ResponseInterface */ - public function dumpFile($params = [], AjaxRequestHandler $ajaxObj = null) + public function dumpFile(ServerRequestInterface $request, ResponseInterface $response) { $parameters = ['eID' => 'dumpFile']; if (GeneralUtility::_GP('t')) { @@ -40,7 +40,7 @@ public function dumpFile($params = [], AjaxRequestHandler $ajaxObj = null) } if (GeneralUtility::hmac(implode('|', $parameters), - 'BeResourceStorageDumpFile') === GeneralUtility::_GP('token') + 'BeResourceStorageDumpFile') === GeneralUtility::_GP('fal_token') ) { if (isset($parameters['f'])) { $file = ResourceFactory::getInstance()->getFileObject($parameters['f']); @@ -52,7 +52,7 @@ public function dumpFile($params = [], AjaxRequestHandler $ajaxObj = null) /** @var \TYPO3\CMS\Core\Resource\ProcessedFile $file */ $file = GeneralUtility::makeInstance(ProcessedFileRepository::class)->findByUid($parameters['p']); if ($file->isDeleted()) { - $file = null; + HttpUtility::setResponseCodeAndExit(HttpUtility::HTTP_STATUS_404); } $orgFile = $file->getOriginalFile(); } @@ -68,6 +68,7 @@ public function dumpFile($params = [], AjaxRequestHandler $ajaxObj = null) ob_start(); $file->getStorage()->dumpFileContents($file); + exit; } else { HttpUtility::setResponseCodeAndExit(HttpUtility::HTTP_STATUS_403); diff --git a/Classes/Domain/Repository/ProcessedFileRepository.php b/Classes/Domain/Repository/ProcessedFileRepository.php index 7193616..e725fc3 100644 --- a/Classes/Domain/Repository/ProcessedFileRepository.php +++ b/Classes/Domain/Repository/ProcessedFileRepository.php @@ -50,17 +50,13 @@ public function findByUid($uid) throw new \InvalidArgumentException('uid has to be integer.', 1316779798); } - if (version_compare(TYPO3_branch, '8.7', '>=')) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table); - $row = $queryBuilder - ->select('*') - ->from($this->table) - ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT))) - ->execute() - ->fetch(); - } else { - $row = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('*', $this->table, 'uid=' . (int)$uid); - } + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table); + $row = $queryBuilder + ->select('*') + ->from($this->table) + ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT))) + ->execute() + ->fetch(); if (empty($row) || !is_array($row)) { throw new \RuntimeException( diff --git a/Classes/FormEngine/DownloadStatistics.php b/Classes/FormEngine/DownloadStatistics.php index 314a1d7..7d7d3f1 100644 --- a/Classes/FormEngine/DownloadStatistics.php +++ b/Classes/FormEngine/DownloadStatistics.php @@ -27,7 +27,6 @@ use TYPO3\CMS\Backend\Form\AbstractNode; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\DatabaseConnection; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\MathUtility; use TYPO3\CMS\Lang\LanguageService; @@ -51,33 +50,22 @@ public function render() return $this->resultArray; } - if (version_compare(TYPO3_branch, '8.7', '>=')) { - $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file'); - $statistics = $queryBuilder - ->selectLiteral( - $queryBuilder->getConnection()->getDatabasePlatform()->getCountExpression( - $queryBuilder->quoteIdentifier('tx_falsecuredownload_download.file') - ) . ' AS ' . $queryBuilder->quoteIdentifier('cnt')) - ->addSelect('sys_file.name') - ->from('sys_file') - ->join('sys_file', 'tx_falsecuredownload_download', 'tx_falsecuredownload_download', - $queryBuilder->expr()->eq('tx_falsecuredownload_download.file', $queryBuilder->quoteIdentifier('sys_file.uid')) - ) - ->where($queryBuilder->expr()->eq('tx_falsecuredownload_download.feuser', $queryBuilder->createNamedParameter((int)$row['uid'], \PDO::PARAM_INT))) - ->groupBy('sys_file.name') - ->orderBy('sys_file.name') - ->execute() - ->fetchAll(); - } else { - $db = $this->getDatabase(); - $statistics = $db->exec_SELECTgetRows( - 'sys_file.name, count(tx_falsecuredownload_download.file) as cnt', - 'sys_file JOIN tx_falsecuredownload_download ON tx_falsecuredownload_download.file = sys_file.uid - AND tx_falsecuredownload_download.feuser = ' . (int)$row['uid'], - '', - 'sys_file.name' - ); - } + $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file'); + $statistics = $queryBuilder + ->selectLiteral( + $queryBuilder->getConnection()->getDatabasePlatform()->getCountExpression( + $queryBuilder->quoteIdentifier('tx_falsecuredownload_download.file') + ) . ' AS ' . $queryBuilder->quoteIdentifier('cnt')) + ->addSelect('sys_file.name') + ->from('sys_file') + ->join('sys_file', 'tx_falsecuredownload_download', 'tx_falsecuredownload_download', + $queryBuilder->expr()->eq('tx_falsecuredownload_download.file', $queryBuilder->quoteIdentifier('sys_file.uid')) + ) + ->where($queryBuilder->expr()->eq('tx_falsecuredownload_download.feuser', $queryBuilder->createNamedParameter((int)$row['uid'], \PDO::PARAM_INT))) + ->groupBy('sys_file.name') + ->orderBy('sys_file.name') + ->execute() + ->fetchAll(); $lang = $this->getLanguageService(); $markup = []; @@ -110,12 +98,4 @@ protected function getLanguageService() { return $GLOBALS['LANG']; } - - /** - * @return DatabaseConnection - */ - protected function getDatabase() - { - return $GLOBALS['TYPO3_DB']; - } } diff --git a/Classes/Hooks/AbstractBeButtons.php b/Classes/Hooks/AbstractBeButtons.php index 7c6b732..15ac193 100644 --- a/Classes/Hooks/AbstractBeButtons.php +++ b/Classes/Hooks/AbstractBeButtons.php @@ -25,18 +25,18 @@ ***************************************************************/ use BeechIt\FalSecuredownload\Service\Utility; +use TYPO3\CMS\Backend\Routing\UriBuilder; use TYPO3\CMS\Core\Imaging\Icon; use TYPO3\CMS\Core\Imaging\IconFactory; use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException; use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException; use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Core\Resource\ResourceFactory; -use TYPO3\CMS\Backend\Utility\BackendUtility; use TYPO3\CMS\Core\Utility\GeneralUtility; /** - * Abstract utility class for classes that want to add album add/edit buttons - * somewhere like a ClickMenuOptions class. + * Abstract utility class for classes that want to add BE buttons + * to edit folder permissions */ abstract class AbstractBeButtons { @@ -153,7 +153,9 @@ protected function buildAddUrl(Folder $folder) protected function buildUrl(array $parameters) { $parameters['returnUrl'] = GeneralUtility::getIndpEnv('REQUEST_URI'); - return BackendUtility::getModuleUrl('record_edit', $parameters); + $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); + + return $uriBuilder->buildUriFromRoute('record_edit', $parameters); } /** diff --git a/Classes/Hooks/ClickMenuOptions.php b/Classes/Hooks/ClickMenuOptions.php deleted file mode 100644 index 2a813fa..0000000 --- a/Classes/Hooks/ClickMenuOptions.php +++ /dev/null @@ -1,92 +0,0 @@ - - * All rights reserved - * - * This script is part of the TYPO3 project. The TYPO3 project is - * free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * The GNU General Public License can be found at - * http://www.gnu.org/copyleft/gpl.html. - * - * This script 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 General Public License for more details. - * - * This copyright notice MUST APPEAR in all copies of the script! - ***************************************************************/ - -use TYPO3\CMS\Backend\ClickMenu\ClickMenu; -use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; - -/** - * Add ClickMenuOptions in file list - */ -class ClickMenuOptions extends AbstractBeButtons -{ - /** - * @var ClickMenu - */ - protected $parentObject; - - /** - * Add create tx_ icon to filemenu - * - * @param ClickMenu $parentObject Back-reference to the calling object - * @param array $menuItems Current list of menu items - * @param string $combinedIdentifier The combined identifier - * @param integer $uid Id of the clicked on item - * @return array Modified list of menu items - */ - public function main(ClickMenu $parentObject, $menuItems, $combinedIdentifier, $uid) - { - - if (!$parentObject->isDBmenu) { - $this->parentObject = $parentObject; - $combinedIdentifier = rawurldecode($combinedIdentifier); - - $extraMenuItems = $this->generateButtons($combinedIdentifier); - if (count($extraMenuItems)) { - $menuItems[] = 'spacer'; - $menuItems = array_merge($menuItems, $extraMenuItems); - } - } - - return $menuItems; - } - - /** - * Create click menu item - * - * @param string $title - * @param string $shortTitle - * @param string $icon - * @param string $url - * @param bool $addReturnUrl - * @return string|array - */ - protected function createLink($title, $shortTitle, $icon, $url, $addReturnUrl = true) - { - - if (strpos($url, 'alert') !== 0) { - $url = $this->parentObject->urlRefForCM($url, $addReturnUrl ? 'returnUrl' : ''); - } - - /** @var BackendUserAuthentication $beUser */ - $beUser = $GLOBALS['BE_USER']; - - return $this->parentObject->linkItem( - '' . $shortTitle . '', - !empty($beUser->uc['noMenuMode']) && $beUser->uc['noMenuMode'] !== 'icons' ? '' : ' ' . $icon, - $url - ); - } -} diff --git a/Classes/Hooks/CmsLayout.php b/Classes/Hooks/CmsLayout.php index cbc94f1..53f0d9e 100644 --- a/Classes/Hooks/CmsLayout.php +++ b/Classes/Hooks/CmsLayout.php @@ -137,15 +137,13 @@ protected function getFieldFromFlexform($key, $sheet = 'sDEF') * * @param string $key * @param string $languageFile - * @param bool $hsc If set, the return value is htmlspecialchar'ed * @return string */ protected function sL( $key, - $languageFile = 'LLL:EXT:fal_securedownload/Resources/Private/Language/locallang_be.xlf', - $hsc = true + $languageFile = 'LLL:EXT:fal_securedownload/Resources/Private/Language/locallang_be.xlf' ) { - return $this->getLangService()->sL($languageFile . ':' . $key, $hsc); + return $this->getLangService()->sL($languageFile . ':' . $key); } /** diff --git a/Classes/Hooks/DocHeaderButtonsHook.php b/Classes/Hooks/DocHeaderButtonsHook.php index ce0bd2d..6d5a6ee 100644 --- a/Classes/Hooks/DocHeaderButtonsHook.php +++ b/Classes/Hooks/DocHeaderButtonsHook.php @@ -1,4 +1,5 @@ = 9LTS + ) { foreach ($this->generateButtons(GeneralUtility::_GP('id')) as $buttonInfo) { $button = $buttonBar->makeLinkButton(); $button->setIcon($buttonInfo['icon']); diff --git a/Classes/Hooks/FileDumpHook.php b/Classes/Hooks/FileDumpHook.php index d850dcd..cd02f04 100644 --- a/Classes/Hooks/FileDumpHook.php +++ b/Classes/Hooks/FileDumpHook.php @@ -28,9 +28,9 @@ use BeechIt\FalSecuredownload\Configuration\ExtensionConfiguration; use BeechIt\FalSecuredownload\Security\CheckPermissions; use TYPO3\CMS\Core\Database\ConnectionPool; -use TYPO3\CMS\Core\Database\DatabaseConnection; use TYPO3\CMS\Core\Resource\File; use TYPO3\CMS\Core\Resource\FileInterface; +use TYPO3\CMS\Core\Resource\FileReference; use TYPO3\CMS\Core\Resource\Hook\FileDumpEIDHookInterface; use TYPO3\CMS\Core\Resource\ResourceInterface; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -138,13 +138,13 @@ public function checkFileAccess(ResourceInterface $file) if ($this->loginRedirectUrl !== null) { $this->redirectToUrl($this->loginRedirectUrl); } else { - $this->exitScript('Authentication required!', 401); + $this->exitScript('Authentication required!'); } } else { if ($this->noAccessRedirectUrl !== null) { $this->redirectToUrl($this->noAccessRedirectUrl); } else { - $this->exitScript('No access!', 403); + $this->exitScript('No access!'); } } } @@ -161,25 +161,22 @@ public function checkFileAccess(ResourceInterface $file) 'file' => (int)$this->originalFile->getUid() ]; - if (version_compare(TYPO3_branch, '8.7', '>=')) { - GeneralUtility::makeInstance(ConnectionPool::class) - ->getConnectionForTable('tx_falsecuredownload_download') - ->insert( - 'tx_falsecuredownload_download', - $columns, - [\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT] - ); - - } else { - $db = $this->getDatabase(); - $db->exec_INSERTquery('tx_falsecuredownload_download', $columns); - } + GeneralUtility::makeInstance(ConnectionPool::class) + ->getConnectionForTable('tx_falsecuredownload_download') + ->insert( + 'tx_falsecuredownload_download', + $columns, + [\PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT, \PDO::PARAM_INT] + ); } - if ($this->forceDownload($this->originalFile->getExtension())) { - $this->dumpFileContents($this->originalFile, true, $this->resumableDownload); + // Dump the precise requested file for File and ProcessedFile, but dump the referenced file for FileReference + $dumpFile = $file instanceof FileReference ? $file->getOriginalFile() : $file; + + if ($this->forceDownload($dumpFile->getExtension())) { + $this->dumpFileContents($dumpFile, true, $this->resumableDownload); } elseif ($this->resumableDownload) { - $this->dumpFileContents($this->originalFile, false, true); + $this->dumpFileContents($dumpFile, false, true); } } @@ -195,13 +192,21 @@ public function checkFileAccess(ResourceInterface $file) */ protected function dumpFileContents($file, $asDownload, $resumableDownload) { + $downloadName = $file->getProperty('download_name') ?: $file->getName(); + + // Make sure downloadName has a file extension + $fileParts = pathinfo($downloadName); + if (empty($fileParts['extension'])) { + $downloadName .= '.' . $file->getExtension(); + } + if (!$resumableDownload) { - $file->getStorage()->dumpFileContents($file, $asDownload); + $file->getStorage()->dumpFileContents($file, $asDownload, $downloadName); exit; } $contentDisposition = $asDownload ? 'attachment' : 'inline'; - header('Content-Disposition: ' . $contentDisposition . '; filename="' . $file->getName() . '"'); + header('Content-Disposition: ' . $contentDisposition . '; filename="' . $downloadName . '"'); header('Content-Type: ' . $file->getMimeType()); header('Expires: -1'); header('Cache-Control: public, must-revalidate, post-check=0, pre-check=0'); @@ -213,6 +218,13 @@ protected function dumpFileContents($file, $asDownload, $resumableDownload) header('Content-Range: bytes */' . $fileSize); exit; } + + // Find part of file and push this out + $filePointer = @fopen($file->getForLocalProcessing(false), 'rb'); + if ($filePointer === false) { + header('HTTP/1.1 404 File not found'); + exit; + } $dumpSize = $fileSize; list($begin, $end) = $range; @@ -230,8 +242,6 @@ protected function dumpFileContents($file, $asDownload, $resumableDownload) ob_end_clean(); } - // Find part of file and push this out - $filePointer = @fopen($file->getForLocalProcessing(false), 'rb'); fseek($filePointer, $begin); $dumpedSize = 0; while (!feof($filePointer) && $dumpedSize < $dumpSize) { @@ -340,14 +350,6 @@ protected function redirectToUrl($url) exit; } - /** - * @return DatabaseConnection - */ - protected function getDatabase() - { - return $GLOBALS['TYPO3_DB']; - } - /** * Determines the HTTP range given in the request * diff --git a/Classes/Service/Utility.php b/Classes/Service/Utility.php index 1430df6..9fd491a 100644 --- a/Classes/Service/Utility.php +++ b/Classes/Service/Utility.php @@ -54,7 +54,7 @@ public function __construct() * Get folder configuration record * * @param Folder $folder - * @return array + * @return array|false */ public function getFolderRecord(Folder $folder) { @@ -62,27 +62,15 @@ public function getFolderRecord(Folder $folder) if (!isset(self::$folderRecordCache[$folder->getCombinedIdentifier()]) || !array_key_exists($folder->getCombinedIdentifier(), self::$folderRecordCache) ) { - if (version_compare(TYPO3_branch, '8.7', '>=')) { - - $queryBuilder = $this->getQueryBuilder(); - $record = $queryBuilder - ->select('*') - ->from('tx_falsecuredownload_folder') - ->where($queryBuilder->expr()->eq('storage', $queryBuilder->createNamedParameter((int)$folder->getStorage()->getUid(), \PDO::PARAM_INT))) - ->andWhere($queryBuilder->expr()->eq('folder_hash', $queryBuilder->createNamedParameter($folder->getHashedIdentifier(), \PDO::PARAM_STR))) - ->execute() - ->fetch(); - - } else { - - $record = $this->getDatabase()->exec_SELECTgetSingleRow( - '*', - 'tx_falsecuredownload_folder', - 'storage = ' . (int)$folder->getStorage()->getUid() . ' - AND folder_hash = ' . $this->getDatabase()->fullQuoteStr($folder->getHashedIdentifier(), 'tx_falsecuredownload_folder') - ); + $queryBuilder = $this->getQueryBuilder(); + $record = $queryBuilder + ->select('*') + ->from('tx_falsecuredownload_folder') + ->where($queryBuilder->expr()->eq('storage', $queryBuilder->createNamedParameter((int)$folder->getStorage()->getUid(), \PDO::PARAM_INT))) + ->andWhere($queryBuilder->expr()->eq('folder_hash', $queryBuilder->createNamedParameter($folder->getHashedIdentifier(), \PDO::PARAM_STR))) + ->execute() + ->fetch(); - } // cache results self::$folderRecordCache[$folder->getCombinedIdentifier()] = $record; } @@ -170,16 +158,6 @@ public function deleteFolderRecord($storageUid, $folderHash, $identifier) } } - /** - * Gets the database object. - * - * @return \TYPO3\CMS\Core\Database\DatabaseConnection - */ - protected function getDatabase() - { - return $GLOBALS['TYPO3_DB']; - } - /** * Gets a query build * diff --git a/Classes/ViewHelpers/LeaveStateViewHelper.php b/Classes/ViewHelpers/LeaveStateViewHelper.php index bbac1f7..75dd495 100644 --- a/Classes/ViewHelpers/LeaveStateViewHelper.php +++ b/Classes/ViewHelpers/LeaveStateViewHelper.php @@ -44,23 +44,30 @@ public function initializeArguments() } /** - * renders child if the current visitor ... - * otherwise renders child. - - * @return string + * @param array $arguments + * @return bool */ - public function render() + protected static function evaluateCondition($arguments = null) { /** @var Folder $folder */ - $folder = $this->arguments['folder']; + $folder = $arguments['folder']; $leafStateService = GeneralUtility::makeInstance(LeafStateService::class); $feUser = !empty($GLOBALS['TSFE']) ? $GLOBALS['TSFE']->fe_user : false; - if ($feUser && $leafStateService->getLeafStateForUser($feUser, $folder->getCombinedIdentifier())) { + return $feUser && $leafStateService->getLeafStateForUser($feUser, $folder->getCombinedIdentifier()); + } + + /** + * Renders child if $condition is true, otherwise renders child. + * + * @return string the rendered string + */ + public function render() + { + if (static::evaluateCondition($this->arguments)) { return $this->renderThenChild(); - } else { - return $this->renderElseChild(); } + return $this->renderElseChild(); } } diff --git a/Classes/ViewHelpers/Security/AssetAccessViewHelper.php b/Classes/ViewHelpers/Security/AssetAccessViewHelper.php index 10072a6..0b4ced3 100644 --- a/Classes/ViewHelpers/Security/AssetAccessViewHelper.php +++ b/Classes/ViewHelpers/Security/AssetAccessViewHelper.php @@ -24,6 +24,7 @@ * This copyright notice MUST APPEAR in all copies of the script! ***************************************************************/ +use TYPO3\CMS\Core\Context\Context; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Resource\Folder; use TYPO3\CMS\Core\Resource\File; @@ -92,13 +93,23 @@ protected static function evaluateCondition($arguments = null) /** * Determines whether the currently logged in FE user belongs to the specified usergroup * - * @return boolean|array FALSE when not logged in or else $GLOBALS['TSFE']->fe_user->groupData['uid'] + * @return boolean|array FALSE when not logged in or else frontend.user.groupIds */ protected static function getFeUserGroups() { - if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE']->loginUser) { - return false; + if (class_exists(Context::class)) { + $context = GeneralUtility::makeInstance(Context::class); + if (!$context->getPropertyFromAspect('frontend.user', 'isLoggedIn')) { + return false; + } + return $context->getPropertyFromAspect('frontend.user', 'groupIds'); + } else { + // Fallback for 8LTS + if (!isset($GLOBALS['TSFE']) || !$GLOBALS['TSFE']->loginUser) { + return false; + } + return $GLOBALS['TSFE']->fe_user->groupData['uid']; } - return $GLOBALS['TSFE']->fe_user->groupData['uid']; + } } \ No newline at end of file diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php new file mode 100644 index 0000000..8e6ec4f --- /dev/null +++ b/Configuration/Backend/AjaxRoutes.php @@ -0,0 +1,7 @@ + [ + 'path' => '/fal_securedownloads/dump_file', + 'target' => BeechIt\FalSecuredownload\Controller\BePublicUrlController::class . '::dumpFile' + ] +]; diff --git a/Configuration/TCA/Overrides/sys_file_metadata.php b/Configuration/TCA/Overrides/sys_file_metadata.php index 8003803..428bf37 100644 --- a/Configuration/TCA/Overrides/sys_file_metadata.php +++ b/Configuration/TCA/Overrides/sys_file_metadata.php @@ -4,7 +4,7 @@ $additionalColumns = [ 'fe_groups' => [ 'exclude' => true, - 'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.fe_group', + 'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_general.xlf:LGL.fe_group', 'config' => [ 'type' => 'select', 'renderType' => 'selectMultipleSideBySide', @@ -12,20 +12,33 @@ 'maxitems' => 20, 'items' => [ [ - 'LLL:EXT:lang/locallang_general.xlf:LGL.any_login', + 'LLL:EXT:lang/Resources/Private/Language/locallang_general.xlf:LGL.any_login', -2 ], [ - 'LLL:EXT:lang/locallang_general.xlf:LGL.usergroups', + 'LLL:EXT:lang/Resources/Private/Language/locallang_general.xlf:LGL.usergroups', '--div--' ] ], 'exclusiveKeys' => '-1,-2', 'foreign_table' => 'fe_groups', - 'foreign_table_where' => 'ORDER BY fe_groups.title' + 'foreign_table_where' => 'ORDER BY fe_groups.title', + 'enableMultiSelectFilterTextfield' => true, ] + ], + 'download_name' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:fal_securedownload/Resources/Private/Language/locallang_db.xlf:downloadName', + 'description' => 'LLL:EXT:fal_securedownload/Resources/Private/Language/locallang_db.xlf:downloadName.description', + 'config' => [ + 'type' => 'input', + 'size' => '255', + 'max' => '255', + ] ] ]; \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('sys_file_metadata', $additionalColumns); \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes('sys_file_metadata', 'fe_groups'); + +\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addToAllTCAtypes('sys_file_metadata', 'download_name','', 'after:alternative'); \ No newline at end of file diff --git a/Configuration/TCA/tx_falsecuredownload_folder.php b/Configuration/TCA/tx_falsecuredownload_folder.php index 38bcd37..5de07ff 100644 --- a/Configuration/TCA/tx_falsecuredownload_folder.php +++ b/Configuration/TCA/tx_falsecuredownload_folder.php @@ -59,7 +59,7 @@ ], 'fe_groups' => [ 'exclude' => false, - 'label' => 'LLL:EXT:lang/locallang_general.xlf:LGL.fe_group', + 'label' => 'LLL:EXT:lang/Resources/Private/Language/locallang_general.xlf:LGL.fe_group', 'config' => [ 'type' => 'select', 'renderType' => 'selectMultipleSideBySide', @@ -67,17 +67,18 @@ 'maxitems' => 40, 'items' => [ [ - 'LLL:EXT:lang/locallang_general.xlf:LGL.any_login', + 'LLL:EXT:lang/Resources/Private/Language/locallang_general.xlf:LGL.any_login', -2 ], [ - 'LLL:EXT:lang/locallang_general.xlf:LGL.usergroups', + 'LLL:EXT:lang/Resources/Private/Language/locallang_general.xlf:LGL.usergroups', '--div--' ] ], 'exclusiveKeys' => '-1,-2', 'foreign_table' => 'fe_groups', - 'foreign_table_where' => 'ORDER BY fe_groups.title' + 'foreign_table_where' => 'ORDER BY fe_groups.title', + 'enableMultiSelectFilterTextfield' => true, ] ] ] diff --git a/Documentation/Images/secure-file-storage.png b/Documentation/Images/secure-file-storage.png index cff8ad6..8074d75 100644 Binary files a/Documentation/Images/secure-file-storage.png and b/Documentation/Images/secure-file-storage.png differ diff --git a/Documentation/Images/set-folder-permissions.png b/Documentation/Images/set-folder-permissions.png index bb6a2f0..eef158f 100644 Binary files a/Documentation/Images/set-folder-permissions.png and b/Documentation/Images/set-folder-permissions.png differ diff --git a/Documentation/Images/set-folder-permissions2.png b/Documentation/Images/set-folder-permissions2.png new file mode 100644 index 0000000..e56279e Binary files /dev/null and b/Documentation/Images/set-folder-permissions2.png differ diff --git a/Documentation/Misc/Index.rst b/Documentation/Misc/Index.rst index f67c0d2..36a252e 100644 --- a/Documentation/Misc/Index.rst +++ b/Documentation/Misc/Index.rst @@ -85,9 +85,24 @@ To have correct urls to indexed files you need to add/adjust following ext:solr .. code-block:: ts + # Make sure the correct public URL is indexed plugin.tx_solr.index.queue._FILES.default.filePublicUrl = public_url plugin.tx_solr.index.queue._FILES.default.url = public_url + # Make sure the fe_groups are considered + plugin.tx_solr.index.queue._FILES.default.access = TEXT + plugin.tx_solr.index.queue._FILES.default.access { + value = r:0 + override { + cObject = TEXT + cObject { + required = 1 + field = fe_groups + wrap = r:| + } + } + } + *This feature is sponsored by: STIMME DER HOFFNUNG Adventist Media Center* diff --git a/README.rst b/README.md similarity index 82% rename from README.rst rename to README.md index d5e2e55..a4c1656 100644 --- a/README.rst +++ b/README.md @@ -1,5 +1,5 @@ FAL SecureDownLoad -================== +=== This extension (fal_securedownload) aims to be a general solution to secure your assets. @@ -7,15 +7,14 @@ When you storage is marked as non-public all links to files from that storage ar The access to assets can be set on folder/file bases by setting access to fe_groups in the file module. -**How to use:** +### How to use 1. Download and install fal_securedownload - 2. Un-check the 'public' checkbox in your file storage - 3. Add a .htaccess file with "Deny from all" (Apache < 2.3) or "Require all denied" (Apache >= 2.3) in your file storage root folder or move your storage outside of your webroot +4. Go to the file list and add access restrictions on file/folder -**Features** +### Features - Restrict FE access on folder level - Restrict FE access on file level @@ -24,10 +23,10 @@ The access to assets can be set on folder/file bases by setting access to fe_gro - Force download for specific file extensions (for protected file storages) - Keep track of requested downloads (count downloads per user and file) -**Requirements:** - TYPO3 7 LTS, TYPO3 8 +### Requirements +- TYPO3 8 LTS or TYPO3 9 LTS -**Suggestions:** - EXT:ke_search v1.8.4 - EXT:solrfal v4.1.0 +### Suggestions +- EXT:ke_search v1.8.4 +- EXT:solrfal v4.1.0 diff --git a/Resources/Private/Language/locallang_db.xlf b/Resources/Private/Language/locallang_db.xlf index 5f91bd2..a7b9781 100644 --- a/Resources/Private/Language/locallang_db.xlf +++ b/Resources/Private/Language/locallang_db.xlf @@ -13,7 +13,12 @@ Folder - + + Alternative Download name + + + Send File with an alternative Filename + \ No newline at end of file diff --git a/Resources/Public/JavaScript/ContextMenuActions.js b/Resources/Public/JavaScript/ContextMenuActions.js new file mode 100644 index 0000000..b3c4cea --- /dev/null +++ b/Resources/Public/JavaScript/ContextMenuActions.js @@ -0,0 +1,47 @@ +/** + * Module: TYPO3/CMS/FalSecuredownload/ContextMenuActions + * + * JavaScript to handle the click action of the "FalSecuredownload" context menu item + * @exports TYPO3/CMS/FalSecuredownload/ContextMenuActions + */ +define(function () { + 'use strict'; + + /** + * @exports TYPO3/CMS/FalSecuredownload/ContextMenuActions + */ + var ContextMenuActions = {}; + + /** + * Open folder permissions edit form + * + * @param {string} table + * @param {string} uid combined folder identifier + */ + ContextMenuActions.folderPermissions = function (table, uid) { + var folderRecordUid = this.data('folderRecordUid') || 0; + + if (folderRecordUid > 0) { + top.TYPO3.Backend.ContentContainer.setUrl( + top.TYPO3.settings.FormEngine.moduleUrl + + '&edit[tx_falsecuredownload_folder][' + parseInt(folderRecordUid, 10) + ']=edit' + + '&returnUrl=' + ContextMenuActions.getReturnUrl() + ); + } else { + top.TYPO3.Backend.ContentContainer.setUrl( + top.TYPO3.settings.FormEngine.moduleUrl + + '&edit[tx_falsecuredownload_folder][0]=new' + + '&defVals[tx_falsecuredownload_folder][storage]=' + this.data('storage') + + '&defVals[tx_falsecuredownload_folder][folder]=' + this.data('folder') + + '&defVals[tx_falsecuredownload_folder][folder_hash]=' + this.data('folderHash') + + '&returnUrl=' + ContextMenuActions.getReturnUrl() + ); + } + }; + + ContextMenuActions.getReturnUrl = function () { + return top.rawurlencode(top.list_frame.document.location.pathname + top.list_frame.document.location.search); + }; + + return ContextMenuActions; +}); \ No newline at end of file diff --git a/composer.json b/composer.json index 72b4df6..d06fb0a 100644 --- a/composer.json +++ b/composer.json @@ -18,10 +18,10 @@ "source": "https://github.com/beechit/fal_securedownload" }, "require": { - "typo3/cms-core": "^7.6 || ^8.0" + "typo3/cms-core": "^8.7 || ^9.5" }, "require-dev": { - "typo3/cms": "^7.6 || ^8.0" + "typo3/cms": "^8.7 || ^9.5" }, "replace": { "fal_securedownload": "self.version", diff --git a/ext_emconf.php b/ext_emconf.php index 7d93930..cfdaa34 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -17,10 +17,10 @@ 'createDirs' => '', 'modify_tables' => '', 'clearCacheOnLoad' => true, - 'version' => '2.1.0', + 'version' => '2.2.1', 'constraints' => [ 'depends' => [ - 'typo3' => '7.6.0 - 8.7.99', + 'typo3' => '8.7.0 - 9.5.99', ], 'conflicts' => [], 'suggests' => [ diff --git a/ext_localconf.php b/ext_localconf.php index 52c59b1..fe1334d 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -34,10 +34,6 @@ 'generatePublicUrl' ); } - \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::registerAjaxHandler( - 'FalSecuredownload::publicUrl', - \BeechIt\FalSecuredownload\Controller\BePublicUrlController::class . '->dumpFile' - ); // Page module hook $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['list_type_Info']['falsecuredownload_filetree']['fal_securedownload'] = @@ -47,6 +43,10 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['Backend\Template\Components\ButtonBar']['getButtonsHook']['FalSecuredownload'] = \BeechIt\FalSecuredownload\Hooks\DocHeaderButtonsHook::class . '->getButtons'; + // Context menu + $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'][1547242135] + = \BeechIt\FalSecuredownload\ContextMenu\ItemProvider::class; + // refresh file tree after change in tx_falsecuredownload_folder record $GLOBALS ['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tcemain.php']['processDatamapClass'][] = \BeechIt\FalSecuredownload\Hooks\ProcessDatamapHook::class; diff --git a/ext_tables.php b/ext_tables.php index 1cf2a44..1f20124 100644 --- a/ext_tables.php +++ b/ext_tables.php @@ -1,13 +1,6 @@ \BeechIt\FalSecuredownload\Hooks\ClickMenuOptions::class - ]; -} - // Initiate $iconRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Imaging\IconRegistry::class); $iconRegistry->registerIcon(