first commit
This commit is contained in:
commit
1218dd1517
12716 files changed
+989233
No files matched your search
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace TYPO3\CMS\Backend\Backend\Avatar;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
|
||||
use TYPO3\CMS\Core\Service\DependencyOrderingService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Main class to render an avatar image of a certain Backend user, resolving any avatar provider
|
||||
* that takes care of fetching the image.
|
||||
*
|
||||
* See render() and getImgTag() as main entry points
|
||||
*/
|
||||
class Avatar
|
||||
{
|
||||
/**
|
||||
* Array of sorted and initialized avatar providers
|
||||
*
|
||||
* @var AvatarProviderInterface[]
|
||||
*/
|
||||
protected $avatarProviders = [];
|
||||
|
||||
/**
|
||||
* Renders an avatar based on a Fluid template which contains some base wrapper classes and does
|
||||
* a simple caching functionality, used in Avatar ViewHelper for instance
|
||||
*
|
||||
* @param array $backendUser be_users record
|
||||
* @param int $size width and height of the image
|
||||
* @param bool $showIcon show the record icon
|
||||
* @return string
|
||||
*/
|
||||
public function render(array $backendUser = null, int $size = 32, bool $showIcon = false)
|
||||
{
|
||||
if (!is_array($backendUser)) {
|
||||
$backendUser = $this->getBackendUser()->user;
|
||||
}
|
||||
|
||||
$cacheId = 'avatar_' . md5(
|
||||
$backendUser['uid'] . '/' .
|
||||
(string)$size . '/' .
|
||||
(string)$showIcon
|
||||
);
|
||||
|
||||
$avatar = $this->getCache()->get($cacheId);
|
||||
|
||||
if (!$avatar) {
|
||||
$this->validateSortAndInitiateAvatarProviders();
|
||||
$view = $this->getFluidTemplateObject();
|
||||
|
||||
$view->assignMultiple([
|
||||
'image' => $this->getImgTag($backendUser, $size),
|
||||
'showIcon' => $showIcon,
|
||||
'backendUser' => $backendUser
|
||||
]);
|
||||
$avatar = $view->render();
|
||||
$this->getCache()->set($cacheId, $avatar);
|
||||
}
|
||||
|
||||
return $avatar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an HTML <img> tag for the avatar
|
||||
*
|
||||
* @param array $backendUser be_users record
|
||||
* @param int $size
|
||||
* @return string
|
||||
*/
|
||||
public function getImgTag(array $backendUser = null, $size = 32)
|
||||
{
|
||||
if (!is_array($backendUser)) {
|
||||
$backendUser = $this->getBackendUser()->user;
|
||||
}
|
||||
|
||||
$avatarImage = false;
|
||||
if ($backendUser !== null) {
|
||||
$avatarImage = $this->getImage($backendUser, $size);
|
||||
}
|
||||
|
||||
if (!$avatarImage) {
|
||||
$avatarImage = GeneralUtility::makeInstance(
|
||||
Image::class,
|
||||
PathUtility::stripPathSitePrefix(GeneralUtility::getFileAbsFileName('EXT:core/Resources/Public/Icons/T3Icons/avatar/avatar-default.svg')),
|
||||
$size,
|
||||
$size
|
||||
);
|
||||
}
|
||||
$imageTag = '<img src="' . htmlspecialchars($avatarImage->getUrl(true)) . '" ' .
|
||||
'width="' . (int)$avatarImage->getWidth() . '" ' .
|
||||
'height="' . (int)$avatarImage->getHeight() . '" />';
|
||||
|
||||
return $imageTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Image from first provider that returns one
|
||||
*
|
||||
* @param array $backendUser be_users record
|
||||
* @param int $size
|
||||
* @return Image|null
|
||||
*/
|
||||
public function getImage(array $backendUser, $size)
|
||||
{
|
||||
foreach ($this->avatarProviders as $provider) {
|
||||
$avatarImage = $provider->getImage($backendUser, $size);
|
||||
if (!empty($avatarImage)) {
|
||||
return $avatarImage;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the registered avatar providers
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function validateSortAndInitiateAvatarProviders()
|
||||
{
|
||||
$providers = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['avatarProviders'] ?? [];
|
||||
if (empty($providers)) {
|
||||
return;
|
||||
}
|
||||
foreach ($providers as $identifier => $configuration) {
|
||||
if (empty($configuration) || !is_array($configuration)) {
|
||||
throw new \RuntimeException(
|
||||
'Missing configuration for avatar provider "' . $identifier . '".',
|
||||
1439317801
|
||||
);
|
||||
}
|
||||
if (!is_string($configuration['provider']) || empty($configuration['provider']) || !class_exists($configuration['provider']) || !is_subclass_of(
|
||||
$configuration['provider'],
|
||||
AvatarProviderInterface::class
|
||||
)) {
|
||||
throw new \RuntimeException(
|
||||
'The avatar provider "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . AvatarProviderInterface::class . '".',
|
||||
1439317802
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$orderedProviders = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($providers);
|
||||
|
||||
// Initializes providers
|
||||
foreach ($orderedProviders as $configuration) {
|
||||
$this->avatarProviders[] = GeneralUtility::makeInstance($configuration['provider']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $filename Which templateFile should be used.
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $filename = null): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
|
||||
if ($filename === null) {
|
||||
$filename = 'Main.html';
|
||||
}
|
||||
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/Avatar/' . $filename));
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FrontendInterface
|
||||
*/
|
||||
protected function getCache()
|
||||
{
|
||||
return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\Avatar;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Contract for avatar providers that ensure how an avater should be rendered for a given Backend User
|
||||
*/
|
||||
interface AvatarProviderInterface
|
||||
{
|
||||
/**
|
||||
* Returns an Image object, prepared for output, based on a given be_users record
|
||||
*
|
||||
* @param array $backendUser be_users record
|
||||
* @param int $size
|
||||
* @return Image|null
|
||||
*/
|
||||
public function getImage(array $backendUser, $size);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\Avatar;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Avatar Provider used for rendering avatars based on local files (based on FAL), stored in the be_users.avatar
|
||||
* relation field with sys_file_reference.
|
||||
*/
|
||||
class DefaultAvatarProvider implements AvatarProviderInterface
|
||||
{
|
||||
/**
|
||||
* Return an Image object for rendering the avatar, based on a FAL-based file
|
||||
*
|
||||
* @param array $backendUser be_users record
|
||||
* @param int $size
|
||||
* @return Image|null
|
||||
*/
|
||||
public function getImage(array $backendUser, $size)
|
||||
{
|
||||
$fileUid = $this->getAvatarFileUid($backendUser['uid']);
|
||||
if ($fileUid === 0) {
|
||||
// Early return if there is no valid image file UID
|
||||
return null;
|
||||
}
|
||||
// Get file object
|
||||
try {
|
||||
$file = ResourceFactory::getInstance()->getFileObject($fileUid);
|
||||
$processedImage = $file->process(
|
||||
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
|
||||
['width' => $size . 'c', 'height' => $size . 'c']
|
||||
);
|
||||
|
||||
$image = GeneralUtility::makeInstance(
|
||||
Image::class,
|
||||
$processedImage->getPublicUrl(),
|
||||
$processedImage->getProperty('width'),
|
||||
$processedImage->getProperty('height')
|
||||
);
|
||||
} catch (FileDoesNotExistException $e) {
|
||||
// No image found
|
||||
$image = null;
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sys_file UID of the avatar of the given backend user ID
|
||||
*
|
||||
* @param int $backendUserId the UID of the be_users record
|
||||
* @return int the sys_file UID or 0 if none found
|
||||
*/
|
||||
protected function getAvatarFileUid($backendUserId)
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_file_reference');
|
||||
$fileUid = $queryBuilder
|
||||
->select('uid_local')
|
||||
->from('sys_file_reference')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'tablenames',
|
||||
$queryBuilder->createNamedParameter('be_users', \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'fieldname',
|
||||
$queryBuilder->createNamedParameter('avatar', \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'table_local',
|
||||
$queryBuilder->createNamedParameter('sys_file', \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid_foreign',
|
||||
$queryBuilder->createNamedParameter((int)$backendUserId, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
return (int)$fileUid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\Avatar;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* Acts as a pseudo model for holding all information of an avatar image
|
||||
* Holds url + dimensions of avatar image
|
||||
*/
|
||||
class Image
|
||||
{
|
||||
/**
|
||||
* Url of avatar image. Needs to be relative to the website root or an absolute URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $width;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $height;
|
||||
|
||||
/**
|
||||
* @param string $url url of image. Needs to be relative to the website root or an absolute URL.
|
||||
* @param int $width width of image
|
||||
* @param int $height height of image
|
||||
*/
|
||||
public function __construct($url, $width, $height)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->width = (int)$width;
|
||||
$this->height = (int)$height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the URL to the the avatar image
|
||||
*
|
||||
* @param bool $relativeToCurrentScript Determines whether the URL returned should be relative to the current script, in case it is relative at all.
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($relativeToCurrentScript = false)
|
||||
{
|
||||
$url = $this->url;
|
||||
if ($relativeToCurrentScript && !GeneralUtility::isValidUrl($url)) {
|
||||
$url = PathUtility::getAbsoluteWebPath(Environment::getPublicPath() . '/' . $url);
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getWidth()
|
||||
{
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getHeight()
|
||||
{
|
||||
return $this->height;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Backend\Shortcut;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Module\ModuleLoader;
|
||||
use TYPO3\CMS\Backend\Routing\Exception\ResourceNotFoundException;
|
||||
use TYPO3\CMS\Backend\Routing\Router;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Repository for backend shortcuts
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ShortcutRepository
|
||||
{
|
||||
/**
|
||||
* @var int Number of super global (All) group
|
||||
*/
|
||||
protected const SUPERGLOBAL_GROUP = -100;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $shortcuts;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $shortcutGroups;
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* @var ModuleLoader
|
||||
*/
|
||||
protected $moduleLoader;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$this->moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
|
||||
$this->moduleLoader->load($GLOBALS['TBE_MODULES']);
|
||||
|
||||
$this->shortcutGroups = $this->initShortcutGroups();
|
||||
$this->shortcuts = $this->initShortcuts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a shortcut by its uid
|
||||
*
|
||||
* @param int $shortcutId Shortcut id to get the complete shortcut for
|
||||
* @return mixed An array containing the shortcut's data on success or FALSE on failure
|
||||
*/
|
||||
public function getShortcutById(int $shortcutId)
|
||||
{
|
||||
foreach ($this->shortcuts as $shortcut) {
|
||||
if ($shortcut['raw']['uid'] === $shortcutId) {
|
||||
return $shortcut;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets shortcuts for a specific group
|
||||
*
|
||||
* @param int $groupId Group Id
|
||||
* @return array Array of shortcuts that matched the group
|
||||
*/
|
||||
public function getShortcutsByGroup(int $groupId): array
|
||||
{
|
||||
$shortcuts = [];
|
||||
|
||||
foreach ($this->shortcuts as $shortcut) {
|
||||
if ($shortcut['group'] === $groupId) {
|
||||
$shortcuts[] = $shortcut;
|
||||
}
|
||||
}
|
||||
|
||||
return $shortcuts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get shortcut groups the current user has access to
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getShortcutGroups(): array
|
||||
{
|
||||
$shortcutGroups = $this->shortcutGroups;
|
||||
|
||||
if (!$this->getBackendUser()->isAdmin()) {
|
||||
foreach ($shortcutGroups as $groupId => $groupName) {
|
||||
if ((int)$groupId < 0) {
|
||||
unset($shortcutGroups[$groupId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $shortcutGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* runs through the available shortcuts an collects their groups
|
||||
*
|
||||
* @return array Array of groups which have shortcuts
|
||||
*/
|
||||
public function getGroupsFromShortcuts(): array
|
||||
{
|
||||
$groups = [];
|
||||
|
||||
foreach ($this->shortcuts as $shortcut) {
|
||||
$groups[$shortcut['group']] = $this->shortcutGroups[$shortcut['group']];
|
||||
}
|
||||
|
||||
return array_unique($groups);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if there already is a shortcut entry for a given TYPO3 URL
|
||||
*
|
||||
* @param string $url
|
||||
* @return bool
|
||||
*/
|
||||
public function shortcutExists(string $url): bool
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_be_shortcuts');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
|
||||
$uid = $queryBuilder->select('uid')
|
||||
->from('sys_be_shortcuts')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($this->getBackendUser()->user['uid'], \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'url',
|
||||
$queryBuilder->createNamedParameter($url, \PDO::PARAM_STR)
|
||||
)
|
||||
)
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
|
||||
return (bool)$uid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shortcut
|
||||
*
|
||||
* @param string $url URL of the new shortcut
|
||||
* @param string $module module identifier of the new shortcut
|
||||
* @param string $parentModule parent module identifier of the new shortcut
|
||||
* @param string $title title of the new shortcut
|
||||
* @return bool
|
||||
* @throws \RuntimeException if the given URL is invalid
|
||||
*/
|
||||
public function addShortcut(string $url, string $module, string $parentModule = '', string $title = ''): bool
|
||||
{
|
||||
if (empty($url) || empty($module)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$queryParts = parse_url($url);
|
||||
$queryParameters = [];
|
||||
parse_str($queryParts['query'] ?? '', $queryParameters);
|
||||
|
||||
if (!empty($queryParameters['scheme'])) {
|
||||
throw new \RuntimeException('Shortcut URLs must be relative', 1518785877);
|
||||
}
|
||||
|
||||
$languageService = $this->getLanguageService();
|
||||
$title = $title ?: 'Shortcut';
|
||||
$titlePrefix = '';
|
||||
$type = 'other';
|
||||
$table = '';
|
||||
$recordId = 0;
|
||||
$pageId = 0;
|
||||
|
||||
if (is_array($queryParameters['edit'])) {
|
||||
$table = key($queryParameters['edit']);
|
||||
$recordId = (int)key($queryParameters['edit'][$table]);
|
||||
$pageId = (int)BackendUtility::getRecord($table, $recordId)['pid'];
|
||||
$languageFile = 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf';
|
||||
$action = $queryParameters['edit'][$table][$recordId];
|
||||
|
||||
switch ($action) {
|
||||
case 'edit':
|
||||
$type = 'edit';
|
||||
$titlePrefix = $languageService->sL($languageFile . ':shortcut_edit');
|
||||
break;
|
||||
case 'new':
|
||||
$type = 'new';
|
||||
$titlePrefix = $languageService->sL($languageFile . ':shortcut_create');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if given id is a combined identifier
|
||||
if (!empty($queryParameters['id']) && preg_match('/^[\d]+:/', $queryParameters['id'])) {
|
||||
try {
|
||||
$resourceFactory = ResourceFactory::getInstance();
|
||||
$resource = $resourceFactory->getObjectFromCombinedIdentifier($queryParameters['id']);
|
||||
$title = trim(sprintf(
|
||||
'%s (%s)',
|
||||
$titlePrefix,
|
||||
$resource->getName()
|
||||
));
|
||||
} catch (ResourceDoesNotExistException $e) {
|
||||
}
|
||||
} else {
|
||||
// Lookup the title of this page and use it as default description
|
||||
$pageId = $pageId ?: $recordId ?: $this->extractPageIdFromShortcutUrl($url);
|
||||
$page = $pageId ? BackendUtility::getRecord('pages', $pageId) : null;
|
||||
|
||||
if (!empty($page)) {
|
||||
// Set the name to the title of the page
|
||||
if ($type === 'other') {
|
||||
$title = sprintf(
|
||||
'%s (%s)',
|
||||
$title,
|
||||
$page['title']
|
||||
);
|
||||
} else {
|
||||
$title = sprintf(
|
||||
'%s %s (%s)',
|
||||
$titlePrefix,
|
||||
$languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title']),
|
||||
$page['title']
|
||||
);
|
||||
}
|
||||
} elseif (!empty($table)) {
|
||||
$title = trim(sprintf(
|
||||
'%s %s',
|
||||
$titlePrefix,
|
||||
$languageService->sL($GLOBALS['TCA'][$table]['ctrl']['title'])
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if ($title === 'Shortcut') {
|
||||
$moduleLabels = $this->moduleLoader->getLabelsForModule($module);
|
||||
|
||||
if (!empty($moduleLabels['shortdescription'])) {
|
||||
$title = $this->getLanguageService()->sL($moduleLabels['shortdescription']);
|
||||
}
|
||||
}
|
||||
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_be_shortcuts');
|
||||
$affectedRows = $queryBuilder
|
||||
->insert('sys_be_shortcuts')
|
||||
->values([
|
||||
'userid' => $this->getBackendUser()->user['uid'],
|
||||
'module_name' => $module . '|' . $parentModule,
|
||||
'url' => $url,
|
||||
'description' => $title,
|
||||
'sorting' => $GLOBALS['EXEC_TIME'],
|
||||
])
|
||||
->execute();
|
||||
|
||||
return $affectedRows === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a shortcut
|
||||
*
|
||||
* @param int $id identifier of a shortcut
|
||||
* @param string $title new title of the shortcut
|
||||
* @param int $groupId new group identifier of the shortcut
|
||||
* @return bool
|
||||
*/
|
||||
public function updateShortcut(int $id, string $title, int $groupId): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_be_shortcuts');
|
||||
$queryBuilder->update('sys_be_shortcuts')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->set('description', $title)
|
||||
->set('sc_group', $groupId);
|
||||
|
||||
if (!$backendUser->isAdmin()) {
|
||||
// Users can only modify their own shortcuts
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($backendUser->user['uid'], \PDO::PARAM_INT)
|
||||
)
|
||||
);
|
||||
|
||||
if ($groupId < 0) {
|
||||
$queryBuilder->set('sc_group', 0);
|
||||
}
|
||||
}
|
||||
|
||||
$affectedRows = $queryBuilder->execute();
|
||||
|
||||
return $affectedRows === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a shortcut
|
||||
*
|
||||
* @param int $id identifier of a shortcut
|
||||
* @return bool
|
||||
*/
|
||||
public function removeShortcut(int $id): bool
|
||||
{
|
||||
$shortcut = $this->getShortcutById($id);
|
||||
$success = false;
|
||||
|
||||
if ($shortcut['raw']['userid'] == $this->getBackendUser()->user['uid']) {
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_be_shortcuts');
|
||||
$affectedRows = $queryBuilder->delete('sys_be_shortcuts')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($id, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute();
|
||||
|
||||
if ($affectedRows === 1) {
|
||||
$success = true;
|
||||
}
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the available shortcut groups from default groups, user TSConfig, and global groups
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function initShortcutGroups(): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUser();
|
||||
// By default, 5 groups are set
|
||||
$shortcutGroups = [
|
||||
1 => '1',
|
||||
2 => '1',
|
||||
3 => '1',
|
||||
4 => '1',
|
||||
5 => '1',
|
||||
];
|
||||
|
||||
// Groups from TSConfig
|
||||
$bookmarkGroups = $backendUser->getTSConfig()['options.']['bookmarkGroups.'] ?? [];
|
||||
|
||||
if (is_array($bookmarkGroups)) {
|
||||
foreach ($bookmarkGroups as $groupId => $label) {
|
||||
if (!empty($label)) {
|
||||
$shortcutGroups[$groupId] = (string)$label;
|
||||
} elseif ($backendUser->isAdmin()) {
|
||||
unset($shortcutGroups[$groupId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate global groups, all global groups have negative IDs.
|
||||
if (!empty($shortcutGroups)) {
|
||||
foreach ($shortcutGroups as $groupId => $groupLabel) {
|
||||
$shortcutGroups[$groupId * -1] = $groupLabel;
|
||||
}
|
||||
}
|
||||
|
||||
// Group -100 is kind of superglobal and can't be changed.
|
||||
$shortcutGroups[self::SUPERGLOBAL_GROUP] = '1';
|
||||
|
||||
// Add labels
|
||||
$languageFile = 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf';
|
||||
|
||||
foreach ($shortcutGroups as $groupId => $groupLabel) {
|
||||
$groupId = (int)$groupId;
|
||||
$label = $groupLabel;
|
||||
|
||||
if ($groupLabel === '1') {
|
||||
$label = $languageService->sL($languageFile . ':bookmark_group_' . abs($groupId));
|
||||
|
||||
if (empty($label)) {
|
||||
// Fallback label
|
||||
$label = $languageService->sL($languageFile . ':bookmark_group') . ' ' . abs($groupId);
|
||||
}
|
||||
}
|
||||
|
||||
if ($groupId < 0) {
|
||||
// Global group
|
||||
$label = $languageService->sL($languageFile . ':bookmark_global') . ': ' . (!empty($label) ? $label : abs($groupId));
|
||||
|
||||
if ($groupId === self::SUPERGLOBAL_GROUP) {
|
||||
$label = $languageService->sL($languageFile . ':bookmark_global') . ': ' . $languageService->sL($languageFile . ':bookmark_all');
|
||||
}
|
||||
}
|
||||
|
||||
$shortcutGroups[$groupId] = htmlspecialchars($label);
|
||||
}
|
||||
|
||||
return $shortcutGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the shortcuts for the current user
|
||||
*
|
||||
* @return array Array of shortcuts
|
||||
*/
|
||||
protected function initShortcuts(): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
// Traverse shortcuts
|
||||
$lastGroup = 0;
|
||||
$shortcuts = [];
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_be_shortcuts');
|
||||
$result = $queryBuilder->select('*')
|
||||
->from('sys_be_shortcuts')
|
||||
->where(
|
||||
$queryBuilder->expr()->andX(
|
||||
$queryBuilder->expr()->eq(
|
||||
'userid',
|
||||
$queryBuilder->createNamedParameter($backendUser->user['uid'], \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->gte(
|
||||
'sc_group',
|
||||
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
)
|
||||
->orWhere(
|
||||
$queryBuilder->expr()->in(
|
||||
'sc_group',
|
||||
$queryBuilder->createNamedParameter(
|
||||
array_keys($this->getGlobalShortcutGroups()),
|
||||
Connection::PARAM_INT_ARRAY
|
||||
)
|
||||
)
|
||||
)
|
||||
->orderBy('sc_group')
|
||||
->addOrderBy('sorting')
|
||||
->execute();
|
||||
|
||||
while ($row = $result->fetch()) {
|
||||
$shortcut = ['raw' => $row];
|
||||
|
||||
list($row['module_name'], $row['M_module_name']) = explode('|', $row['module_name']);
|
||||
|
||||
$queryParts = parse_url($row['url']);
|
||||
// Explode GET vars recursively
|
||||
$queryParameters = [];
|
||||
parse_str($queryParts['query'] ?? '', $queryParameters);
|
||||
|
||||
if ($row['module_name'] === 'xMOD_alt_doc.php' && is_array($queryParameters['edit'])) {
|
||||
$shortcut['table'] = key($queryParameters['edit']);
|
||||
$shortcut['recordid'] = key($queryParameters['edit'][$shortcut['table']]);
|
||||
|
||||
if ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] === 'edit') {
|
||||
$shortcut['type'] = 'edit';
|
||||
} elseif ($queryParameters['edit'][$shortcut['table']][$shortcut['recordid']] === 'new') {
|
||||
$shortcut['type'] = 'new';
|
||||
}
|
||||
|
||||
if (substr((string)$shortcut['recordid'], -1) === ',') {
|
||||
$shortcut['recordid'] = substr((string)$shortcut['recordid'], 0, -1);
|
||||
}
|
||||
} else {
|
||||
$shortcut['type'] = 'other';
|
||||
}
|
||||
|
||||
// Check for module access
|
||||
$moduleName = $row['M_module_name'] ?: $row['module_name'];
|
||||
|
||||
// Check if the user has access to this module
|
||||
if (!is_array($this->moduleLoader->checkMod($moduleName))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$pageId = $this->extractPageIdFromShortcutUrl($row['url']);
|
||||
|
||||
if (!$backendUser->isAdmin()) {
|
||||
if (MathUtility::canBeInterpretedAsInteger($pageId)) {
|
||||
// Check for webmount access
|
||||
if ($backendUser->isInWebMount($pageId) === null) {
|
||||
continue;
|
||||
}
|
||||
// Check for record access
|
||||
$pageRow = BackendUtility::getRecord('pages', $pageId);
|
||||
|
||||
if ($pageRow === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$backendUser->doesUserHaveAccess($pageRow, Permission::PAGE_SHOW)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$moduleParts = explode('_', $moduleName);
|
||||
$shortcutGroup = (int)$row['sc_group'];
|
||||
|
||||
if ($shortcutGroup && $lastGroup !== $shortcutGroup && $shortcutGroup !== self::SUPERGLOBAL_GROUP) {
|
||||
$shortcut['groupLabel'] = $this->getShortcutGroupLabel($shortcutGroup);
|
||||
}
|
||||
|
||||
$lastGroup = $shortcutGroup;
|
||||
|
||||
if ($row['description']) {
|
||||
$shortcut['label'] = $row['description'];
|
||||
} else {
|
||||
$shortcut['label'] = GeneralUtility::fixed_lgd_cs(rawurldecode($queryParts['query']), 150);
|
||||
}
|
||||
|
||||
$shortcut['group'] = $shortcutGroup;
|
||||
$shortcut['icon'] = $this->getShortcutIcon($row, $shortcut);
|
||||
$shortcut['iconTitle'] = $this->getShortcutIconTitle($shortcut['label'], $row['module_name'], $row['M_module_name']);
|
||||
$shortcut['action'] = 'jump(' . GeneralUtility::quoteJSvalue($this->getTokenUrl($row['url'])) . ',' . GeneralUtility::quoteJSvalue($moduleName) . ',' . GeneralUtility::quoteJSvalue($moduleParts[0]) . ', ' . (int)$pageId . ');';
|
||||
|
||||
$shortcuts[] = $shortcut;
|
||||
}
|
||||
|
||||
return $shortcuts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of global groups, shortcuts in these groups are available to all users
|
||||
*
|
||||
* @return array Array of global groups
|
||||
*/
|
||||
protected function getGlobalShortcutGroups(): array
|
||||
{
|
||||
$globalGroups = [];
|
||||
|
||||
foreach ($this->shortcutGroups as $groupId => $groupLabel) {
|
||||
if ($groupId < 0) {
|
||||
$globalGroups[$groupId] = $groupLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return $globalGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the label for a shortcut group
|
||||
*
|
||||
* @param int $groupId A shortcut group id
|
||||
* @return string The shortcut group label, can be an empty string if no group was found for the id
|
||||
*/
|
||||
protected function getShortcutGroupLabel(int $groupId): string
|
||||
{
|
||||
return $this->shortcutGroups[$groupId] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the icon for the shortcut
|
||||
*
|
||||
* @param array $row
|
||||
* @param array $shortcut
|
||||
* @return string Shortcut icon as img tag
|
||||
*/
|
||||
protected function getShortcutIcon(array $row, array $shortcut): string
|
||||
{
|
||||
switch ($row['module_name']) {
|
||||
case 'xMOD_alt_doc.php':
|
||||
$table = $shortcut['table'];
|
||||
$recordid = $shortcut['recordid'];
|
||||
$icon = '';
|
||||
|
||||
if ($shortcut['type'] === 'edit') {
|
||||
// Creating the list of fields to include in the SQL query:
|
||||
$selectFields[] = 'uid';
|
||||
$selectFields[] = 'pid';
|
||||
|
||||
if ($table === 'pages') {
|
||||
$selectFields[] = 'module';
|
||||
$selectFields[] = 'extendToSubpages';
|
||||
$selectFields[] = 'doktype';
|
||||
}
|
||||
|
||||
if (is_array($GLOBALS['TCA'][$table]['ctrl']['enablecolumns'])) {
|
||||
$selectFields = array_merge($selectFields, $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']);
|
||||
}
|
||||
|
||||
if ($GLOBALS['TCA'][$table]['ctrl']['typeicon_column']) {
|
||||
$selectFields[] = $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
|
||||
}
|
||||
|
||||
if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
|
||||
$selectFields[] = 't3ver_state';
|
||||
}
|
||||
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($table);
|
||||
$queryBuilder->select(...array_unique(array_values($selectFields)))
|
||||
->from($table)
|
||||
->where(
|
||||
$queryBuilder->expr()->in(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($recordid, \PDO::PARAM_INT)
|
||||
)
|
||||
);
|
||||
|
||||
$row = $queryBuilder->execute()->fetch();
|
||||
|
||||
$icon = $this->iconFactory->getIconForRecord($table, (array)$row, Icon::SIZE_SMALL)->render();
|
||||
} elseif ($shortcut['type'] === 'new') {
|
||||
$icon = $this->iconFactory->getIconForRecord($table, [], Icon::SIZE_SMALL)->render();
|
||||
}
|
||||
break;
|
||||
case 'file_edit':
|
||||
$icon = $this->iconFactory->getIcon('mimetypes-text-html', Icon::SIZE_SMALL)->render();
|
||||
break;
|
||||
case 'wizard_rte':
|
||||
$icon = $this->iconFactory->getIcon('mimetypes-word', Icon::SIZE_SMALL)->render();
|
||||
break;
|
||||
default:
|
||||
$iconIdentifier = '';
|
||||
$moduleName = $row['module_name'];
|
||||
|
||||
if (strpos($moduleName, '_') !== false) {
|
||||
list($mainModule, $subModule) = explode('_', $moduleName, 2);
|
||||
$iconIdentifier = $this->moduleLoader->modules[$mainModule]['sub'][$subModule]['iconIdentifier'];
|
||||
} elseif (!empty($moduleName)) {
|
||||
$iconIdentifier = $this->moduleLoader->modules[$moduleName]['iconIdentifier'];
|
||||
}
|
||||
|
||||
if (!$iconIdentifier) {
|
||||
$iconIdentifier = 'empty-empty';
|
||||
}
|
||||
|
||||
$icon = $this->iconFactory->getIcon($iconIdentifier, Icon::SIZE_SMALL)->render();
|
||||
}
|
||||
|
||||
return $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns title for the shortcut icon
|
||||
*
|
||||
* @param string $shortcutLabel Shortcut label
|
||||
* @param string $moduleName Backend module name (key)
|
||||
* @param string $parentModuleName Parent module label
|
||||
* @return string Title for the shortcut icon
|
||||
*/
|
||||
protected function getShortcutIconTitle(string $shortcutLabel, string $moduleName, string $parentModuleName = ''): string
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
if (strpos($moduleName, 'xMOD_') === 0) {
|
||||
$title = substr($moduleName, 5);
|
||||
} else {
|
||||
list($mainModule, $subModule) = explode('_', $moduleName);
|
||||
$mainModuleLabels = $this->moduleLoader->getLabelsForModule($mainModule);
|
||||
$title = $languageService->sL($mainModuleLabels['title']);
|
||||
|
||||
if (!empty($subModule)) {
|
||||
$subModuleLabels = $this->moduleLoader->getLabelsForModule($moduleName);
|
||||
$title .= '>' . $languageService->sL($subModuleLabels['title']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($parentModuleName) {
|
||||
$title .= ' (' . $parentModuleName . ')';
|
||||
}
|
||||
|
||||
$title .= ': ' . $shortcutLabel;
|
||||
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the page in the URL if found.
|
||||
*
|
||||
* @param string $url The URL of the current shortcut link
|
||||
* @return int If a page ID was found, it is returned. Otherwise: 0
|
||||
*/
|
||||
protected function extractPageIdFromShortcutUrl(string $url): int
|
||||
{
|
||||
return (int)preg_replace('/.*[\\?&]id=([^&]+).*/', '$1', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the correct token, if the url is an index.php script
|
||||
* @todo: this needs love
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
*/
|
||||
protected function getTokenUrl(string $url): string
|
||||
{
|
||||
$parsedUrl = parse_url($url);
|
||||
$parameters = [];
|
||||
parse_str($parsedUrl['query'] ?? '', $parameters);
|
||||
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
// parse the returnUrl and replace the module token of it
|
||||
if (!empty($parameters['returnUrl'])) {
|
||||
$parsedReturnUrl = parse_url($parameters['returnUrl']);
|
||||
$returnUrlParameters = [];
|
||||
parse_str($parsedReturnUrl['query'] ?? '', $returnUrlParameters);
|
||||
|
||||
if (strpos($parsedReturnUrl['path'] ?? '', 'index.php') !== false && !empty($returnUrlParameters['route'])) {
|
||||
$module = $returnUrlParameters['route'];
|
||||
$parameters['returnUrl'] = (string)$uriBuilder->buildUriFromRoutePath($module, $returnUrlParameters);
|
||||
$url = $parsedUrl['path'] . '?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parameters['M']) && empty($parameters['route'])) {
|
||||
$parameters['route'] = $parameters['M'];
|
||||
unset($parameters['M']);
|
||||
}
|
||||
|
||||
if (strpos($parsedUrl['path'], 'index.php') !== false && isset($parameters['route'])) {
|
||||
$routePath = $parameters['route'];
|
||||
/** @var \TYPO3\CMS\Backend\Routing\Router $router */
|
||||
$router = GeneralUtility::makeInstance(Router::class);
|
||||
|
||||
try {
|
||||
$route = $router->match($routePath);
|
||||
|
||||
if ($route) {
|
||||
$routeIdentifier = $route->getOption('_identifier');
|
||||
unset($parameters['route']);
|
||||
$url = (string)$uriBuilder->buildUriFromRoute($routeIdentifier, $parameters);
|
||||
}
|
||||
} catch (ResourceNotFoundException $e) {
|
||||
$url = '';
|
||||
}
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Toolbar\ClearCacheActionsHookInterface;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Render cache clearing toolbar item
|
||||
* Adds a dropdown if there are more than one item to clear (usually for admins to render the flush all caches)
|
||||
*
|
||||
* The dropdown items can be extended via a hook named "cacheActions".
|
||||
*/
|
||||
class ClearCacheToolbarItem implements ToolbarItemInterface
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $cacheActions = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $optionValues = [];
|
||||
|
||||
/**
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Toolbar/ClearCacheMenu');
|
||||
$isAdmin = $this->getBackendUser()->isAdmin();
|
||||
$userTsConfig = $this->getBackendUser()->getTSConfig();
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
|
||||
// Clear all page-related caches
|
||||
if ($isAdmin || $userTsConfig['options.']['clearCache.']['pages'] ?? false) {
|
||||
$this->cacheActions[] = [
|
||||
'id' => 'pages',
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesTitle',
|
||||
'description' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushPageCachesDescription',
|
||||
'href' => (string)$uriBuilder->buildUriFromRoute('tce_db', ['cacheCmd' => 'pages']),
|
||||
'iconIdentifier' => 'actions-system-cache-clear-impact-low'
|
||||
];
|
||||
$this->optionValues[] = 'pages';
|
||||
}
|
||||
|
||||
// Clearing of all caches is only shown if explicitly enabled via TSConfig
|
||||
// or if BE-User is admin and the TSconfig explicitly disables the possibility for admins.
|
||||
// This is useful for big production systems where admins accidentally could slow down the system.
|
||||
if (($userTsConfig['options.']['clearCache.']['all'] ?? false)
|
||||
|| ($isAdmin && (bool)($userTsConfig['options.']['clearCache.']['all'] ?? true))
|
||||
) {
|
||||
$this->cacheActions[] = [
|
||||
'id' => 'all',
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushAllCachesTitle2',
|
||||
'description' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:flushAllCachesDescription2',
|
||||
'href' => (string)$uriBuilder->buildUriFromRoute('tce_db', ['cacheCmd' => 'all']),
|
||||
'iconIdentifier' => 'actions-system-cache-clear-impact-high'
|
||||
];
|
||||
$this->optionValues[] = 'all';
|
||||
}
|
||||
|
||||
// Hook for manipulating cacheActions
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['additionalBackendItems']['cacheActions'] ?? [] as $cacheAction) {
|
||||
$hookObject = GeneralUtility::makeInstance($cacheAction);
|
||||
if (!$hookObject instanceof ClearCacheActionsHookInterface) {
|
||||
throw new \UnexpectedValueException($cacheAction . ' must implement interface ' . ClearCacheActionsHookInterface::class, 1228262000);
|
||||
}
|
||||
$hookObject->manipulateCacheActions($this->cacheActions, $this->optionValues);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item
|
||||
*
|
||||
* @return bool TRUE if user has access, FALSE if not
|
||||
*/
|
||||
public function checkAccess()
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
if ($backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
foreach ($this->optionValues as $value) {
|
||||
if ($backendUser->getTSConfig()['options.']['clearCache.'][$value] ?? false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render clear cache icon, based on the option if there is more than one icon or just one.
|
||||
*
|
||||
* @return string Icon HTML
|
||||
*/
|
||||
public function getItem()
|
||||
{
|
||||
if ($this->hasDropDown()) {
|
||||
return $this->getFluidTemplateObject('ClearCacheToolbarItem.html')->render();
|
||||
}
|
||||
$view = $this->getFluidTemplateObject('ClearCacheToolbarItemSingle.html');
|
||||
$cacheAction = end($this->cacheActions);
|
||||
$view->assignMultiple([
|
||||
'link' => $cacheAction['href'],
|
||||
'title' => $cacheAction['title'],
|
||||
'iconIdentifier' => $cacheAction['iconIdentifier'],
|
||||
]);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop down
|
||||
*
|
||||
* @return string Drop down HTML
|
||||
*/
|
||||
public function getDropDown()
|
||||
{
|
||||
$view = $this->getFluidTemplateObject('ClearCacheToolbarItemDropDown.html');
|
||||
$view->assign('cacheActions', $this->cacheActions);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* No additional attributes needed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalAttributes()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop down if there is more than one cache action available for the current Backend user.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDropDown()
|
||||
{
|
||||
return count($this->cacheActions) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 25;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PageRenderer
|
||||
*/
|
||||
protected function getPageRenderer()
|
||||
{
|
||||
return GeneralUtility::makeInstance(PageRenderer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $filename Which templateFile should be used.
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $filename): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials/ToolbarItems']);
|
||||
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ToolbarItems']);
|
||||
|
||||
$view->setTemplate($filename);
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Domain\Model\Module\BackendModule;
|
||||
use TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Help toolbar item
|
||||
*/
|
||||
class HelpToolbarItem implements ToolbarItemInterface
|
||||
{
|
||||
/**
|
||||
* @var BackendModule
|
||||
*/
|
||||
protected $helpModuleMenu;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$backendModuleRepository = GeneralUtility::makeInstance(BackendModuleRepository::class);
|
||||
$helpModuleMenu = $backendModuleRepository->findByModuleName('help');
|
||||
if ($helpModuleMenu && $helpModuleMenu->getChildren()->count() > 0) {
|
||||
$this->helpModuleMenu = $helpModuleMenu;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Users see this if a module is available
|
||||
*
|
||||
* @return bool TRUE
|
||||
*/
|
||||
public function checkAccess()
|
||||
{
|
||||
return (bool)$this->helpModuleMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render help icon
|
||||
*
|
||||
* @return string toolbar item for the help icon
|
||||
*/
|
||||
public function getItem()
|
||||
{
|
||||
return $this->getFluidTemplateObject('HelpToolbarItem.html')->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop down
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDropDown()
|
||||
{
|
||||
$view = $this->getFluidTemplateObject('HelpToolbarItemDropDown.html');
|
||||
$view->assign('modules', $this->helpModuleMenu->getChildren());
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* No additional attributes needed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalAttributes()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop down
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDropDown()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 70;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $filename Which templateFile should be used.
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $filename): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials/ToolbarItems']);
|
||||
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ToolbarItems']);
|
||||
$view->setTemplate($filename);
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Adds backend live search to the toolbar by adding JavaScript and adding an input search field
|
||||
*/
|
||||
class LiveSearchToolbarItem implements ToolbarItemInterface
|
||||
{
|
||||
/**
|
||||
* Loads the needed JavaScript file, ands includes it to the page renderer
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/LiveSearch');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item,
|
||||
* only allowed when the list module is available.
|
||||
* Live search is heavily dependent on the list module and only available when that module is.
|
||||
*
|
||||
* @return bool TRUE if user has access, FALSE if not
|
||||
*/
|
||||
public function checkAccess()
|
||||
{
|
||||
$backendModuleRepository = GeneralUtility::makeInstance(BackendModuleRepository::class);
|
||||
$listModule = $backendModuleRepository->findByModuleName('web_list');
|
||||
return $listModule !== null && $listModule !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render search field
|
||||
*
|
||||
* @return string Live search form HTML
|
||||
*/
|
||||
public function getItem()
|
||||
{
|
||||
return $this->getFluidTemplateObject('LiveSearchToolbarItem.html')->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* This item needs to additional attributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalAttributes()
|
||||
{
|
||||
return ['class' => 'toolbar-item-search t3js-toolbar-item-search'];
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has no drop down
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDropDown()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* No drop down here
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDropDown()
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others, live search should be very right
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 90;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current PageRenderer
|
||||
*
|
||||
* @return PageRenderer
|
||||
*/
|
||||
protected function getPageRenderer()
|
||||
{
|
||||
return GeneralUtility::makeInstance(PageRenderer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $filename Which templateFile should be used.
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $filename): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials/ToolbarItems']);
|
||||
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ToolbarItems']);
|
||||
|
||||
$view->setTemplate($filename);
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Class to render the shortcut menu
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ShortcutToolbarItem implements ToolbarItemInterface
|
||||
{
|
||||
/**
|
||||
* @var ShortcutRepository
|
||||
*/
|
||||
protected $shortcutRepository;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->shortcutRepository = GeneralUtility::makeInstance(ShortcutRepository::class);
|
||||
|
||||
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
|
||||
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Toolbar/ShortcutMenu');
|
||||
$languageService = $this->getLanguageService();
|
||||
$languageFile = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf';
|
||||
$pageRenderer->addInlineLanguageLabelArray([
|
||||
'bookmark.delete' => $languageService->sL($languageFile . ':toolbarItems.bookmarksDelete'),
|
||||
'bookmark.confirmDelete' => $languageService->sL($languageFile . ':toolbarItems.confirmBookmarksDelete'),
|
||||
'bookmark.create' => $languageService->sL($languageFile . ':toolbarItems.createBookmark'),
|
||||
'bookmark.savedTitle' => $languageService->sL($languageFile . ':toolbarItems.bookmarkSavedTitle'),
|
||||
'bookmark.savedMessage' => $languageService->sL($languageFile . ':toolbarItems.bookmarkSavedMessage'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item
|
||||
*
|
||||
* @return bool TRUE if user has access, FALSE if not
|
||||
*/
|
||||
public function checkAccess()
|
||||
{
|
||||
return (bool)($this->getBackendUser()->getTSConfig()['options.']['enableBookmarks'] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render shortcut icon
|
||||
*
|
||||
* @return string HTML
|
||||
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getItem()
|
||||
{
|
||||
return $this->getFluidTemplateObject('Item.html')->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop down
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDropDown()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop down content
|
||||
*
|
||||
* @return string HTML
|
||||
*/
|
||||
public function getDropDown()
|
||||
{
|
||||
$shortcutMenu = [];
|
||||
$groups = $this->shortcutRepository->getGroupsFromShortcuts();
|
||||
arsort($groups, SORT_NUMERIC);
|
||||
|
||||
foreach ($groups as $groupId => $groupLabel) {
|
||||
$shortcutMenu[] = [
|
||||
'id' => (int)$groupId,
|
||||
'title' => $groupLabel,
|
||||
'shortcuts' => $this->shortcutRepository->getShortcutsByGroup($groupId),
|
||||
];
|
||||
}
|
||||
|
||||
$dropDownView = $this->getFluidTemplateObject('DropDown.html');
|
||||
$dropDownView->assign('shortcutMenu', $shortcutMenu);
|
||||
|
||||
return $dropDownView->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* This toolbar item needs no additional attributes
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalAttributes()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others, live search should be very right
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 20;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $templateFilename
|
||||
* @return StandaloneView
|
||||
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException
|
||||
* @throws \InvalidArgumentException
|
||||
* @internal param string $templateFile
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $templateFilename): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
|
||||
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ShortcutToolbarItem']);
|
||||
$view->setTemplate($templateFilename);
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \TYPO3\CMS\Core\Localization\LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Toolbar\Enumeration\InformationStatus;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Routing\RouteNotFoundException;
|
||||
use TYPO3\CMS\Core\Utility\CommandUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
|
||||
use TYPO3\CMS\Extbase\Object\ObjectManager;
|
||||
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Render system info toolbar item
|
||||
*/
|
||||
class SystemInformationToolbarItem implements ToolbarItemInterface
|
||||
{
|
||||
/**
|
||||
* Number displayed as badge on the dropdown trigger
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $totalCount = 0;
|
||||
|
||||
/**
|
||||
* Holds the highest severity
|
||||
*
|
||||
* @var InformationStatus
|
||||
*/
|
||||
protected $highestSeverity;
|
||||
|
||||
/**
|
||||
* The CSS class for the badge
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $severityBadgeClass = '';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $systemInformation = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $systemMessages = [];
|
||||
|
||||
/**
|
||||
* @var Dispatcher
|
||||
*/
|
||||
protected $signalSlotDispatcher;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $maximumCountInBadge = 99;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Toolbar/SystemInformationMenu');
|
||||
$this->highestSeverity = InformationStatus::cast(InformationStatus::STATUS_INFO);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system message.
|
||||
* This is a callback method for signal receivers.
|
||||
*
|
||||
* @param string $text The text to be displayed
|
||||
* @param string $status The status of this system message
|
||||
* @param int $count Will be added to the total count
|
||||
* @param string $module The associated module
|
||||
* @param string $params Query string with additional parameters
|
||||
*/
|
||||
public function addSystemMessage($text, $status = InformationStatus::STATUS_OK, $count = 0, $module = '', $params = '')
|
||||
{
|
||||
$this->totalCount += (int)$count;
|
||||
|
||||
/** @var InformationStatus $messageSeverity */
|
||||
$messageSeverity = InformationStatus::cast($status);
|
||||
// define the severity for the badge
|
||||
if ($messageSeverity->isGreaterThan($this->highestSeverity)) {
|
||||
$this->highestSeverity = $messageSeverity;
|
||||
}
|
||||
|
||||
$this->systemMessages[] = [
|
||||
'module' => $module,
|
||||
'params' => $params,
|
||||
'count' => (int)$count,
|
||||
'status' => $messageSeverity,
|
||||
'text' => $text
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a system information.
|
||||
* This is a callback method for signal receivers.
|
||||
*
|
||||
* @param string $title The title of this system information
|
||||
* @param string $value The associated value
|
||||
* @param string $iconIdentifier The icon identifier
|
||||
* @param string $status The status of this system information
|
||||
*/
|
||||
public function addSystemInformation($title, $value, $iconIdentifier, $status = InformationStatus::STATUS_NOTICE)
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => $title,
|
||||
'value' => $value,
|
||||
'iconIdentifier' => $iconIdentifier,
|
||||
'status' => $status
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the user has access to this toolbar item
|
||||
*
|
||||
* @return bool TRUE if user has access, FALSE if not
|
||||
*/
|
||||
public function checkAccess()
|
||||
{
|
||||
return $this->getBackendUserAuthentication()->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render system information dropdown
|
||||
*
|
||||
* @return string Icon HTML
|
||||
*/
|
||||
public function getItem()
|
||||
{
|
||||
return $this->getFluidTemplateObject('SystemInformationToolbarItem.html')->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop down
|
||||
*
|
||||
* @return string Drop down HTML
|
||||
*/
|
||||
public function getDropDown()
|
||||
{
|
||||
if (!$this->checkAccess()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->collectInformation();
|
||||
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$view = $this->getFluidTemplateObject('SystemInformationDropDown.html');
|
||||
|
||||
try {
|
||||
$environmentToolUrl = (string)$uriBuilder->buildUriFromRoute('tools_toolsenvironment');
|
||||
} catch (RouteNotFoundException $e) {
|
||||
$environmentToolUrl = '';
|
||||
}
|
||||
|
||||
$view->assignMultiple([
|
||||
'environmentToolUrl' => $environmentToolUrl,
|
||||
'messages' => $this->systemMessages,
|
||||
'count' => $this->totalCount > $this->maximumCountInBadge ? $this->maximumCountInBadge . '+' : $this->totalCount,
|
||||
'severityBadgeClass' => $this->severityBadgeClass,
|
||||
'systemInformation' => $this->systemInformation
|
||||
]);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* No additional attributes needed.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalAttributes()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop down
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDropDown()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 75;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the information for the menu
|
||||
*/
|
||||
protected function collectInformation()
|
||||
{
|
||||
$this->getTypo3Version();
|
||||
$this->getWebServer();
|
||||
$this->getPhpVersion();
|
||||
$this->getDatabase();
|
||||
$this->getApplicationContext();
|
||||
$this->getComposerMode();
|
||||
$this->getGitRevision();
|
||||
$this->getOperatingSystem();
|
||||
|
||||
$this->emitGetSystemInformation();
|
||||
$this->emitLoadMessages();
|
||||
|
||||
$this->severityBadgeClass = !$this->highestSeverity->equals(InformationStatus::STATUS_NOTICE) ? 'badge-' . (string)$this->highestSeverity : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the TYPO3 version
|
||||
*/
|
||||
protected function getTypo3Version()
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.typo3-version',
|
||||
'value' => VersionNumberUtility::getCurrentTypo3Version(),
|
||||
'iconIdentifier' => 'information-typo3-version'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the webserver software
|
||||
*/
|
||||
protected function getWebServer()
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.webserver',
|
||||
'value' => $_SERVER['SERVER_SOFTWARE'],
|
||||
'iconIdentifier' => 'information-webserver'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the PHP version
|
||||
*/
|
||||
protected function getPhpVersion()
|
||||
{
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.phpversion',
|
||||
'value' => PHP_VERSION,
|
||||
'iconIdentifier' => 'information-php-version'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database info
|
||||
*/
|
||||
protected function getDatabase()
|
||||
{
|
||||
foreach (GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionNames() as $connectionName) {
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.database',
|
||||
'titleAddition' => $connectionName,
|
||||
'value' => GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionByName($connectionName)
|
||||
->getServerVersion(),
|
||||
'iconIdentifier' => 'information-database'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the application context
|
||||
*/
|
||||
protected function getApplicationContext()
|
||||
{
|
||||
$applicationContext = GeneralUtility::getApplicationContext();
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.applicationcontext',
|
||||
'value' => (string)$applicationContext,
|
||||
'status' => $applicationContext->isProduction() ? InformationStatus::STATUS_OK : InformationStatus::STATUS_WARNING,
|
||||
'iconIdentifier' => 'information-application-context'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the information if the Composer mode is enabled or disabled to the displayed system information
|
||||
*/
|
||||
protected function getComposerMode()
|
||||
{
|
||||
if (!Environment::isComposerMode()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.composerMode',
|
||||
'value' => $GLOBALS['LANG']->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.enabled'),
|
||||
'iconIdentifier' => 'information-composer-mode'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current GIT revision and branch
|
||||
*/
|
||||
protected function getGitRevision()
|
||||
{
|
||||
if (!StringUtility::endsWith(TYPO3_version, '-dev') || SystemEnvironmentBuilder::isFunctionDisabled('exec')) {
|
||||
return;
|
||||
}
|
||||
// check if git exists
|
||||
CommandUtility::exec('git --version', $_, $returnCode);
|
||||
if ((int)$returnCode !== 0) {
|
||||
// git is not available
|
||||
return;
|
||||
}
|
||||
|
||||
$revision = trim(CommandUtility::exec('git rev-parse --short HEAD'));
|
||||
$branch = trim(CommandUtility::exec('git rev-parse --abbrev-ref HEAD'));
|
||||
if (!empty($revision) && !empty($branch)) {
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.gitrevision',
|
||||
'value' => sprintf('%s [%s]', $revision, $branch),
|
||||
'iconIdentifier' => 'information-git'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the system kernel and version
|
||||
*/
|
||||
protected function getOperatingSystem()
|
||||
{
|
||||
switch (PHP_OS_FAMILY) {
|
||||
case 'Linux':
|
||||
$icon = 'linux';
|
||||
break;
|
||||
case 'Darwin':
|
||||
$icon = 'apple';
|
||||
break;
|
||||
case 'Windows':
|
||||
$icon = 'windows';
|
||||
break;
|
||||
default:
|
||||
$icon = 'unknown';
|
||||
}
|
||||
$this->systemInformation[] = [
|
||||
'title' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:toolbarItems.sysinfo.operatingsystem',
|
||||
'value' => PHP_OS . ' ' . php_uname('r'),
|
||||
'iconIdentifier' => 'information-os-' . $icon
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the "getSystemInformation" signal
|
||||
*/
|
||||
protected function emitGetSystemInformation()
|
||||
{
|
||||
$this->getSignalSlotDispatcher()->dispatch(__CLASS__, 'getSystemInformation', [$this]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emits the "loadMessages" signal
|
||||
*/
|
||||
protected function emitLoadMessages()
|
||||
{
|
||||
$this->getSignalSlotDispatcher()->dispatch(__CLASS__, 'loadMessages', [$this]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $filename Which templateFile should be used.
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $filename): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials/ToolbarItems']);
|
||||
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ToolbarItems']);
|
||||
|
||||
$view->setTemplate($filename);
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current PageRenderer
|
||||
*
|
||||
* @return PageRenderer
|
||||
*/
|
||||
protected function getPageRenderer()
|
||||
{
|
||||
return GeneralUtility::makeInstance(PageRenderer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SignalSlot dispatcher
|
||||
*
|
||||
* @return Dispatcher
|
||||
*/
|
||||
protected function getSignalSlotDispatcher()
|
||||
{
|
||||
if (!isset($this->signalSlotDispatcher)) {
|
||||
$this->signalSlotDispatcher = GeneralUtility::makeInstance(ObjectManager::class)->get(Dispatcher::class);
|
||||
}
|
||||
return $this->signalSlotDispatcher;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Backend\ToolbarItems;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* User toolbar item
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class UserToolbarItem implements ToolbarItemInterface
|
||||
{
|
||||
/**
|
||||
* Item is always enabled
|
||||
*
|
||||
* @return bool TRUE
|
||||
*/
|
||||
public function checkAccess()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render username and an icon
|
||||
*
|
||||
* @return string HTML
|
||||
*/
|
||||
public function getItem()
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$view = $this->getFluidTemplateObject('UserToolbarItem.html');
|
||||
$view->assignMultiple([
|
||||
'currentUser' => $backendUser->user,
|
||||
'switchUserMode' => $backendUser->user['ses_backuserid'],
|
||||
]);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render drop down
|
||||
*
|
||||
* @return string HTML
|
||||
*/
|
||||
public function getDropDown()
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
|
||||
/** @var BackendModuleRepository $backendModuleRepository */
|
||||
$backendModuleRepository = GeneralUtility::makeInstance(BackendModuleRepository::class);
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
|
||||
$mostRecentUsers = [];
|
||||
if (ExtensionManagementUtility::isLoaded('beuser')
|
||||
&& $backendUser->isAdmin()
|
||||
&& (int)$backendUser->user['ses_backuserid'] === 0
|
||||
&& isset($backendUser->uc['recentSwitchedToUsers'])
|
||||
&& is_array($backendUser->uc['recentSwitchedToUsers'])
|
||||
) {
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
|
||||
$result = $queryBuilder
|
||||
->select('uid', 'username', 'realName')
|
||||
->from('be_users')
|
||||
->where(
|
||||
$queryBuilder->expr()->in('uid', $queryBuilder->createNamedParameter($backendUser->uc['recentSwitchedToUsers'], Connection::PARAM_INT_ARRAY))
|
||||
)->execute();
|
||||
|
||||
// Flip the array to have a "sorted" list of items
|
||||
$mostRecentUsers = array_flip($backendUser->uc['recentSwitchedToUsers']);
|
||||
|
||||
while ($row = $result->fetch()) {
|
||||
$row['switchUserLink'] = (string)$uriBuilder->buildUriFromRoute(
|
||||
'system_BeuserTxBeuser',
|
||||
[
|
||||
'SwitchUser' => $row['uid']
|
||||
]
|
||||
);
|
||||
|
||||
$mostRecentUsers[$row['uid']] = $row;
|
||||
}
|
||||
|
||||
// Remove any item that is not an array (means, the stored uid is not available anymore)
|
||||
$mostRecentUsers = array_filter($mostRecentUsers, function ($record) {
|
||||
return is_array($record);
|
||||
});
|
||||
|
||||
$availableUsers = array_keys($mostRecentUsers);
|
||||
if (!empty(array_diff($backendUser->uc['recentSwitchedToUsers'], $availableUsers))) {
|
||||
$backendUser->uc['recentSwitchedToUsers'] = $availableUsers;
|
||||
$backendUser->writeUC();
|
||||
}
|
||||
}
|
||||
|
||||
$view = $this->getFluidTemplateObject('UserToolbarItemDropDown.html');
|
||||
$view->assignMultiple([
|
||||
'modules' => $backendModuleRepository->findByModuleName('user')->getChildren(),
|
||||
'logoutUrl' => (string)$uriBuilder->buildUriFromRoute('logout'),
|
||||
'switchUserMode' => $this->getBackendUser()->user['ses_backuserid'],
|
||||
'recentUsers' => $mostRecentUsers,
|
||||
]);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an additional class if user is in "switch user" mode
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAdditionalAttributes()
|
||||
{
|
||||
$result = [
|
||||
'class' => 'toolbar-item-user'
|
||||
];
|
||||
if ($this->getBackendUser()->user['ses_backuserid']) {
|
||||
$result['class'] .= ' su-user';
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This item has a drop down
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDropDown()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Position relative to others
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getIndex()
|
||||
{
|
||||
return 80;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $filename Which templateFile should be used.
|
||||
*
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $filename): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials/ToolbarItems']);
|
||||
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ToolbarItems']);
|
||||
|
||||
$view->setTemplate($filename);
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Command;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Core function for locking the TYPO3 Backend
|
||||
*/
|
||||
class LockBackendCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Configure the command by defining the name, options and arguments
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setDescription('Lock the TYPO3 Backend')
|
||||
->addArgument(
|
||||
'redirect',
|
||||
InputArgument::OPTIONAL,
|
||||
'If set, then the TYPO3 Backend will redirect to the locking state (only used when locking the TYPO3 Backend'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command for adding the lock file
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title($this->getDescription());
|
||||
$lockFile = $this->getLockFileName();
|
||||
if (@is_file($lockFile)) {
|
||||
$io->note('A lock file already exists. Overwriting it.');
|
||||
}
|
||||
$output = 'Wrote lock file to "' . $lockFile . '"';
|
||||
if ($input->getArgument('redirect')) {
|
||||
$lockFileContent = $input->getArgument('redirect');
|
||||
$output .= LF . 'with content "' . $lockFileContent . '".';
|
||||
} else {
|
||||
$lockFileContent = '';
|
||||
$output .= '.';
|
||||
}
|
||||
GeneralUtility::writeFile($lockFile, $lockFileContent);
|
||||
$io->success($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Location of the file name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getLockFileName()
|
||||
{
|
||||
return Environment::getLegacyConfigPath() . '/LOCK_BACKEND';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Command;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Database\ReferenceIndex;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Core function to check/update the Reference Index
|
||||
*/
|
||||
class ReferenceIndexUpdateCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Configure the command by defining the name, options and arguments
|
||||
*/
|
||||
public function configure()
|
||||
{
|
||||
$this->setDescription('Update the reference index of TYPO3')
|
||||
->addOption(
|
||||
'check',
|
||||
'c',
|
||||
InputOption::VALUE_NONE,
|
||||
'Only check the reference index of TYPO3'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command for adding or removing the lock file
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
Bootstrap::initializeBackendAuthentication();
|
||||
|
||||
$isTestOnly = $input->getOption('check');
|
||||
$isSilent = $output->getVerbosity() !== OutputInterface::VERBOSITY_QUIET;
|
||||
|
||||
/** @var ReferenceIndex $referenceIndex */
|
||||
$referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
|
||||
$referenceIndex->enableRuntimeCache();
|
||||
$referenceIndex->updateIndex($isTestOnly, $isSilent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Command;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* Core function for unlocking the TYPO3 Backend
|
||||
*/
|
||||
class UnlockBackendCommand extends Command
|
||||
{
|
||||
/**
|
||||
* Configure the command by defining the name, options and arguments
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->setDescription('Unlock the TYPO3 Backend');
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the command for removing the lock file
|
||||
*
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title($this->getDescription());
|
||||
$lockFile = $this->getLockFileName();
|
||||
if (@is_file($lockFile)) {
|
||||
unlink($lockFile);
|
||||
if (@is_file($lockFile)) {
|
||||
$io->caution('Could not remove lock file "' . $lockFile . '"!');
|
||||
} else {
|
||||
$io->success('Removed lock file "' . $lockFile . '".');
|
||||
}
|
||||
} else {
|
||||
$io->note('No lock file "' . $lockFile . '" was found.' . LF . 'Hence no lock can be removed.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Location of the file name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getLockFileName()
|
||||
{
|
||||
return Environment::getLegacyConfigPath() . '/LOCK_BACKEND';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Composer;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Composer\Script\Event;
|
||||
use TYPO3\CMS\Composer\Plugin\Core\InstallerScripts\EntryPoint;
|
||||
use TYPO3\CMS\Composer\Plugin\Core\InstallerScriptsRegistration;
|
||||
use TYPO3\CMS\Composer\Plugin\Core\ScriptDispatcher;
|
||||
|
||||
/**
|
||||
* Hook into Composer build to generate TYPO3 backend entry script
|
||||
*
|
||||
* @internal this is used internally for TYPO3 core only
|
||||
*/
|
||||
class InstallerScripts implements InstallerScriptsRegistration
|
||||
{
|
||||
/**
|
||||
* @param Event $event
|
||||
* @param ScriptDispatcher $scriptDispatcher
|
||||
*/
|
||||
public static function register(Event $event, ScriptDispatcher $scriptDispatcher)
|
||||
{
|
||||
$scriptDispatcher->addInstallerScript(
|
||||
new EntryPoint(
|
||||
dirname(__DIR__, 2) . '/Resources/Private/Php/backend.php',
|
||||
'typo3/index.php'
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Configuration;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Convenience wrapper for backend user configuration
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class BackendUserConfiguration
|
||||
{
|
||||
/**
|
||||
* @var BackendUserAuthentication
|
||||
*/
|
||||
protected $backendUser;
|
||||
|
||||
/**
|
||||
* @param BackendUserAuthentication|null $backendUser
|
||||
*/
|
||||
public function __construct(BackendUserAuthentication $backendUser = null)
|
||||
{
|
||||
$this->backendUser = $backendUser ?: $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a specific user setting
|
||||
*
|
||||
* @param string $key Identifier, allows also dotted notation for subarrays
|
||||
* @return mixed Value associated
|
||||
*/
|
||||
public function get(string $key)
|
||||
{
|
||||
return (strpos($key, '.') !== false) ? $this->getFromDottedNotation($key) : $this->backendUser->uc[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all user settings
|
||||
*
|
||||
* @return mixed all values, usually a multi-dimensional array
|
||||
*/
|
||||
public function getAll()
|
||||
{
|
||||
return $this->backendUser->uc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets user settings by key/value pair
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function set(string $key, $value): void
|
||||
{
|
||||
if (strpos($key, '.') !== false) {
|
||||
$this->setFromDottedNotation($key, $value);
|
||||
} else {
|
||||
$this->backendUser->uc[$key] = $value;
|
||||
}
|
||||
|
||||
$this->backendUser->writeUC($this->backendUser->uc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an value to an Comma-separated list
|
||||
* stored in $key of user settings
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function addToList(string $key, $value): void
|
||||
{
|
||||
$list = $this->get($key);
|
||||
|
||||
if (!isset($list)) {
|
||||
$list = $value;
|
||||
} elseif (!GeneralUtility::inList($list, $value)) {
|
||||
$list .= ',' . $value;
|
||||
}
|
||||
|
||||
$this->set($key, $list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an value from an Comma-separated list
|
||||
* stored $key of user settings
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function removeFromList(string $key, $value): void
|
||||
{
|
||||
$list = $this->get($key);
|
||||
|
||||
if (GeneralUtility::inList($list, $value)) {
|
||||
$list = GeneralUtility::trimExplode(',', $list, true);
|
||||
$list = ArrayUtility::removeArrayEntryByValue($list, $value);
|
||||
$this->set($key, implode(',', $list));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the user settings to the default
|
||||
*/
|
||||
public function clear(): void
|
||||
{
|
||||
$this->backendUser->resetUC();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsets a key in user settings
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function unsetOption(string $key): void
|
||||
{
|
||||
if (isset($this->backendUser->uc[$key])) {
|
||||
unset($this->backendUser->uc[$key]);
|
||||
$this->backendUser->writeUC($this->backendUser->uc);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the subarray from dotted notation
|
||||
*
|
||||
* @param string $key Dotted notation of subkeys like moduleData.module1.general.checked
|
||||
* @return mixed value of the settings
|
||||
*/
|
||||
protected function getFromDottedNotation(string $key)
|
||||
{
|
||||
$subkeys = GeneralUtility::trimExplode('.', $key);
|
||||
$configuration = $this->backendUser->uc;
|
||||
|
||||
foreach ($subkeys as $subkey) {
|
||||
if (isset($configuration[$subkey])) {
|
||||
$configuration = &$configuration[$subkey];
|
||||
} else {
|
||||
$configuration = [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of a key written in dotted notation
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*/
|
||||
protected function setFromDottedNotation(string $key, $value): void
|
||||
{
|
||||
$subkeys = GeneralUtility::trimExplode('.', $key, true);
|
||||
$lastKey = $subkeys[count($subkeys) - 1];
|
||||
$configuration = &$this->backendUser->uc;
|
||||
|
||||
foreach ($subkeys as $subkey) {
|
||||
if ($subkey === $lastKey) {
|
||||
$configuration[$subkey] = $value;
|
||||
} else {
|
||||
$configuration = &$configuration[$subkey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Backend\Configuration;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Helper class for the backend "Sites" module
|
||||
*
|
||||
* Load Site configuration TCA from ext:*Configuration/SiteConfiguration
|
||||
* and ext:*Configuration/SiteConfiguration/Overrides
|
||||
*
|
||||
* @internal This class is a specific Backend implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SiteTcaConfiguration
|
||||
{
|
||||
/**
|
||||
* Returns a "fake TCA" array that is syntactically identical to
|
||||
* "normal" TCA, but is not available as $GLOBALS['TCA']. During
|
||||
* configuration loading time, the target array is available as
|
||||
* $GLOBALS['SiteConfiguration'] within the Overrides files.
|
||||
*
|
||||
* It is not possible to use ExtensionManagementUtility methods.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTca(): array
|
||||
{
|
||||
$GLOBALS['SiteConfiguration'] = [];
|
||||
$activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
|
||||
// First load "full table" files from Configuration/SiteConfiguration
|
||||
$finder = (new Finder())->files()->depth(0)->name('*.php');
|
||||
$hasDirectoryEntries = false;
|
||||
foreach ($activePackages as $package) {
|
||||
try {
|
||||
$finder->in($package->getPackagePath() . 'Configuration/SiteConfiguration');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// No such directory in this package
|
||||
continue;
|
||||
}
|
||||
$hasDirectoryEntries = true;
|
||||
}
|
||||
if ($hasDirectoryEntries) {
|
||||
foreach ($finder as $fileInfo) {
|
||||
$GLOBALS['SiteConfiguration'][substr($fileInfo->getBasename(), 0, -4)] = require $fileInfo->getPathname();
|
||||
}
|
||||
}
|
||||
// Execute override files from Configuration/SiteConfiguration/Overrides
|
||||
$finder = (new Finder())->files()->depth(0)->name('*.php');
|
||||
$hasDirectoryEntries = false;
|
||||
foreach ($activePackages as $package) {
|
||||
try {
|
||||
$finder->in($package->getPackagePath() . 'Configuration/SiteConfiguration/Overrides');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// No such directory in this package
|
||||
continue;
|
||||
}
|
||||
$hasDirectoryEntries = true;
|
||||
}
|
||||
if ($hasDirectoryEntries) {
|
||||
foreach ($finder as $fileInfo) {
|
||||
require $fileInfo->getPathname();
|
||||
}
|
||||
}
|
||||
$result = $GLOBALS['SiteConfiguration'];
|
||||
unset($GLOBALS['SiteConfiguration']);
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Configuration\TCA;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
|
||||
/**
|
||||
* This class provides user functions for the usage in TCA definition
|
||||
* @internal
|
||||
*/
|
||||
class UserFunctions
|
||||
{
|
||||
/**
|
||||
* Used to build the IRRE title of a site language element
|
||||
*
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function getSiteLanguageTitle(array &$parameters): void
|
||||
{
|
||||
$record = $parameters['row'];
|
||||
$parameters['title'] = sprintf(
|
||||
'%s %s (%s) Base: %s',
|
||||
$record['enabled'] ? '' : '[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disabled') . ']',
|
||||
$record['title'],
|
||||
$record['locale'],
|
||||
$record['base']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to build the IRRE title of a site route element
|
||||
*
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function getRouteTitle(array &$parameters): void
|
||||
{
|
||||
$record = $parameters['row'];
|
||||
if ($record['type'][0] === 'uri') {
|
||||
$parameters['title'] = sprintf(
|
||||
'%s %s %s',
|
||||
$record['route'],
|
||||
$this->getLanguageService()->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration_tca.xlf:site.routes.irreHeader.redirectsTo'),
|
||||
$record['source'] ?: '[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:undefined') . ']'
|
||||
);
|
||||
} else {
|
||||
$parameters['title'] = $record['route'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to build the IRRE title of a site error handling element
|
||||
* @param array $parameters
|
||||
*/
|
||||
public function getErrorHandlingTitle(array &$parameters): void
|
||||
{
|
||||
$record = $parameters['row'];
|
||||
$format = '%s: %s';
|
||||
$arguments = [$record['errorCode']];
|
||||
switch ($record['errorHandler'][0]) {
|
||||
case 'Fluid':
|
||||
$arguments[] = $record['errorFluidTemplate'];
|
||||
break;
|
||||
case 'Page':
|
||||
$arguments[] = $record['errorContentSource'];
|
||||
break;
|
||||
case 'PHP':
|
||||
$arguments[] = $record['errorPhpClassFQCN'];
|
||||
break;
|
||||
default:
|
||||
$arguments[] = $record['errorHandler'][0];
|
||||
}
|
||||
$parameters['title'] = sprintf($format, ...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
<?php
|
||||
|
||||
namespace TYPO3\CMS\Backend\Configuration;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Routing\SiteMatcher;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Contains translation tools
|
||||
*
|
||||
* @internal The whole class is subject to be removed, fetch all language info from the current site object.
|
||||
*/
|
||||
class TranslationConfigurationProvider
|
||||
{
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns array of system languages
|
||||
* The property flagIcon returns a string <flags-xx>.
|
||||
*
|
||||
* @param int $pageId Page id (used to get TSconfig configuration setting flag and label for default language)
|
||||
* @return array Array with languages (uid, title, ISOcode, flagIcon)
|
||||
*/
|
||||
public function getSystemLanguages($pageId = 0)
|
||||
{
|
||||
try {
|
||||
$site = GeneralUtility::makeInstance(SiteMatcher::class)->matchByPageId((int)$pageId);
|
||||
$siteLanguages = $site->getAvailableLanguages($this->getBackendUserAuthentication(), true);
|
||||
|
||||
if (!isset($siteLanguages[0])) {
|
||||
$siteLanguages[0] = $site->getDefaultLanguage();
|
||||
ksort($siteLanguages);
|
||||
}
|
||||
|
||||
$languages = [];
|
||||
foreach ($siteLanguages as $id => $siteLanguage) {
|
||||
$languages[$id] = [
|
||||
'uid' => $id,
|
||||
'title' => $siteLanguage->getTitle(),
|
||||
'ISOcode' => $siteLanguage->getTwoLetterIsoCode(),
|
||||
'flagIcon' => $siteLanguage->getFlagIdentifier(),
|
||||
];
|
||||
}
|
||||
} catch (SiteNotFoundException $e) {
|
||||
// default language and "all languages" are always present
|
||||
$modSharedTSconfig = BackendUtility::getPagesTSconfig($pageId)['mod.']['SHARED.'] ?? [];
|
||||
$languages = [
|
||||
// 0: default language
|
||||
0 => [
|
||||
'uid' => 0,
|
||||
'title' => $this->getDefaultLanguageLabel($modSharedTSconfig),
|
||||
'ISOcode' => 'DEF',
|
||||
'flagIcon' => $this->getDefaultLanguageFlag($modSharedTSconfig),
|
||||
],
|
||||
// -1: all languages
|
||||
-1 => [
|
||||
'uid' => -1,
|
||||
'title' => $this->getLanguageService()->getLL('multipleLanguages'),
|
||||
'ISOcode' => 'DEF',
|
||||
'flagIcon' => 'flags-multiple',
|
||||
],
|
||||
];
|
||||
|
||||
// add the additional languages from database records
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_language');
|
||||
$languageRecords = $queryBuilder
|
||||
->select('*')
|
||||
->from('sys_language')
|
||||
->orderBy('sorting')
|
||||
->execute()
|
||||
->fetchAll();
|
||||
foreach ($languageRecords as $languageRecord) {
|
||||
$languages[$languageRecord['uid']] = $languageRecord;
|
||||
// @todo: this should probably resolve language_isocode too and throw a deprecation if not filled
|
||||
if ($languageRecord['static_lang_isocode'] && ExtensionManagementUtility::isLoaded('static_info_tables')) {
|
||||
$staticLangRow = BackendUtility::getRecord('static_languages', $languageRecord['static_lang_isocode'], 'lg_iso_2');
|
||||
if ($staticLangRow['lg_iso_2']) {
|
||||
$languages[$languageRecord['uid']]['ISOcode'] = $staticLangRow['lg_iso_2'];
|
||||
}
|
||||
}
|
||||
if ($languageRecord['flag'] !== '') {
|
||||
$languages[$languageRecord['uid']]['flagIcon'] = 'flags-' . $languageRecord['flag'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about translation for an element
|
||||
* Will overlay workspace version of record too!
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param int $uid Record uid
|
||||
* @param int $languageUid Language uid. If 0, then all languages are selected.
|
||||
* @param array $row The record to be translated
|
||||
* @param string $selFieldList Select fields for the query which fetches the translations of the current record
|
||||
* @return mixed Array with information or error message as a string.
|
||||
*/
|
||||
public function translationInfo($table, $uid, $languageUid = 0, array $row = null, $selFieldList = '')
|
||||
{
|
||||
if (!$GLOBALS['TCA'][$table] || !$uid) {
|
||||
return 'No table "' . $table . '" or no UID value';
|
||||
}
|
||||
if ($row === null) {
|
||||
$row = BackendUtility::getRecordWSOL($table, $uid);
|
||||
}
|
||||
if (!is_array($row)) {
|
||||
return 'Record "' . $table . '_' . $uid . '" was not found';
|
||||
}
|
||||
if (!BackendUtility::isTableLocalizable($table)) {
|
||||
return 'Translation is not supported for this table!';
|
||||
}
|
||||
if ($row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0) {
|
||||
return 'Record "' . $table . '_' . $uid . '" seems to be a translation already (has a language value "' . $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']] . '", relation to record "' . $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] . '")';
|
||||
}
|
||||
if ($row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] != 0) {
|
||||
return 'Record "' . $table . '_' . $uid . '" seems to be a translation already (has a relation to record "' . $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] . '")';
|
||||
}
|
||||
// Look for translations of this record, index by language field value:
|
||||
if (!$selFieldList) {
|
||||
$selFieldList = 'uid,' . $GLOBALS['TCA'][$table]['ctrl']['languageField'];
|
||||
}
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
|
||||
$queryBuilder
|
||||
->select(...GeneralUtility::trimExplode(',', $selFieldList))
|
||||
->from($table)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'],
|
||||
$queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter(
|
||||
$row['pid'],
|
||||
\PDO::PARAM_INT
|
||||
)
|
||||
)
|
||||
);
|
||||
if (!$languageUid) {
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->gt(
|
||||
$GLOBALS['TCA'][$table]['ctrl']['languageField'],
|
||||
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$queryBuilder
|
||||
->andWhere(
|
||||
$queryBuilder->expr()->eq(
|
||||
$GLOBALS['TCA'][$table]['ctrl']['languageField'],
|
||||
$queryBuilder->createNamedParameter($languageUid, \PDO::PARAM_INT)
|
||||
)
|
||||
);
|
||||
}
|
||||
$translationRecords = $queryBuilder
|
||||
->execute()
|
||||
->fetchAll();
|
||||
|
||||
$translations = [];
|
||||
$translationsErrors = [];
|
||||
foreach ($translationRecords as $translationRecord) {
|
||||
if (!isset($translations[$translationRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]])) {
|
||||
$translations[$translationRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]] = $translationRecord;
|
||||
} else {
|
||||
$translationsErrors[$translationRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']]][] = $translationRecord;
|
||||
}
|
||||
}
|
||||
return [
|
||||
'table' => $table,
|
||||
'uid' => $uid,
|
||||
'CType' => $row['CType'],
|
||||
'sys_language_uid' => $row[$GLOBALS['TCA'][$table]['ctrl']['languageField']],
|
||||
'translations' => $translations,
|
||||
'excessive_translations' => $translationsErrors
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table in which translations for input table is found.
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @return string
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 as foreign translation table is not supported anymore
|
||||
*/
|
||||
public function getTranslationTable($table)
|
||||
{
|
||||
trigger_error('TranslationConfigurationProvider->getTranslationTable() will be removed in TYPO3 v10.0, as the translation table is always the same as the original table.', E_USER_DEPRECATED);
|
||||
return BackendUtility::isTableLocalizable($table) ? $table : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE, if the input table has localization enabled and done so with records from the same table
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @return bool
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 as foreign translation table is not supported anymore
|
||||
*/
|
||||
public function isTranslationInOwnTable($table)
|
||||
{
|
||||
trigger_error('TranslationConfigurationProvider->isTranslationInOwnTable() will be removed in TYPO3 v10.0, as the translation table is always the same as the original table.', E_USER_DEPRECATED);
|
||||
return $GLOBALS['TCA'][$table]['ctrl']['languageField'] && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns foreign translation table, if any.
|
||||
* Since TYPO3 v9, even "pages" translations are stored in the same table, having this method return always
|
||||
* empty, as with other tables as well.
|
||||
*
|
||||
* @param string $table The table name
|
||||
* @return string Translation foreign table
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0 as foreign translation table is not supported anymore
|
||||
*/
|
||||
public function foreignTranslationTable($table)
|
||||
{
|
||||
trigger_error('TranslationConfigurationProvider->foreignTranslationTable() will be removed in TYPO3 v10.0, as the translation table is always the same as the original table.', E_USER_DEPRECATED);
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $modSharedTSconfig
|
||||
* @return string
|
||||
*/
|
||||
protected function getDefaultLanguageFlag(array $modSharedTSconfig)
|
||||
{
|
||||
if (strlen($modSharedTSconfig['defaultLanguageFlag'])) {
|
||||
$defaultLanguageFlag = 'flags-' . $modSharedTSconfig['defaultLanguageFlag'];
|
||||
} else {
|
||||
$defaultLanguageFlag = 'empty-empty';
|
||||
}
|
||||
return $defaultLanguageFlag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $modSharedTSconfig
|
||||
* @return string
|
||||
*/
|
||||
protected function getDefaultLanguageLabel(array $modSharedTSconfig)
|
||||
{
|
||||
if (strlen($modSharedTSconfig['defaultLanguageLabel'])) {
|
||||
$defaultLanguageLabel = $modSharedTSconfig['defaultLanguageLabel'] . ' (' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage') . ')';
|
||||
} else {
|
||||
$defaultLanguageLabel = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:defaultLanguage');
|
||||
}
|
||||
return $defaultLanguageLabel;
|
||||
}
|
||||
|
||||
protected function getBackendUserAuthentication(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Configuration;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Cache\CacheManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A TS-Config parsing class which performs condition evaluation
|
||||
*/
|
||||
class TsConfigParser extends \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $rootLine = [];
|
||||
|
||||
/**
|
||||
* The uid of the page being handled
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* Parses the passed TS-Config using conditions and caching
|
||||
*
|
||||
* @param string $TStext The TSConfig being parsed
|
||||
* @param string $type The type of TSConfig (either "userTS" or "PAGES")
|
||||
* @param int $id The uid of the page being handled
|
||||
* @param array $rootLine The rootline of the page being handled
|
||||
* @return array Array containing the parsed TSConfig and a flag whether the content was retrieved from cache
|
||||
*/
|
||||
public function parseTSconfig($TStext, $type, $id = 0, array $rootLine = [])
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->id = $id;
|
||||
$this->rootLine = $rootLine;
|
||||
$hash = md5($type . ':' . $TStext);
|
||||
|
||||
$cache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_hash');
|
||||
$cachedContent = $cache->get($hash);
|
||||
if (is_array($cachedContent)) {
|
||||
$storedData = $cachedContent[0];
|
||||
$storedMD5 = $cachedContent[1];
|
||||
$storedData['match'] = [];
|
||||
$storedData = $this->matching($storedData);
|
||||
$checkMD5 = md5(serialize($storedData));
|
||||
if ($checkMD5 == $storedMD5) {
|
||||
$res = [
|
||||
'TSconfig' => $storedData['TSconfig'],
|
||||
'cached' => 1,
|
||||
'hash' => $hash
|
||||
];
|
||||
} else {
|
||||
$shash = md5($checkMD5 . $hash);
|
||||
$cachedSpec = $cache->get($shash);
|
||||
if (is_array($cachedSpec)) {
|
||||
$storedData = $cachedSpec;
|
||||
$res = [
|
||||
'TSconfig' => $storedData['TSconfig'],
|
||||
'cached' => 1,
|
||||
'hash' => $shash
|
||||
];
|
||||
} else {
|
||||
$storeData = $this->parseWithConditions($TStext);
|
||||
$res = [
|
||||
'TSconfig' => $storeData['TSconfig'],
|
||||
'cached' => 0,
|
||||
'hash' => $shash
|
||||
];
|
||||
$cache->set($shash, $storeData, ['ident_' . $type . '_TSconfig'], 0);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$storeData = $this->parseWithConditions($TStext);
|
||||
$md5 = md5(serialize($storeData));
|
||||
$cache->set($hash, [$storeData, $md5], ['ident_' . $type . '_TSconfig'], 0);
|
||||
$res = [
|
||||
'TSconfig' => $storeData['TSconfig'],
|
||||
'cached' => 0,
|
||||
'hash' => $hash
|
||||
];
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the actual parsing using the parent objects "parse" method. Creates the match-Object
|
||||
*
|
||||
* @param string $TSconfig The TSConfig being parsed
|
||||
* @return array Array containing the parsed TSConfig, the encountered sectiosn, the matched sections
|
||||
*/
|
||||
protected function parseWithConditions($TSconfig)
|
||||
{
|
||||
/** @var \TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher $matchObj */
|
||||
$matchObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher::class);
|
||||
$matchObj->setRootline($this->rootLine);
|
||||
$matchObj->setPageId($this->id);
|
||||
$this->parse($TSconfig, $matchObj);
|
||||
return [
|
||||
'TSconfig' => $this->setup,
|
||||
'sections' => $this->sections,
|
||||
'match' => $this->sectionsMatch
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is just going through an array of conditions to determine which are matching (for getting correct cache entry)
|
||||
*
|
||||
* @param array $cc An array containing the sections to match
|
||||
* @return array The input array with matching sections filled into the "match" key
|
||||
*/
|
||||
protected function matching(array $cc)
|
||||
{
|
||||
if (is_array($cc['sections'])) {
|
||||
/** @var \TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher $matchObj */
|
||||
$matchObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher::class);
|
||||
$matchObj->setRootline($this->rootLine);
|
||||
$matchObj->setPageId($this->id);
|
||||
foreach ($cc['sections'] as $key => $pre) {
|
||||
if ($matchObj->match($pre)) {
|
||||
$cc['match'][$key] = $pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $cc;
|
||||
}
|
||||
}
|
||||
+336
@@ -0,0 +1,336 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Controller\EditDocumentController;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Matching TypoScript conditions for backend disposal.
|
||||
*
|
||||
* Used with the TypoScript parser.
|
||||
* Matches browserinfo, IPnumbers for use with templates
|
||||
*/
|
||||
class ConditionMatcher extends AbstractConditionMatcher
|
||||
{
|
||||
/**
|
||||
* @var Context
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
public function __construct(Context $context = null)
|
||||
{
|
||||
$this->context = $context ?? GeneralUtility::makeInstance(Context::class);
|
||||
$this->rootline = $this->determineRootline() ?? [];
|
||||
$this->initializeExpressionLanguageResolver();
|
||||
}
|
||||
|
||||
protected function updateExpressionLanguageVariables(): void
|
||||
{
|
||||
$treeLevel = $this->rootline ? count($this->rootline) - 1 : 0;
|
||||
if ($this->isNewPageWithPageId($this->pageId)) {
|
||||
$treeLevel++;
|
||||
}
|
||||
$tree = new \stdClass();
|
||||
$tree->level = $treeLevel;
|
||||
$tree->rootLine = $this->rootline;
|
||||
$tree->rootLineIds = array_column($this->rootline, 'uid');
|
||||
|
||||
$backendUserAspect = $this->context->getAspect('backend.user');
|
||||
$backend = new \stdClass();
|
||||
$backend->user = new \stdClass();
|
||||
$backend->user->isAdmin = $backendUserAspect->get('isAdmin');
|
||||
$backend->user->isLoggedIn = $backendUserAspect->get('isLoggedIn');
|
||||
$backend->user->userId = $backendUserAspect->get('id');
|
||||
$backend->user->userGroupList = implode(',', $backendUserAspect->get('groupIds'));
|
||||
|
||||
$this->expressionLanguageResolverVariables = [
|
||||
'tree' => $tree,
|
||||
'backend' => $backend,
|
||||
'page' => BackendUtility::getRecord('pages', $this->pageId ?? $this->determinePageId()) ?: [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluates a TypoScript condition given as input, eg. "[browser=net][...(other conditions)...]"
|
||||
*
|
||||
* @param string $string The condition to match against its criteria.
|
||||
* @return bool Whether the condition matched
|
||||
* @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser::parse()
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function evaluateCondition($string)
|
||||
{
|
||||
if ($this->strictSyntaxEnabled()) {
|
||||
trigger_error('The old condition syntax will be removed in TYPO3 v10.0, use the new expression language. Used condition: [' . $string . '].', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
list($key, $value) = GeneralUtility::trimExplode('=', $string, false, 2);
|
||||
$result = $this->evaluateConditionCommon($key, $value);
|
||||
if (is_bool($result)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
switch ($key) {
|
||||
case 'usergroup':
|
||||
$groupList = $this->getGroupList();
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
foreach ($values as $test) {
|
||||
if ($test === '*' || GeneralUtility::inList($groupList, $test)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'adminUser':
|
||||
if ($this->isUserLoggedIn()) {
|
||||
return !((bool)$value xor $this->isAdminUser());
|
||||
}
|
||||
break;
|
||||
case 'treeLevel':
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
$treeLevel = count($this->rootline) - 1;
|
||||
// If a new page is being edited or saved the treeLevel is higher by one:
|
||||
if ($this->isNewPageWithPageId($this->pageId)) {
|
||||
$treeLevel++;
|
||||
}
|
||||
foreach ($values as $test) {
|
||||
if ($test == $treeLevel) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'PIDupinRootline':
|
||||
case 'PIDinRootline':
|
||||
$values = GeneralUtility::trimExplode(',', $value, true);
|
||||
if ($key === 'PIDinRootline' || !in_array($this->pageId, $values) || $this->isNewPageWithPageId($this->pageId)) {
|
||||
foreach ($values as $test) {
|
||||
foreach ($this->rootline as $rl_dat) {
|
||||
if ($rl_dat['uid'] == $test) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$conditionResult = $this->evaluateCustomDefinedCondition($string);
|
||||
if ($conditionResult !== null) {
|
||||
return $conditionResult;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns GP / ENV vars
|
||||
*
|
||||
* @param string $var Identifier
|
||||
* @return mixed The value of the variable pointed to or NULL if variable did not exist
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getVariable($var)
|
||||
{
|
||||
$vars = explode(':', $var, 2);
|
||||
return $this->getVariableCommon($vars);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the usergroup list of the current user.
|
||||
*
|
||||
* @return string The usergroup list of the current user
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getGroupList()
|
||||
{
|
||||
return $this->getBackendUserAuthentication()->groupList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to determine the ID of the page currently processed.
|
||||
* When User/Group TS-Config is parsed when no specific page is handled
|
||||
* (i.e. in the Extension Manager, etc.) this function will return "0", so that
|
||||
* the accordant conditions (e.g. PIDinRootline) will return "FALSE"
|
||||
*
|
||||
* @return int The determined page id or otherwise 0
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function determinePageId()
|
||||
{
|
||||
$pageId = 0;
|
||||
$editStatement = GeneralUtility::_GP('edit');
|
||||
$commandStatement = GeneralUtility::_GP('cmd');
|
||||
// Determine id from module that was called with an id:
|
||||
if ($id = (int)GeneralUtility::_GP('id')) {
|
||||
$pageId = $id;
|
||||
} elseif (is_array($editStatement)) {
|
||||
$table = key($editStatement);
|
||||
$uidAndAction = current($editStatement);
|
||||
$uid = key($uidAndAction);
|
||||
$action = current($uidAndAction);
|
||||
if ($action === 'edit') {
|
||||
$pageId = $this->getPageIdByRecord($table, $uid);
|
||||
} elseif ($action === 'new') {
|
||||
$pageId = $this->getPageIdByRecord($table, $uid, true);
|
||||
}
|
||||
} elseif (is_array($commandStatement)) {
|
||||
$table = key($commandStatement);
|
||||
$uidActionAndTarget = current($commandStatement);
|
||||
$uid = key($uidActionAndTarget);
|
||||
$actionAndTarget = current($uidActionAndTarget);
|
||||
$action = key($actionAndTarget);
|
||||
$target = current($actionAndTarget);
|
||||
if ($action === 'delete') {
|
||||
$pageId = $this->getPageIdByRecord($table, $uid);
|
||||
} elseif ($action === 'copy' || $action === 'move') {
|
||||
$pageId = $this->getPageIdByRecord($table, $target, true);
|
||||
}
|
||||
}
|
||||
return $pageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the properties for the current page.
|
||||
*
|
||||
* @return array The properties for the current page.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getPage()
|
||||
{
|
||||
$pageId = $this->pageId ?? $this->determinePageId();
|
||||
return BackendUtility::getRecord('pages', $pageId) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the page id by a record.
|
||||
*
|
||||
* @param string $table Name of the table
|
||||
* @param int $id Id of the accordant record
|
||||
* @param bool $ignoreTable Whether to ignore the page, if TRUE a positive
|
||||
* @return int Id of the page the record is persisted on
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getPageIdByRecord($table, $id, $ignoreTable = false)
|
||||
{
|
||||
$pageId = 0;
|
||||
$id = (int)$id;
|
||||
if ($table && $id) {
|
||||
if (($ignoreTable || $table === 'pages') && $id >= 0) {
|
||||
$pageId = $id;
|
||||
} else {
|
||||
$record = BackendUtility::getRecordWSOL($table, abs($id), '*', '', false);
|
||||
$pageId = $record['pid'];
|
||||
}
|
||||
}
|
||||
return $pageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if record of table 'pages' with the given $pid is currently created in TCEforms.
|
||||
* This information is required for conditions in BE for PIDupinRootline.
|
||||
*
|
||||
* @param int $pageId The pid the check for as parent page
|
||||
* @return bool TRUE if the is currently a new page record being edited with $pid as uid of the parent page
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function isNewPageWithPageId($pageId)
|
||||
{
|
||||
if (isset($GLOBALS['SOBE']) && $GLOBALS['SOBE'] instanceof EditDocumentController) {
|
||||
$pageId = (int)$pageId;
|
||||
$elementsData = $GLOBALS['SOBE']->elementsData;
|
||||
$data = $GLOBALS['SOBE']->data;
|
||||
// If saving a new page record:
|
||||
if (is_array($data) && isset($data['pages']) && is_array($data['pages'])) {
|
||||
foreach ($data['pages'] as $uid => $fields) {
|
||||
if (strpos($uid, 'NEW') === 0 && $fields['pid'] == $pageId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If editing a new page record (not saved yet):
|
||||
if (is_array($elementsData)) {
|
||||
foreach ($elementsData as $element) {
|
||||
if ($element['cmd'] === 'new' && $element['table'] === 'pages') {
|
||||
if ($element['pid'] < 0) {
|
||||
$pageRecord = BackendUtility::getRecord('pages', abs($element['pid']), 'pid');
|
||||
$element['pid'] = $pageRecord['pid'];
|
||||
}
|
||||
if ($element['pid'] == $pageId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the rootline for the current page.
|
||||
*
|
||||
* @return array The rootline for the current page.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function determineRootline()
|
||||
{
|
||||
$pageId = $this->pageId ?? $this->determinePageId();
|
||||
return BackendUtility::BEgetRootLine($pageId, '', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the id of the current user.
|
||||
*
|
||||
* @return int The id of the current user
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getUserId()
|
||||
{
|
||||
return $this->getBackendUserAuthentication()->user['uid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a user is logged in.
|
||||
*
|
||||
* @return bool Determines if a user is logged in
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function isUserLoggedIn()
|
||||
{
|
||||
return (bool)$this->getBackendUserAuthentication()->user['uid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the current user is admin.
|
||||
*
|
||||
* @return bool Whether the current user is admin
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function isAdminUser()
|
||||
{
|
||||
return $this->getBackendUserAuthentication()->isAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\ContextMenu;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class for generating the click menu
|
||||
* @internal
|
||||
*/
|
||||
class ContextMenu
|
||||
{
|
||||
/**
|
||||
* Click menu item providers shipped with EXT:backend
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $itemProviders = [
|
||||
ItemProviders\PageProvider::class,
|
||||
ItemProviders\RecordProvider::class
|
||||
];
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
* @param string $identifier
|
||||
* @param string $context
|
||||
* @return array
|
||||
*/
|
||||
public function getItems(string $table, string $identifier, string $context = ''): array
|
||||
{
|
||||
$items = [];
|
||||
$itemsProviders = $this->getAvailableProviders($table, $identifier, $context);
|
||||
|
||||
/** @var \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface $provider */
|
||||
foreach ($itemsProviders as $provider) {
|
||||
$items = $provider->addItems($items);
|
||||
}
|
||||
return $this->cleanItems($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $table
|
||||
* @param string $identifier
|
||||
* @param string $context
|
||||
* @return array of \TYPO3\CMS\Backend\ContextMenu\ItemProviders\ProviderInterface
|
||||
*/
|
||||
protected function getAvailableProviders(string $table, string $identifier, string $context): array
|
||||
{
|
||||
$providers = array_merge($this->itemProviders, $GLOBALS['TYPO3_CONF_VARS']['BE']['ContextMenu']['ItemProviders'] ?? []);
|
||||
$availableProviders = [];
|
||||
foreach ($providers as $providerClass) {
|
||||
$provider = GeneralUtility::makeInstance($providerClass, $table, $identifier, $context);
|
||||
if ($provider->canHandle()) {
|
||||
$priority = $provider->getPriority();
|
||||
$availableProviders[$priority] = $provider;
|
||||
}
|
||||
}
|
||||
krsort($availableProviders);
|
||||
return $availableProviders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up double dividers.
|
||||
* Don't render menu when there are no item or submenu.
|
||||
*
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
protected function cleanItems(array $items): array
|
||||
{
|
||||
$canRender = false;
|
||||
$prevItemWasDivider = false;
|
||||
foreach ($items as $key => $item) {
|
||||
if ($item['type'] === 'item') {
|
||||
$canRender = true;
|
||||
$prevItemWasDivider = false;
|
||||
continue;
|
||||
}
|
||||
if ($item['type'] === 'divider') {
|
||||
if ($prevItemWasDivider === true) {
|
||||
unset($items[$key]);
|
||||
} else {
|
||||
$prevItemWasDivider = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($item['type'] === 'submenu') {
|
||||
$childItems = $this->cleanItems($item['childItems']);
|
||||
if (empty($childItems)) {
|
||||
unset($items[$key]);
|
||||
} else {
|
||||
$items[$key]['childItems'] = $childItems;
|
||||
$canRender = true;
|
||||
$prevItemWasDivider = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
//Remove first and last divider
|
||||
$fistItem = reset($items);
|
||||
if ($fistItem['type'] === 'divider') {
|
||||
$key = key($items);
|
||||
unset($items[$key]);
|
||||
}
|
||||
$lastItem = end($items);
|
||||
if ($lastItem['type'] === 'divider') {
|
||||
$key = key($items);
|
||||
unset($items[$key]);
|
||||
}
|
||||
//no menu when there are no item or submenu
|
||||
if (!$canRender) {
|
||||
$items = [];
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Clipboard\Clipboard;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Abstract provider is a base class for context menu item providers
|
||||
*/
|
||||
class AbstractProvider implements ProviderInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Language Service property. Used to access localized labels
|
||||
*
|
||||
* @var LanguageService
|
||||
*/
|
||||
protected $languageService;
|
||||
|
||||
/**
|
||||
* @var BackendUserAuthentication
|
||||
*/
|
||||
protected $backendUser;
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Backend\Clipboard\Clipboard
|
||||
*/
|
||||
protected $clipboard;
|
||||
|
||||
/**
|
||||
* Array of items the class is providing
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $itemsConfiguration = [];
|
||||
|
||||
/**
|
||||
* Click menu items disabled by TSConfig
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $disabledItems = [];
|
||||
|
||||
/**
|
||||
* Current table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = '';
|
||||
|
||||
/**
|
||||
* @var string clicked record identifier (usually uid or file combined identifier)
|
||||
*/
|
||||
protected $identifier = '';
|
||||
|
||||
/**
|
||||
* Context - from where the click menu was triggered (e.g. 'tree')
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $context = '';
|
||||
|
||||
/**
|
||||
* Lightweight constructor, just to be able to call ->canHandle(). Rest of the initialization is done
|
||||
* in the initialize() method
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $identifier
|
||||
* @param string $context
|
||||
*/
|
||||
public function __construct(string $table, string $identifier, string $context = '')
|
||||
{
|
||||
$this->table = $table;
|
||||
$this->identifier = $identifier;
|
||||
$this->context = $context;
|
||||
$this->languageService = $GLOBALS['LANG'];
|
||||
$this->backendUser = $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider initialization, heavy stuff
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
$this->initClipboard();
|
||||
$this->initDisabledItems();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider priority which is used for determining the order in which providers are adding items
|
||||
* to the result array. Highest priority means provider is evaluated first.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this provider can handle given request (usually a check based on table, uid and context)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canHandle(): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize clipboard object - necessary for all copy/cut/paste operations
|
||||
*/
|
||||
protected function initClipboard()
|
||||
{
|
||||
$clipboard = GeneralUtility::makeInstance(Clipboard::class);
|
||||
$clipboard->initializeClipboard();
|
||||
// This locks the clipboard to the Normal for this request.
|
||||
$clipboard->lockToNormal();
|
||||
$this->clipboard = $clipboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills $this->disabledItems with the values from TSConfig.
|
||||
* Disabled items can be set separately for each context.
|
||||
*/
|
||||
protected function initDisabledItems()
|
||||
{
|
||||
if ($this->context) {
|
||||
$tsConfigValue = $this->backendUser->getTSConfig()['options.']['contextMenu.']['table.'][$this->table . '.'][$this->context . '.']['disableItems'] ?? '';
|
||||
} else {
|
||||
$tsConfigValue = $this->backendUser->getTSConfig()['options.']['contextMenu.']['table.'][$this->table . '.']['disableItems'] ?? '';
|
||||
}
|
||||
$this->disabledItems = GeneralUtility::trimExplode(',', $tsConfigValue, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new items to the given array or modifies existing items
|
||||
*
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function addItems(array $items): array
|
||||
{
|
||||
$this->initialize();
|
||||
$items += $this->prepareItems($this->itemsConfiguration);
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts item configuration (from $this->itemsConfiguration) into an array ready for returning by controller
|
||||
*
|
||||
* @param array $itemsConfiguration
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareItems(array $itemsConfiguration): array
|
||||
{
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$items = [];
|
||||
foreach ($itemsConfiguration as $name => $configuration) {
|
||||
$type = !empty($configuration['type']) ? $configuration['type'] : 'item';
|
||||
if ($this->canRender($name, $type)) {
|
||||
$items[$name] = [
|
||||
'type' => $type,
|
||||
'label' => !empty($configuration['label']) ? htmlspecialchars($this->languageService->sL($configuration['label'])) : '',
|
||||
'icon' => !empty($configuration['iconIdentifier']) ? $iconFactory->getIcon($configuration['iconIdentifier'], Icon::SIZE_SMALL)->render() : '',
|
||||
'additionalAttributes' => $this->getAdditionalAttributes($name),
|
||||
'callbackAction' => !empty($configuration['callbackAction']) ? $configuration['callbackAction'] : ''
|
||||
];
|
||||
if ($type === 'submenu') {
|
||||
$items[$name]['childItems'] = $this->prepareItems($configuration['childItems']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of additional attributes for given item. Additional attributes are used to pass item specific data
|
||||
* to the JS. E.g. message for the delete confirmation dialog
|
||||
*
|
||||
* @param string $itemName
|
||||
* @return array
|
||||
*/
|
||||
protected function getAdditionalAttributes(string $itemName): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether certain item can be rendered (e.g. check for disabled items or permissions)
|
||||
*
|
||||
* @param string $itemName
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function canRender(string $itemName, string $type): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a clicked record identifier
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getIdentifier(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,565 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Context menu item provider for pages table
|
||||
*/
|
||||
class PageProvider extends RecordProvider
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'pages';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $itemsConfiguration = [
|
||||
'view' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view',
|
||||
'iconIdentifier' => 'actions-view-page',
|
||||
'callbackAction' => 'viewRecord'
|
||||
],
|
||||
'edit' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit',
|
||||
'iconIdentifier' => 'actions-page-open',
|
||||
'callbackAction' => 'editRecord'
|
||||
],
|
||||
'new' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.new',
|
||||
'iconIdentifier' => 'actions-page-new',
|
||||
'callbackAction' => 'newRecord'
|
||||
],
|
||||
'info' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info',
|
||||
'iconIdentifier' => 'actions-document-info',
|
||||
'callbackAction' => 'openInfoPopUp'
|
||||
],
|
||||
'divider1' => [
|
||||
'type' => 'divider'
|
||||
],
|
||||
'copy' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy',
|
||||
'callbackAction' => 'copy'
|
||||
],
|
||||
'copyRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy-release',
|
||||
'callbackAction' => 'clipboardRelease'
|
||||
],
|
||||
'cut' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut',
|
||||
'iconIdentifier' => 'actions-edit-cut',
|
||||
'callbackAction' => 'cut'
|
||||
],
|
||||
'cutRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease',
|
||||
'iconIdentifier' => 'actions-edit-cut-release',
|
||||
'callbackAction' => 'clipboardRelease'
|
||||
],
|
||||
'pasteAfter' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
|
||||
'iconIdentifier' => 'actions-document-paste-after',
|
||||
'callbackAction' => 'pasteAfter'
|
||||
],
|
||||
'pasteInto' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteinto',
|
||||
'iconIdentifier' => 'actions-document-paste-into',
|
||||
'callbackAction' => 'pasteInto'
|
||||
],
|
||||
'divider2' => [
|
||||
'type' => 'divider'
|
||||
],
|
||||
'more' => [
|
||||
'type' => 'submenu',
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more',
|
||||
'iconIdentifier' => '',
|
||||
'callbackAction' => 'openSubmenu',
|
||||
'childItems' => [
|
||||
'newWizard' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_newWizard',
|
||||
'iconIdentifier' => 'actions-page-new',
|
||||
'callbackAction' => 'newPageWizard',
|
||||
],
|
||||
'pagesSort' => [
|
||||
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_sort.xlf:title',
|
||||
'iconIdentifier' => 'actions-page-move',
|
||||
'callbackAction' => 'pagesSort',
|
||||
],
|
||||
'pagesNewMultiple' => [
|
||||
'label' => 'LLL:EXT:backend/Resources/Private/Language/locallang_pages_new.xlf:title',
|
||||
'iconIdentifier' => 'apps-pagetree-drag-move-between',
|
||||
'callbackAction' => 'pagesNewMultiple',
|
||||
],
|
||||
'openListModule' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
|
||||
'iconIdentifier' => 'actions-system-list-open',
|
||||
'callbackAction' => 'openListModule',
|
||||
],
|
||||
'mountAsTreeRoot' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.tempMountPoint',
|
||||
'iconIdentifier' => 'actions-pagetree-mountroot',
|
||||
'callbackAction' => 'mountAsTreeRoot',
|
||||
],
|
||||
],
|
||||
],
|
||||
'divider3' => [
|
||||
'type' => 'divider'
|
||||
],
|
||||
'enable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable',
|
||||
'iconIdentifier' => 'actions-edit-unhide',
|
||||
'callbackAction' => 'enableRecord',
|
||||
],
|
||||
'disable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable',
|
||||
'iconIdentifier' => 'actions-edit-hide',
|
||||
'callbackAction' => 'disableRecord',
|
||||
],
|
||||
'delete' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete',
|
||||
'iconIdentifier' => 'actions-edit-delete',
|
||||
'callbackAction' => 'deleteRecord',
|
||||
],
|
||||
'history' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history',
|
||||
'iconIdentifier' => 'actions-document-history-open',
|
||||
'callbackAction' => 'openHistoryPopUp',
|
||||
],
|
||||
'clearCache' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.clear_cache',
|
||||
'iconIdentifier' => 'actions-system-cache-clear',
|
||||
'callbackAction' => 'clearCache',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $languageAccess = false;
|
||||
|
||||
/**
|
||||
* Checks if the provider can add items to the menu
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canHandle(): bool
|
||||
{
|
||||
return $this->table === 'pages';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $itemName
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function canRender(string $itemName, string $type): bool
|
||||
{
|
||||
if (in_array($type, ['divider', 'submenu'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($itemName, $this->disabledItems, true)) {
|
||||
return false;
|
||||
}
|
||||
$canRender = false;
|
||||
switch ($itemName) {
|
||||
case 'view':
|
||||
$canRender = $this->canBeViewed();
|
||||
break;
|
||||
case 'edit':
|
||||
$canRender = $this->canBeEdited();
|
||||
break;
|
||||
case 'new':
|
||||
case 'newWizard':
|
||||
case 'pagesNewMultiple':
|
||||
$canRender = $this->canBeCreated();
|
||||
break;
|
||||
case 'info':
|
||||
$canRender = $this->canShowInfo();
|
||||
break;
|
||||
case 'enable':
|
||||
$canRender = $this->canBeEnabled();
|
||||
break;
|
||||
case 'disable':
|
||||
$canRender = $this->canBeDisabled();
|
||||
break;
|
||||
case 'delete':
|
||||
$canRender = $this->canBeDeleted();
|
||||
break;
|
||||
case 'history':
|
||||
$canRender = $this->canShowHistory();
|
||||
break;
|
||||
case 'openListModule':
|
||||
$canRender = $this->canOpenListModule();
|
||||
break;
|
||||
case 'pagesSort':
|
||||
$canRender = $this->canBeSorted();
|
||||
break;
|
||||
case 'mountAsTreeRoot':
|
||||
$canRender = !$this->isRoot();
|
||||
break;
|
||||
case 'copy':
|
||||
$canRender = $this->canBeCopied();
|
||||
break;
|
||||
case 'copyRelease':
|
||||
$canRender = $this->isRecordInClipboard('copy');
|
||||
break;
|
||||
case 'cut':
|
||||
$canRender = $this->canBeCut() && !$this->isRecordInClipboard('cut');
|
||||
break;
|
||||
case 'cutRelease':
|
||||
$canRender = $this->isRecordInClipboard('cut');
|
||||
break;
|
||||
case 'pasteAfter':
|
||||
$canRender = $this->canBePastedAfter();
|
||||
break;
|
||||
case 'pasteInto':
|
||||
$canRender = $this->canBePastedInto();
|
||||
break;
|
||||
case 'clearCache':
|
||||
$canRender = $this->canClearCache();
|
||||
break;
|
||||
}
|
||||
return $canRender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves calculated permissions for a page to speed things up
|
||||
*/
|
||||
protected function initPermissions()
|
||||
{
|
||||
$this->pagePermissions = $this->backendUser->calcPerms($this->record);
|
||||
$this->languageAccess = $this->hasLanguageAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user may create pages below the given page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeCreated(): bool
|
||||
{
|
||||
if (!$this->backendUser->checkLanguageAccess(0)) {
|
||||
return false;
|
||||
}
|
||||
return $this->hasPagePermission(Permission::PAGE_NEW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has editing rights
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeEdited(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
if ($this->isRoot()) {
|
||||
return false;
|
||||
}
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) {
|
||||
return false;
|
||||
}
|
||||
if ($this->backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isRecordLocked() && $this->hasPagePermission(Permission::PAGE_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is locked
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isRecordLocked(): bool
|
||||
{
|
||||
return (bool)$this->record['editlock'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to can be cut
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeCut(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField'])
|
||||
&& !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isWebMount()
|
||||
&& $this->canBeEdited()
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to be copied
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeCopied(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['languageField'])
|
||||
&& !in_array($this->record[$GLOBALS['TCA'][$this->table]['ctrl']['languageField']], [0, -1])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isRoot()
|
||||
&& !$this->isWebMount()
|
||||
&& !$this->isRecordInClipboard('copy')
|
||||
&& $this->hasPagePermission(Permission::PAGE_SHOW)
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if something can be pasted into the node
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBePastedInto(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
$clipboardElementCount = count($this->clipboard->elFromTable($this->table));
|
||||
|
||||
return $clipboardElementCount
|
||||
&& $this->canBeCreated()
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if something can be pasted after the node
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBePastedAfter(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
$clipboardElementCount = count($this->clipboard->elFromTable($this->table));
|
||||
return $clipboardElementCount
|
||||
&& $this->canBeCreated()
|
||||
&& !$this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if sub pages of given page can be sorted
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeSorted(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
return $this->backendUser->check('tables_modify', $this->table)
|
||||
&& $this->hasPagePermission(Permission::CONTENT_EDIT)
|
||||
&& !$this->isDeletePlaceholder()
|
||||
&& $this->backendUser->workspace === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to be removed
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeDeleted(): bool
|
||||
{
|
||||
if (!$this->languageAccess) {
|
||||
return false;
|
||||
}
|
||||
return !$this->isDeletePlaceholder()
|
||||
&& !$this->isRecordLocked()
|
||||
&& !$this->isDeletionDisabledInTS()
|
||||
&& $this->hasPagePermission(Permission::PAGE_DELETE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to be viewed in frontend
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeViewed(): bool
|
||||
{
|
||||
return !$this->isRoot() && !$this->isDeleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to show info
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canShowInfo(): bool
|
||||
{
|
||||
return !$this->isRoot();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has clear cache rights
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canClearCache(): bool
|
||||
{
|
||||
return !$this->isRoot()
|
||||
&& ($this->backendUser->isAdmin() || $this->backendUser->getTSConfig()['options.']['clearCache.']['pages'] ?? false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether this node is deleted.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDeleted(): bool
|
||||
{
|
||||
return !empty($this->record['deleted']) || $this->isDeletePlaceholder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record is a root page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isRoot()
|
||||
{
|
||||
return (int)$this->identifier === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record is a web mount
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isWebMount()
|
||||
{
|
||||
return in_array($this->identifier, $this->backendUser->returnWebmounts());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $itemName
|
||||
* @return array
|
||||
*/
|
||||
protected function getAdditionalAttributes(string $itemName): array
|
||||
{
|
||||
$attributes = [];
|
||||
if ($itemName === 'view') {
|
||||
$attributes += $this->getViewAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'enable' || $itemName === 'disable') {
|
||||
$attributes += $this->getEnableDisableAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'delete') {
|
||||
$attributes += $this->getDeleteAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'pasteInto') {
|
||||
$attributes += $this->getPasteAdditionalAttributes('into');
|
||||
}
|
||||
if ($itemName === 'pasteAfter') {
|
||||
$attributes += $this->getPasteAdditionalAttributes('after');
|
||||
}
|
||||
if ($itemName === 'pagesSort') {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$attributes += [
|
||||
'data-pages-sort-url' => (string)$uriBuilder->buildUriFromRoute('pages_sort', ['id' => $this->record['uid']]),
|
||||
];
|
||||
}
|
||||
if ($itemName === 'pagesNewMultiple') {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$attributes += [
|
||||
'data-pages-new-multiple-url' => (string)$uriBuilder->buildUriFromRoute('pages_new', ['id' => $this->record['uid']]),
|
||||
];
|
||||
}
|
||||
if ($itemName === 'edit') {
|
||||
$attributes = [
|
||||
'data-pages-language-uid' => $this->record['sys_language_uid']
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
protected function getPreviewPid(): int
|
||||
{
|
||||
return (int)$this->record['sys_language_uid'] === 0 ? (int)$this->record['uid'] : (int)$this->record['l10n_parent'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view link
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getViewLink(): string
|
||||
{
|
||||
$language = (int)$this->record['sys_language_uid'];
|
||||
$additionalParams = ($language > 0) ? '&L=' . $language : '';
|
||||
return BackendUtility::getPreviewUrl(
|
||||
$this->getPreviewPid(),
|
||||
'',
|
||||
null,
|
||||
'',
|
||||
'',
|
||||
$additionalParams
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a current user has access to the language of the record
|
||||
*
|
||||
* @see BackendUserAuthentication::checkLanguageAccess()
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasLanguageAccess(): bool
|
||||
{
|
||||
if ($this->backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
$languageField = $GLOBALS['TCA'][$this->table]['ctrl']['languageField'] ?? '';
|
||||
if ($languageField !== '' && isset($this->record[$languageField])) {
|
||||
return $this->backendUser->checkLanguageAccess((int)$this->record[$languageField]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Interface for context menu items providers
|
||||
*/
|
||||
interface ProviderInterface
|
||||
{
|
||||
/**
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function addItems(array $items): array;
|
||||
|
||||
/**
|
||||
* Returns the priority of the provider. Higher priority value means provider is executed first
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority(): int;
|
||||
|
||||
/**
|
||||
* Checks if the provider can add items to the menu
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canHandle(): bool;
|
||||
}
|
||||
@@ -0,0 +1,696 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\ContextMenu\ItemProviders;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* Class responsible for providing click menu items for db records which don't have custom provider (as e.g. pages)
|
||||
*/
|
||||
class RecordProvider extends AbstractProvider
|
||||
{
|
||||
/**
|
||||
* Database record
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $record = [];
|
||||
|
||||
/**
|
||||
* Database record of the page $this->record is placed on
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pageRecord = [];
|
||||
|
||||
/**
|
||||
* Local cache for the result of BackendUserAuthentication::calcPerms()
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $pagePermissions = 0;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $itemsConfiguration = [
|
||||
'view' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.view',
|
||||
'iconIdentifier' => 'actions-view',
|
||||
'callbackAction' => 'viewRecord'
|
||||
],
|
||||
'edit' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.edit',
|
||||
'iconIdentifier' => 'actions-open',
|
||||
'callbackAction' => 'editRecord'
|
||||
],
|
||||
'new' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.new',
|
||||
'iconIdentifier' => 'actions-add',
|
||||
'callbackAction' => 'newRecord'
|
||||
],
|
||||
'info' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.info',
|
||||
'iconIdentifier' => 'actions-document-info',
|
||||
'callbackAction' => 'openInfoPopUp'
|
||||
],
|
||||
'divider1' => [
|
||||
'type' => 'divider'
|
||||
],
|
||||
'copy' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy',
|
||||
'callbackAction' => 'copy'
|
||||
],
|
||||
'copyRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.copy',
|
||||
'iconIdentifier' => 'actions-edit-copy-release',
|
||||
'callbackAction' => 'clipboardRelease'
|
||||
],
|
||||
'cut' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cut',
|
||||
'iconIdentifier' => 'actions-edit-cut',
|
||||
'callbackAction' => 'cut'
|
||||
],
|
||||
'cutRelease' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.cutrelease',
|
||||
'iconIdentifier' => 'actions-edit-cut-release',
|
||||
'callbackAction' => 'clipboardRelease'
|
||||
],
|
||||
'pasteAfter' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.pasteafter',
|
||||
'iconIdentifier' => 'actions-document-paste-after',
|
||||
'callbackAction' => 'pasteAfter'
|
||||
],
|
||||
'divider2' => [
|
||||
'type' => 'divider'
|
||||
],
|
||||
'more' => [
|
||||
'type' => 'submenu',
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.more',
|
||||
'iconIdentifier' => '',
|
||||
'callbackAction' => 'openSubmenu',
|
||||
'childItems' => [
|
||||
'newWizard' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_newWizard',
|
||||
'iconIdentifier' => 'actions-add',
|
||||
'callbackAction' => 'newContentWizard',
|
||||
],
|
||||
'openListModule' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_db_list',
|
||||
'iconIdentifier' => 'actions-system-list-open',
|
||||
'callbackAction' => 'openListModule',
|
||||
],
|
||||
],
|
||||
],
|
||||
'divider3' => [
|
||||
'type' => 'divider'
|
||||
],
|
||||
'enable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:enable',
|
||||
'iconIdentifier' => 'actions-edit-unhide',
|
||||
'callbackAction' => 'enableRecord',
|
||||
],
|
||||
'disable' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable',
|
||||
'iconIdentifier' => 'actions-edit-hide',
|
||||
'callbackAction' => 'disableRecord',
|
||||
],
|
||||
'delete' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.delete',
|
||||
'iconIdentifier' => 'actions-edit-delete',
|
||||
'callbackAction' => 'deleteRecord',
|
||||
],
|
||||
'history' => [
|
||||
'label' => 'LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:CM_history',
|
||||
'iconIdentifier' => 'actions-document-history-open',
|
||||
'callbackAction' => 'openHistoryPopUp',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Whether this provider should kick in
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canHandle(): bool
|
||||
{
|
||||
if (in_array($this->table, ['sys_file', 'sys_filemounts', 'sys_file_storage', 'pages'], true)
|
||||
|| strpos($this->table, '-drag') !== false) {
|
||||
return false;
|
||||
}
|
||||
return isset($GLOBALS['TCA'][$this->table]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize db record
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
$this->record = BackendUtility::getRecordWSOL($this->table, $this->identifier);
|
||||
$this->initPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority is set to lower then default value, in order to skip this provider if there is less generic provider available.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getPriority(): int
|
||||
{
|
||||
return 60;
|
||||
}
|
||||
|
||||
/**
|
||||
* This provider works as a fallback if there is no provider dedicated for certain table, thus it's only kicking in when $items are empty.
|
||||
*
|
||||
* @param array $items
|
||||
* @return array
|
||||
*/
|
||||
public function addItems(array $items): array
|
||||
{
|
||||
if (!empty($items)) {
|
||||
return $items;
|
||||
}
|
||||
$this->initialize();
|
||||
return $this->prepareItems($this->itemsConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a given item can be rendered (e.g. user has enough permissions)
|
||||
*
|
||||
* @param string $itemName
|
||||
* @param string $type
|
||||
* @return bool
|
||||
*/
|
||||
protected function canRender(string $itemName, string $type): bool
|
||||
{
|
||||
if (in_array($type, ['divider', 'submenu'], true)) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($itemName, $this->disabledItems, true)) {
|
||||
return false;
|
||||
}
|
||||
$canRender = false;
|
||||
switch ($itemName) {
|
||||
case 'view':
|
||||
$canRender = $this->canBeViewed();
|
||||
break;
|
||||
case 'edit':
|
||||
case 'new':
|
||||
$canRender = $this->canBeEdited();
|
||||
break;
|
||||
case 'newWizard':
|
||||
$canRender = $this->canOpenNewCEWizard();
|
||||
break;
|
||||
case 'info':
|
||||
$canRender = $this->canShowInfo();
|
||||
break;
|
||||
case 'enable':
|
||||
$canRender = $this->canBeEnabled();
|
||||
break;
|
||||
case 'disable':
|
||||
$canRender = $this->canBeDisabled();
|
||||
break;
|
||||
case 'delete':
|
||||
$canRender = $this->canBeDeleted();
|
||||
break;
|
||||
case 'history':
|
||||
$canRender = $this->canShowHistory();
|
||||
break;
|
||||
case 'openListModule':
|
||||
$canRender = $this->canOpenListModule();
|
||||
break;
|
||||
case 'copy':
|
||||
$canRender = $this->canBeCopied();
|
||||
break;
|
||||
case 'copyRelease':
|
||||
$canRender = $this->isRecordInClipboard('copy');
|
||||
break;
|
||||
case 'cut':
|
||||
$canRender = $this->canBeCut();
|
||||
break;
|
||||
case 'cutRelease':
|
||||
$canRender = $this->isRecordInClipboard('cut');
|
||||
break;
|
||||
case 'pasteAfter':
|
||||
$canRender = $this->canBePastedAfter();
|
||||
break;
|
||||
}
|
||||
return $canRender;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves calculated permissions for a page containing given record, to speed things up
|
||||
*/
|
||||
protected function initPermissions()
|
||||
{
|
||||
$this->pageRecord = BackendUtility::getRecord('pages', $this->record['pid']);
|
||||
$this->pagePermissions = $this->backendUser->calcPerms($this->pageRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a current user have access to given permission
|
||||
*
|
||||
* @see BackendUserAuthentication::doesUserHaveAccess()
|
||||
* @param int $permission
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasPagePermission(int $permission): bool
|
||||
{
|
||||
return $this->backendUser->isAdmin() || ($this->pagePermissions & $permission) == $permission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for JS
|
||||
*
|
||||
* @param string $itemName
|
||||
* @return array
|
||||
*/
|
||||
protected function getAdditionalAttributes(string $itemName): array
|
||||
{
|
||||
$attributes = [];
|
||||
if ($itemName === 'view') {
|
||||
$attributes += $this->getViewAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'enable' || $itemName === 'disable') {
|
||||
$attributes += $this->getEnableDisableAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'newWizard' && $this->table === 'tt_content') {
|
||||
$moduleName = BackendUtility::getPagesTSconfig($this->record['pid'])['mod.']['newContentElementWizard.']['override']
|
||||
?? 'new_content_element_wizard';
|
||||
$urlParameters = [
|
||||
'id' => $this->record['pid'],
|
||||
'sys_language_uid' => $this->record['sys_language_uid'],
|
||||
'colPos' => $this->record['colPos'],
|
||||
'uid_pid' => -$this->record['uid']
|
||||
];
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$url = (string)$uriBuilder->buildUriFromRoute($moduleName, $urlParameters);
|
||||
$attributes += [
|
||||
'data-new-wizard-url' => htmlspecialchars($url),
|
||||
'data-title' => $this->languageService->getLL('newContentElement'),
|
||||
];
|
||||
}
|
||||
if ($itemName === 'delete') {
|
||||
$attributes += $this->getDeleteAdditionalAttributes();
|
||||
}
|
||||
if ($itemName === 'openListModule') {
|
||||
$attributes += [
|
||||
'data-page-uid' => $this->record['pid']
|
||||
];
|
||||
}
|
||||
if ($itemName === 'pasteAfter') {
|
||||
$attributes += $this->getPasteAdditionalAttributes('after');
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for the 'view' item
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getViewAdditionalAttributes(): array
|
||||
{
|
||||
$attributes = [];
|
||||
$viewLink = $this->getViewLink();
|
||||
if ($viewLink) {
|
||||
$attributes += [
|
||||
'data-preview-url' => htmlspecialchars($viewLink),
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for the hide & unhide items
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getEnableDisableAdditionalAttributes(): array
|
||||
{
|
||||
return [
|
||||
'data-disable-field' => $GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'] ?? ''
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional attributes for the pasteInto and pasteAfter items
|
||||
*
|
||||
* @param string $type "after" or "into"
|
||||
* @return array
|
||||
*/
|
||||
protected function getPasteAdditionalAttributes(string $type): array
|
||||
{
|
||||
$closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel');
|
||||
$okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:ok');
|
||||
$attributes = [];
|
||||
if ($this->backendUser->jsConfirmation(JsConfirmation::COPY_MOVE_PASTE)) {
|
||||
$selItem = $this->clipboard->getSelectedRecord();
|
||||
$title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:clip_paste');
|
||||
|
||||
$confirmMessage = sprintf(
|
||||
$this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.'
|
||||
. ($this->clipboard->currentMode() === 'copy' ? 'copy' : 'move') . '_' . $type),
|
||||
GeneralUtility::fixed_lgd_cs($selItem['_RECORD_TITLE'], $this->backendUser->uc['titleLen']),
|
||||
GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($this->table, $this->record), $this->backendUser->uc['titleLen'])
|
||||
);
|
||||
$attributes += [
|
||||
'data-title' => htmlspecialchars($title),
|
||||
'data-message' => htmlspecialchars($confirmMessage),
|
||||
'data-button-close-text' => htmlspecialchars($closeText),
|
||||
'data-button-ok-text' => htmlspecialchars($okText),
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional data for a "delete" action (confirmation modal title and message)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDeleteAdditionalAttributes(): array
|
||||
{
|
||||
$closeText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel');
|
||||
$okText = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete');
|
||||
$attributes = [];
|
||||
if ($this->backendUser->jsConfirmation(JsConfirmation::DELETE)) {
|
||||
$recordTitle = GeneralUtility::fixed_lgd_cs(BackendUtility::getRecordTitle($this->table, $this->record), $this->backendUser->uc['titleLen']);
|
||||
|
||||
$title = $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete');
|
||||
$confirmMessage = sprintf(
|
||||
$this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.delete'),
|
||||
$recordTitle
|
||||
);
|
||||
$confirmMessage .= BackendUtility::referenceCount(
|
||||
$this->table,
|
||||
$this->record['uid'],
|
||||
' ' . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.referencesToRecord')
|
||||
);
|
||||
$confirmMessage .= BackendUtility::translationCount(
|
||||
$this->table,
|
||||
$this->record['uid'],
|
||||
' ' . $this->languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.translationsOfRecord')
|
||||
);
|
||||
$attributes += [
|
||||
'data-title' => htmlspecialchars($title),
|
||||
'data-message' => htmlspecialchars($confirmMessage),
|
||||
'data-button-close-text' => htmlspecialchars($closeText),
|
||||
'data-button-ok-text' => htmlspecialchars($okText),
|
||||
];
|
||||
}
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns id of the Page used for preview
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getPreviewPid(): int
|
||||
{
|
||||
return (int)$this->record['pid'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view link
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getViewLink(): string
|
||||
{
|
||||
$anchorSection = '';
|
||||
$additionalParams = '';
|
||||
if ($this->table === 'tt_content') {
|
||||
$anchorSection = '#c' . $this->record['uid'];
|
||||
$language = (int)$this->record[$GLOBALS['TCA']['tt_content']['ctrl']['languageField']];
|
||||
if ($language > 0) {
|
||||
$additionalParams = '&L=' . $language;
|
||||
}
|
||||
}
|
||||
|
||||
return BackendUtility::getPreviewUrl(
|
||||
$this->getPreviewPid(),
|
||||
'',
|
||||
null,
|
||||
$anchorSection,
|
||||
'',
|
||||
$additionalParams
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to show info
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canShowInfo(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the page is allowed to show info
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canShowHistory(): bool
|
||||
{
|
||||
$userTsConfig = $this->backendUser->getTSConfig();
|
||||
return (bool)trim($userTsConfig['options.']['showHistory.'][$this->table] ?? $userTsConfig['options.']['showHistory'] ?? '1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the record can be previewed in frontend
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeViewed(): bool
|
||||
{
|
||||
return $this->table === 'tt_content';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a record can be edited
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeEdited(): bool
|
||||
{
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['readOnly']) {
|
||||
return false;
|
||||
}
|
||||
if ($this->backendUser->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) && $GLOBALS['TCA'][$this->table]['ctrl']['adminOnly']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$access = !$this->isRecordLocked()
|
||||
&& $this->backendUser->check('tables_modify', $this->table)
|
||||
&& $this->hasPagePermission(Permission::CONTENT_EDIT)
|
||||
&& $this->backendUser->recordEditAccessInternals($this->table, $this->record);
|
||||
return $access;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if disableDelete flag is set in TSConfig for the current table
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDeletionDisabledInTS(): bool
|
||||
{
|
||||
return (bool)\trim(
|
||||
$this->backendUser->getTSConfig()['options.']['disableDelete.'][$this->table]
|
||||
?? $this->backendUser->getTSConfig()['options.']['disableDelete']
|
||||
?? ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has the right to delete the page
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeDeleted(): bool
|
||||
{
|
||||
return !$this->isDeletionDisabledInTS() && $this->canBeEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record can be unhidden/enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeEnabled(): bool
|
||||
{
|
||||
return $this->hasDisableColumnWithValue(1) && $this->canBeEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if current record can be hidden
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeDisabled(): bool
|
||||
{
|
||||
return $this->hasDisableColumnWithValue(0) && $this->canBeEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true new content element wizard can be shown
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canOpenNewCEWizard(): bool
|
||||
{
|
||||
return $this->table === 'tt_content'
|
||||
&& (bool)(BackendUtility::getPagesTSconfig($this->record['pid'])['mod.']['web_layout.']['disableNewContentElementWizard'] ?? true)
|
||||
&& $this->canBeEdited();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function canOpenListModule(): bool
|
||||
{
|
||||
return $this->backendUser->check('modules', 'web_list');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeCopied(): bool
|
||||
{
|
||||
return !$this->isRecordInClipboard('copy')
|
||||
&& !$this->isRecordATranslation();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeCut(): bool
|
||||
{
|
||||
return !$this->isRecordInClipboard('cut')
|
||||
&& $this->canBeEdited()
|
||||
&& !$this->isRecordATranslation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Paste after is only shown for records from the same table (comparing record in clipboard and record clicked)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBePastedAfter(): bool
|
||||
{
|
||||
$clipboardElementCount = count($this->clipboard->elFromTable($this->table));
|
||||
|
||||
return $clipboardElementCount
|
||||
&& $this->backendUser->check('tables_modify', $this->table)
|
||||
&& $this->hasPagePermission(Permission::CONTENT_EDIT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if table have "disable" column (e.g. "hidden"), if user has access to this column
|
||||
* and if it contains given value
|
||||
*
|
||||
* @param int $value
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasDisableColumnWithValue(int $value): bool
|
||||
{
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'])) {
|
||||
$hiddenFieldName = $GLOBALS['TCA'][$this->table]['ctrl']['enablecolumns']['disabled'];
|
||||
if (
|
||||
$hiddenFieldName !== '' && !empty($GLOBALS['TCA'][$this->table]['columns'][$hiddenFieldName])
|
||||
&& (
|
||||
empty($GLOBALS['TCA'][$this->table]['columns'][$hiddenFieldName]['exclude'])
|
||||
|| $this->backendUser->check('non_exclude_fields', $this->table . ':' . $hiddenFieldName)
|
||||
)
|
||||
) {
|
||||
return (int)$this->record[$hiddenFieldName] === (int)$value;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Record is locked if page is locked or page is not locked but record is
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isRecordLocked(): bool
|
||||
{
|
||||
return (int)$this->pageRecord['editlock'] === 1
|
||||
|| isset($GLOBALS['TCA'][$this->table]['ctrl']['editlock'])
|
||||
&& (int)$this->record[$GLOBALS['TCA'][$this->table]['ctrl']['editlock']] === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is a current record is a delete placeholder
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDeletePlaceholder(): bool
|
||||
{
|
||||
if (!isset($this->record['t3ver_state'])) {
|
||||
return false;
|
||||
}
|
||||
return VersionState::cast($this->record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if current record is in the "normal" pad of the clipboard
|
||||
*
|
||||
* @param string $mode "copy", "cut" or '' for any mode
|
||||
* @return bool
|
||||
*/
|
||||
protected function isRecordInClipboard(string $mode = ''): bool
|
||||
{
|
||||
$isSelected = '';
|
||||
if ($this->clipboard->current === 'normal') {
|
||||
$isSelected = $this->clipboard->isSelected($this->table, $this->record['uid']);
|
||||
}
|
||||
return $mode === '' ? !empty($isSelected) : $isSelected === $mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true is a record ia a translation
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isRecordATranslation(): bool
|
||||
{
|
||||
return BackendUtility::isTableLocalizable($this->table) && (int)$this->record[$GLOBALS['TCA'][$this->table]['ctrl']['transOrigPointerField']] !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
protected function getIdentifier(): string
|
||||
{
|
||||
return $this->record['uid'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LocalizationFactory;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* Abstract class for a couple of FormEngine controllers triggered by
|
||||
* ajax calls. The class containers some helpers to for instance prepare
|
||||
* the form render result for json output.
|
||||
*
|
||||
* @internal Marked as internal for now, methods in this class may change any time.
|
||||
*/
|
||||
abstract class AbstractFormEngineAjaxController
|
||||
{
|
||||
/**
|
||||
* Gets result array from FormEngine and returns string with js modules
|
||||
* that need to be loaded and evaluated by JavaScript.
|
||||
*
|
||||
* @param array $result
|
||||
* @return array
|
||||
*/
|
||||
protected function createExecutableStringRepresentationOfRegisteredRequireJsModules(array $result): array
|
||||
{
|
||||
if (empty($result['requireJsModules'])) {
|
||||
return [];
|
||||
}
|
||||
$requireJs = [];
|
||||
foreach ($result['requireJsModules'] as $module) {
|
||||
$moduleName = null;
|
||||
$callback = null;
|
||||
if (is_string($module)) {
|
||||
// if $module is a string, no callback
|
||||
$moduleName = $module;
|
||||
$callback = null;
|
||||
} elseif (is_array($module)) {
|
||||
// if $module is an array, callback is possible
|
||||
foreach ($module as $key => $value) {
|
||||
$moduleName = $key;
|
||||
$callback = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($moduleName !== null) {
|
||||
$inlineCodeKey = $moduleName;
|
||||
$javaScriptCode = 'require(["' . $moduleName . '"]';
|
||||
if ($callback !== null) {
|
||||
$inlineCodeKey .= sha1($callback);
|
||||
$javaScriptCode .= ', ' . $callback;
|
||||
}
|
||||
$javaScriptCode .= ');';
|
||||
$requireJs[] = '/*RequireJS-Module-' . $inlineCodeKey . '*/' . LF . $javaScriptCode;
|
||||
}
|
||||
}
|
||||
return $requireJs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a CSS file position, possibly prefixed with 'EXT:'
|
||||
*
|
||||
* @param string $stylesheetFile Given file, possibly prefixed with EXT:
|
||||
* @return string Web root relative position to file
|
||||
*/
|
||||
protected function getRelativePathToStylesheetFile(string $stylesheetFile): string
|
||||
{
|
||||
if (strpos($stylesheetFile, 'EXT:') === 0) {
|
||||
$stylesheetFile = GeneralUtility::getFileAbsFileName($stylesheetFile);
|
||||
$stylesheetFile = PathUtility::getRelativePathTo($stylesheetFile);
|
||||
$stylesheetFile = rtrim($stylesheetFile, '/');
|
||||
} else {
|
||||
$stylesheetFile = GeneralUtility::resolveBackPath($stylesheetFile);
|
||||
}
|
||||
$stylesheetFile = GeneralUtility::createVersionNumberedFilename($stylesheetFile);
|
||||
return PathUtility::getAbsoluteWebPath($stylesheetFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a language file and get a label/value array from it.
|
||||
*
|
||||
* @param string $file EXT:path/to/file
|
||||
* @return array Label/value array
|
||||
*/
|
||||
protected function getLabelsFromLocalizationFile($file)
|
||||
{
|
||||
/** @var LocalizationFactory $languageFactory */
|
||||
$languageFactory = GeneralUtility::makeInstance(LocalizationFactory::class);
|
||||
$language = $GLOBALS['LANG']->lang;
|
||||
$localizationArray = $languageFactory->getParsedData($file, $language);
|
||||
if (is_array($localizationArray) && !empty($localizationArray)) {
|
||||
if (!empty($localizationArray[$language])) {
|
||||
$xlfLabelArray = $localizationArray['default'];
|
||||
ArrayUtility::mergeRecursiveWithOverrule($xlfLabelArray, $localizationArray[$language], true, false);
|
||||
} else {
|
||||
$xlfLabelArray = $localizationArray['default'];
|
||||
}
|
||||
} else {
|
||||
$xlfLabelArray = [];
|
||||
}
|
||||
$labelArray = [];
|
||||
foreach ($xlfLabelArray as $key => $value) {
|
||||
if (isset($value[0]['target'])) {
|
||||
$labelArray[$key] = $value[0]['target'];
|
||||
} else {
|
||||
$labelArray[$key] = '';
|
||||
}
|
||||
}
|
||||
return $labelArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
|
||||
/**
|
||||
* This is the ajax handler for backend login after timeout.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class AjaxLoginController
|
||||
{
|
||||
/**
|
||||
* Handles the actual login process, more specifically it defines the response.
|
||||
* The login details were sent in as part of the ajax request and automatically logged in
|
||||
* the user inside the TYPO3 CMS bootstrap part of the ajax call. If that was successful, we have
|
||||
* a BE user and reset the timer and hide the login window.
|
||||
* If it was unsuccessful, we display that and show the login box again.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function loginAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($this->isAuthorizedBackendSession()) {
|
||||
$result = ['success' => true];
|
||||
if ($this->hasLoginBeenProcessed()) {
|
||||
/** @var \TYPO3\CMS\Core\FormProtection\BackendFormProtection $formProtection */
|
||||
$formProtection = FormProtectionFactory::get();
|
||||
$formProtection->setSessionTokenFromRegistry();
|
||||
$formProtection->persistSessionToken();
|
||||
}
|
||||
} else {
|
||||
$result = ['success' => false];
|
||||
}
|
||||
return new JsonResponse(['login' => $result]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out the current BE user
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function logoutAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$backendUser->logoff();
|
||||
return new JsonResponse([
|
||||
'logout' => [
|
||||
'success' => !isset($backendUser->user['uid'])
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function preflightAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$headers = $request->getHeaders();
|
||||
return new JsonResponse([
|
||||
'capabilities' => [
|
||||
'cookie' => !empty($request->getCookieParams()),
|
||||
// using legacy `Referer` (sic!) header name
|
||||
'referrer' => array_filter($headers['referer'] ?? []) !== [],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the login without needing login information. We just refresh the session.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function refreshAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->getBackendUser()->checkAuthentication();
|
||||
return new JsonResponse([
|
||||
'refresh' => [
|
||||
'success' => true
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user session is expired yet
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function isTimedOutAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$session = [
|
||||
'timed_out' => false,
|
||||
'will_time_out' => false,
|
||||
'locked' => false
|
||||
];
|
||||
$backendUser = $this->getBackendUser();
|
||||
if (@is_file(Environment::getLegacyConfigPath() . '/LOCK_BACKEND')) {
|
||||
$session['locked'] = true;
|
||||
} elseif (!isset($backendUser->user['uid'])) {
|
||||
$session['timed_out'] = true;
|
||||
} else {
|
||||
$backendUser->fetchUserSession(true);
|
||||
$ses_tstamp = $backendUser->user['ses_tstamp'];
|
||||
$timeout = $backendUser->sessionTimeout;
|
||||
// If 120 seconds from now is later than the session timeout, we need to show the refresh dialog.
|
||||
// 120 is somewhat arbitrary to allow for a little room during the countdown and load times, etc.
|
||||
$session['will_time_out'] = $GLOBALS['EXEC_TIME'] >= $ses_tstamp + $timeout - 120;
|
||||
}
|
||||
return new JsonResponse(['login' => $session]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user is logged in and the session is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isAuthorizedBackendSession()
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
return $backendUser !== null && $backendUser instanceof BackendUserAuthentication && isset($backendUser->user['uid']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the user was already authorized or not
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasLoginBeenProcessed()
|
||||
{
|
||||
$loginFormData = $this->getBackendUser()->getLoginFormData();
|
||||
return $loginFormData['status'] === 'login' && !empty($loginFormData['uname']) && !empty($loginFormData['uident']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication|null
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository;
|
||||
use TYPO3\CMS\Backend\Module\ModuleLoader;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Type\File\ImageInfo;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Class for rendering the TYPO3 backend
|
||||
*/
|
||||
class BackendController
|
||||
{
|
||||
use PublicMethodDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicMethods = [
|
||||
'render' => 'Using BackendController::render() is deprecated and will not be possible anymore in TYPO3 v10.0.',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $content = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $css = '';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $cssFiles = [];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $js = '';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $jsFiles = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $toolbarItems = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $debug;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $templatePath = 'EXT:backend/Resources/Private/Templates/';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $partialPath = 'EXT:backend/Resources/Private/Partials/';
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository
|
||||
*/
|
||||
protected $backendModuleRepository;
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Backend\Module\ModuleLoader Object for loading backend modules
|
||||
*/
|
||||
protected $moduleLoader;
|
||||
|
||||
/**
|
||||
* @var PageRenderer
|
||||
*/
|
||||
protected $pageRenderer;
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
|
||||
$this->backendModuleRepository = GeneralUtility::makeInstance(BackendModuleRepository::class);
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
// Set debug flag for BE development only
|
||||
$this->debug = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] === 1;
|
||||
// Initializes the backend modules structure for use later.
|
||||
$this->moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
|
||||
$this->moduleLoader->load($GLOBALS['TBE_MODULES']);
|
||||
$this->pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
|
||||
// Add default BE javascript
|
||||
$this->jsFiles = [
|
||||
'md5' => 'EXT:backend/Resources/Public/JavaScript/md5.js',
|
||||
'evalfield' => 'EXT:backend/Resources/Public/JavaScript/jsfunc.evalfield.js',
|
||||
'backend' => 'EXT:backend/Resources/Public/JavaScript/backend.js',
|
||||
];
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LoginRefresh', 'function(LoginRefresh) {
|
||||
LoginRefresh.setIntervalTime(' . MathUtility::forceIntegerInRange((int)$GLOBALS['TYPO3_CONF_VARS']['BE']['sessionTimeout'] - 60, 60) . ');
|
||||
LoginRefresh.setLoginFramesetUrl(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('login_frameset')) . ');
|
||||
LoginRefresh.setLogoutUrl(' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('logout')) . ');
|
||||
LoginRefresh.initialize();
|
||||
}');
|
||||
|
||||
// load module menu
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ModuleMenu');
|
||||
|
||||
// load Toolbar class
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Toolbar');
|
||||
|
||||
// load Utility class
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Utility');
|
||||
|
||||
// load Notification functionality
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Notification');
|
||||
|
||||
// load Modals
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
|
||||
|
||||
// load InfoWindow
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/InfoWindow');
|
||||
|
||||
// load ContextMenu
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
|
||||
// load the storage API and fill the UC into the PersistentStorage, so no additional AJAX call is needed
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Storage');
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Storage/Persistent', 'function(PersistentStorage) {
|
||||
PersistentStorage.load(' . json_encode($this->getBackendUser()->uc) . ');
|
||||
}');
|
||||
|
||||
// load debug console
|
||||
$this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DebugConsole');
|
||||
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_core.xlf');
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:backend/Resources/Private/Language/locallang_layout.xlf');
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf');
|
||||
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/debugger.xlf');
|
||||
$this->pageRenderer->addInlineLanguageLabelFile('EXT:core/Resources/Private/Language/wizard.xlf');
|
||||
|
||||
$this->pageRenderer->addInlineSetting('ShowItem', 'moduleUrl', (string)$uriBuilder->buildUriFromRoute('show_item'));
|
||||
$this->pageRenderer->addInlineSetting('RecordHistory', 'moduleUrl', (string)$uriBuilder->buildUriFromRoute('record_history'));
|
||||
$this->pageRenderer->addInlineSetting('NewRecord', 'moduleUrl', (string)$uriBuilder->buildUriFromRoute('db_new'));
|
||||
$this->pageRenderer->addInlineSetting('FormEngine', 'moduleUrl', (string)$uriBuilder->buildUriFromRoute('record_edit'));
|
||||
$this->pageRenderer->addInlineSetting('RecordCommit', 'moduleUrl', (string)$uriBuilder->buildUriFromRoute('tce_db'));
|
||||
$this->pageRenderer->addInlineSetting('WebLayout', 'moduleUrl', (string)$uriBuilder->buildUriFromRoute('web_layout'));
|
||||
|
||||
$this->css = '';
|
||||
|
||||
$this->initializeToolbarItems();
|
||||
$this->executeHook('constructPostProcess');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize toolbar item objects
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function initializeToolbarItems()
|
||||
{
|
||||
$toolbarItemInstances = [];
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['BE']['toolbarItems'] ?? [] as $className) {
|
||||
$toolbarItemInstance = GeneralUtility::makeInstance($className);
|
||||
if (!$toolbarItemInstance instanceof ToolbarItemInterface) {
|
||||
throw new \RuntimeException(
|
||||
'class ' . $className . ' is registered as toolbar item but does not implement'
|
||||
. ToolbarItemInterface::class,
|
||||
1415958218
|
||||
);
|
||||
}
|
||||
$index = (int)$toolbarItemInstance->getIndex();
|
||||
if ($index < 0 || $index > 100) {
|
||||
throw new \RuntimeException(
|
||||
'getIndex() must return an integer between 0 and 100',
|
||||
1415968498
|
||||
);
|
||||
}
|
||||
// Find next free position in array
|
||||
while (array_key_exists($index, $toolbarItemInstances)) {
|
||||
$index++;
|
||||
}
|
||||
$toolbarItemInstances[$index] = $toolbarItemInstance;
|
||||
}
|
||||
ksort($toolbarItemInstances);
|
||||
$this->toolbarItems = $toolbarItemInstances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the render() method, it is rather simple for now
|
||||
*
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(): ResponseInterface
|
||||
{
|
||||
$this->render();
|
||||
return new HtmlResponse($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function generating the BE scaffolding
|
||||
*/
|
||||
protected function render()
|
||||
{
|
||||
$this->executeHook('renderPreProcess');
|
||||
|
||||
// Prepare the scaffolding, at this point extension may still add javascript and css
|
||||
$view = $this->getFluidTemplateObject($this->templatePath . 'Backend/Main.html');
|
||||
|
||||
$view->assign('moduleMenuCollapsed', $this->getCollapseStateOfMenu());
|
||||
$view->assign('moduleMenu', $this->generateModuleMenu());
|
||||
$view->assign('topbar', $this->renderTopbar());
|
||||
|
||||
/******************************************************
|
||||
* Now put the complete backend document together
|
||||
******************************************************/
|
||||
foreach ($this->cssFiles as $cssFileName => $cssFile) {
|
||||
$this->pageRenderer->addCssFile($cssFile);
|
||||
// Load additional css files to overwrite existing core styles
|
||||
if (!empty($GLOBALS['TBE_STYLES']['stylesheets'][$cssFileName])) {
|
||||
$this->pageRenderer->addCssFile($GLOBALS['TBE_STYLES']['stylesheets'][$cssFileName]);
|
||||
}
|
||||
}
|
||||
if (!empty($this->css)) {
|
||||
$this->pageRenderer->addCssInlineBlock('BackendInlineCSS', $this->css);
|
||||
}
|
||||
foreach ($this->jsFiles as $jsFile) {
|
||||
$this->pageRenderer->addJsFile($jsFile);
|
||||
}
|
||||
$this->generateJavascript();
|
||||
$this->pageRenderer->addJsInlineCode('BackendInlineJavascript', $this->js, false);
|
||||
|
||||
// Set document title:
|
||||
$title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ? $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ' [TYPO3 CMS ' . TYPO3_version . ']' : 'TYPO3 CMS ' . TYPO3_version;
|
||||
// Renders the module page
|
||||
$this->content = $this->getDocumentTemplate()->render($title, $view->render());
|
||||
$hookConfiguration = ['content' => &$this->content];
|
||||
$this->executeHook('renderPostProcess', $hookConfiguration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the topbar, containing the backend logo, sitename etc.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTopbar()
|
||||
{
|
||||
$view = $this->getFluidTemplateObject($this->partialPath . 'Backend/Topbar.html');
|
||||
|
||||
// Extension Configuration to find the TYPO3 logo in the left corner
|
||||
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
|
||||
$logoPath = '';
|
||||
if (!empty($extConf['backendLogo'])) {
|
||||
$customBackendLogo = GeneralUtility::getFileAbsFileName(ltrim($extConf['backendLogo'], '/'));
|
||||
if (!empty($customBackendLogo)) {
|
||||
$logoPath = $customBackendLogo;
|
||||
}
|
||||
}
|
||||
// if no custom logo was set or the path is invalid, use the original one
|
||||
if (empty($logoPath) || !file_exists($logoPath)) {
|
||||
$logoPath = GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Public/Images/typo3_logo_orange.svg');
|
||||
$logoWidth = 22;
|
||||
$logoHeight = 22;
|
||||
} else {
|
||||
// set width/height for custom logo
|
||||
$imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $logoPath);
|
||||
$logoWidth = $imageInfo->getWidth() ?? '22';
|
||||
$logoHeight = $imageInfo->getHeight() ?? '22';
|
||||
|
||||
// High-resolution?
|
||||
if (strpos($logoPath, '@2x.') !== false) {
|
||||
$logoWidth /= 2;
|
||||
$logoHeight /= 2;
|
||||
}
|
||||
}
|
||||
|
||||
$view->assign('logoUrl', PathUtility::getAbsoluteWebPath($logoPath));
|
||||
$view->assign('logoWidth', $logoWidth);
|
||||
$view->assign('logoHeight', $logoHeight);
|
||||
$view->assign('applicationVersion', TYPO3_version);
|
||||
$view->assign('siteName', $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
|
||||
$view->assign('toolbar', $this->renderToolbar());
|
||||
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the items in the top toolbar
|
||||
*
|
||||
* @return string top toolbar elements as HTML
|
||||
*/
|
||||
protected function renderToolbar()
|
||||
{
|
||||
$toolbar = [];
|
||||
foreach ($this->toolbarItems as $toolbarItem) {
|
||||
/** @var \TYPO3\CMS\Backend\Toolbar\ToolbarItemInterface $toolbarItem */
|
||||
if ($toolbarItem->checkAccess()) {
|
||||
$hasDropDown = (bool)$toolbarItem->hasDropDown();
|
||||
$additionalAttributes = (array)$toolbarItem->getAdditionalAttributes();
|
||||
|
||||
$liAttributes = [];
|
||||
|
||||
// Merge class: Add dropdown class if hasDropDown, add classes from additional attributes
|
||||
$classes = [];
|
||||
$classes[] = 'toolbar-item';
|
||||
$classes[] = 't3js-toolbar-item';
|
||||
if (isset($additionalAttributes['class'])) {
|
||||
$classes[] = $additionalAttributes['class'];
|
||||
unset($additionalAttributes['class']);
|
||||
}
|
||||
$liAttributes['class'] = implode(' ', $classes);
|
||||
|
||||
// Add further attributes
|
||||
foreach ($additionalAttributes as $name => $value) {
|
||||
$liAttributes[$name] = $value;
|
||||
}
|
||||
|
||||
// Create a unique id from class name
|
||||
$fullyQualifiedClassName = \get_class($toolbarItem);
|
||||
$className = GeneralUtility::underscoredToLowerCamelCase($fullyQualifiedClassName);
|
||||
$className = GeneralUtility::camelCaseToLowerCaseUnderscored($className);
|
||||
$className = str_replace(['_', '\\'], '-', $className);
|
||||
$liAttributes['id'] = $className;
|
||||
|
||||
// Create data attribute identifier
|
||||
$shortName = substr($fullyQualifiedClassName, strrpos($fullyQualifiedClassName, '\\') + 1);
|
||||
$dataToolbarIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($shortName);
|
||||
$dataToolbarIdentifier = str_replace('_', '-', $dataToolbarIdentifier);
|
||||
$liAttributes['data-toolbar-identifier'] = $dataToolbarIdentifier;
|
||||
|
||||
$toolbar[] = '<li ' . GeneralUtility::implodeAttributes($liAttributes, true) . '>';
|
||||
|
||||
if ($hasDropDown) {
|
||||
$toolbar[] = '<a href="#" class="toolbar-item-link dropdown-toggle" data-toggle="dropdown">';
|
||||
$toolbar[] = $toolbarItem->getItem();
|
||||
$toolbar[] = '</a>';
|
||||
$toolbar[] = '<div class="dropdown-menu" role="menu">';
|
||||
$toolbar[] = $toolbarItem->getDropDown();
|
||||
$toolbar[] = '</div>';
|
||||
} else {
|
||||
$toolbar[] = $toolbarItem->getItem();
|
||||
}
|
||||
$toolbar[] = '</li>';
|
||||
}
|
||||
}
|
||||
return implode(LF, $toolbar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the JavaScript code for the backend.
|
||||
*/
|
||||
protected function generateJavascript()
|
||||
{
|
||||
$beUser = $this->getBackendUser();
|
||||
// Needed for FormEngine manipulation (date picker)
|
||||
$dateFormat = ($GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? ['MM-DD-YYYY', 'HH:mm MM-DD-YYYY'] : ['DD-MM-YYYY', 'HH:mm DD-MM-YYYY']);
|
||||
$this->pageRenderer->addInlineSetting('DateTimePicker', 'DateFormat', $dateFormat);
|
||||
|
||||
// If another page module was specified, replace the default Page module with the new one
|
||||
$newPageModule = trim($beUser->getTSConfig()['options.']['overridePageModule'] ?? '');
|
||||
$pageModule = BackendUtility::isModuleSetInTBE_MODULES($newPageModule) ? $newPageModule : 'web_layout';
|
||||
$pageModuleUrl = '';
|
||||
if (!$beUser->check('modules', $pageModule)) {
|
||||
$pageModule = '';
|
||||
} else {
|
||||
$pageModuleUrl = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute($pageModule);
|
||||
}
|
||||
$t3Configuration = [
|
||||
'username' => htmlspecialchars($beUser->user['username']),
|
||||
'pageModule' => $pageModule,
|
||||
'pageModuleUrl' => $pageModuleUrl,
|
||||
'inWorkspace' => $beUser->workspace !== 0,
|
||||
'showRefreshLoginPopup' => (bool)($GLOBALS['TYPO3_CONF_VARS']['BE']['showRefreshLoginPopup'] ?? false)
|
||||
];
|
||||
$this->js .= '
|
||||
TYPO3.configuration = ' . json_encode($t3Configuration) . ';
|
||||
/**
|
||||
* Frameset Module object
|
||||
*
|
||||
* Used in main modules with a frameset for submodules to keep the ID between modules
|
||||
* Typically that is set by something like this in a Web>* sub module:
|
||||
* if (top.fsMod) top.fsMod.recentIds["web"] = "\'.(int)$this->id.\'";
|
||||
* if (top.fsMod) top.fsMod.recentIds["file"] = "...(file reference/string)...";
|
||||
*/
|
||||
var fsMod = {
|
||||
recentIds: [], // used by frameset modules to track the most recent used id for list frame.
|
||||
navFrameHighlightedID: [], // used by navigation frames to track which row id was highlighted last time
|
||||
currentBank: "0"
|
||||
};
|
||||
|
||||
top.goToModule = function(modName, cMR_flag, addGetVars) {
|
||||
TYPO3.ModuleMenu.App.showModule(modName, addGetVars);
|
||||
}
|
||||
' . $this->setStartupModule();
|
||||
// Check editing of page:
|
||||
$this->handlePageEditing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking if the "&edit" variable was sent so we can open it for editing the page.
|
||||
*/
|
||||
protected function handlePageEditing()
|
||||
{
|
||||
$beUser = $this->getBackendUser();
|
||||
$userTsConfig = $this->getBackendUser()->getTSConfig();
|
||||
// EDIT page:
|
||||
$editId = preg_replace('/[^[:alnum:]_]/', '', GeneralUtility::_GET('edit'));
|
||||
if ($editId) {
|
||||
// Looking up the page to edit, checking permissions:
|
||||
$where = ' AND (' . $beUser->getPagePermsClause(Permission::PAGE_EDIT) . ' OR ' . $beUser->getPagePermsClause(Permission::CONTENT_EDIT) . ')';
|
||||
if (MathUtility::canBeInterpretedAsInteger($editId)) {
|
||||
$editRecord = BackendUtility::getRecordWSOL('pages', $editId, '*', $where);
|
||||
} else {
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
|
||||
|
||||
$editRecord = $queryBuilder->select('*')
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'alias',
|
||||
$queryBuilder->createNamedParameter($editId, \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->orX(
|
||||
$beUser->getPagePermsClause(Permission::PAGE_EDIT),
|
||||
$beUser->getPagePermsClause(Permission::CONTENT_EDIT)
|
||||
)
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->execute()
|
||||
->fetch();
|
||||
|
||||
if ($editRecord !== false) {
|
||||
BackendUtility::workspaceOL('pages', $editRecord);
|
||||
}
|
||||
}
|
||||
// If the page was accessible, then let the user edit it.
|
||||
if (is_array($editRecord) && $beUser->isInWebMount($editRecord)) {
|
||||
// Setting JS code to open editing:
|
||||
$this->js .= '
|
||||
// Load page to edit:
|
||||
window.setTimeout("top.loadEditId(' . (int)$editRecord['uid'] . ');", 500);
|
||||
';
|
||||
// Checking page edit parameter:
|
||||
if (!($userTsConfig['options.']['bookmark_onEditId_dontSetPageTree'] ?? false)) {
|
||||
$bookmarkKeepExpanded = (bool)($userTsConfig['options.']['bookmark_onEditId_keepExistingExpanded'] ?? false);
|
||||
// Expanding page tree:
|
||||
BackendUtility::openPageTree((int)$editRecord['pid'], !$bookmarkKeepExpanded);
|
||||
}
|
||||
} else {
|
||||
$this->js .= '
|
||||
// Warning about page editing:
|
||||
require(["TYPO3/CMS/Backend/Modal", "TYPO3/CMS/Backend/Severity"], function(Modal, Severity) {
|
||||
Modal.show("", ' . GeneralUtility::quoteJSvalue(sprintf($this->getLanguageService()->getLL('noEditPage'), $editId)) . ', Severity.notice, [{
|
||||
text: ' . GeneralUtility::quoteJSvalue($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:close')) . ',
|
||||
active: true,
|
||||
btnClass: "btn-info",
|
||||
name: "cancel",
|
||||
trigger: function () {
|
||||
Modal.currentModal.trigger("modal-dismiss");
|
||||
}
|
||||
}])
|
||||
});';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the startup module from either GETvars module and modParams or user configuration.
|
||||
*
|
||||
* @return string the JavaScript code for the startup module
|
||||
*/
|
||||
protected function setStartupModule()
|
||||
{
|
||||
$startModule = preg_replace('/[^[:alnum:]_]/', '', GeneralUtility::_GET('module'));
|
||||
$startModuleParameters = '';
|
||||
if (!$startModule) {
|
||||
$beUser = $this->getBackendUser();
|
||||
// start module on first login, will be removed once used the first time
|
||||
if (isset($beUser->uc['startModuleOnFirstLogin'])) {
|
||||
$startModule = $beUser->uc['startModuleOnFirstLogin'];
|
||||
unset($beUser->uc['startModuleOnFirstLogin']);
|
||||
$beUser->writeUC();
|
||||
} elseif ($beUser->uc['startModule']) {
|
||||
$startModule = $beUser->uc['startModule'];
|
||||
}
|
||||
|
||||
// check if the start module has additional parameters, so a redirect to a specific
|
||||
// action is possible
|
||||
if (strpos($startModule, '->') !== false) {
|
||||
list($startModule, $startModuleParameters) = explode('->', $startModule, 2);
|
||||
}
|
||||
}
|
||||
|
||||
$moduleParameters = GeneralUtility::_GET('modParams');
|
||||
// if no GET parameters are set, check if there are parameters given from the UC
|
||||
if (!$moduleParameters && $startModuleParameters) {
|
||||
$moduleParameters = $startModuleParameters;
|
||||
}
|
||||
|
||||
if ($startModule) {
|
||||
return '
|
||||
// start in module:
|
||||
top.startInModule = [' . GeneralUtility::quoteJSvalue($startModule) . ', ' . GeneralUtility::quoteJSvalue($moduleParameters) . '];
|
||||
';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a css snippet to the backend
|
||||
*
|
||||
* @param string $css Css snippet
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function addCss($css)
|
||||
{
|
||||
if (!is_string($css)) {
|
||||
throw new \InvalidArgumentException('parameter $css must be of type string', 1195129642);
|
||||
}
|
||||
$this->css .= $css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes defined hooks functions for the given identifier.
|
||||
*
|
||||
* These hook identifiers are valid:
|
||||
* + constructPostProcess
|
||||
* + renderPreProcess
|
||||
* + renderPostProcess
|
||||
*
|
||||
* @param string $identifier Specific hook identifier
|
||||
* @param array $hookConfiguration Additional configuration passed to hook functions
|
||||
*/
|
||||
protected function executeHook($identifier, array $hookConfiguration = [])
|
||||
{
|
||||
$options = &$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/backend.php'];
|
||||
foreach ($options[$identifier] ?? [] as $hookFunction) {
|
||||
GeneralUtility::callUserFunction($hookFunction, $hookConfiguration, $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* loads all modules from the repository
|
||||
* and renders it with a template
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function generateModuleMenu()
|
||||
{
|
||||
// get all modules except the user modules for the side menu
|
||||
$moduleStorage = $this->backendModuleRepository->loadAllowedModules(['user', 'help']);
|
||||
|
||||
$view = $this->getFluidTemplateObject($this->templatePath . 'ModuleMenu/Main.html');
|
||||
$view->assign('modules', $moduleStorage);
|
||||
return $view->render();
|
||||
}
|
||||
|
||||
protected function getCollapseStateOfMenu(): bool
|
||||
{
|
||||
$uc = json_decode(json_encode($this->getBackendUser()->uc), true);
|
||||
$collapseState = $uc['BackendComponents']['States']['typo3-module-menu']['collapsed'] ?? false;
|
||||
|
||||
return $collapseState === true || $collapseState === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Module menu for the AJAX request
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getModuleMenu(): ResponseInterface
|
||||
{
|
||||
return new JsonResponse(['menu' => $this->generateModuleMenu()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the toolbar for the AJAX request
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getTopbar(): ResponseInterface
|
||||
{
|
||||
return new JsonResponse(['topbar' => $this->renderTopbar()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $templatePathAndFileName optional the path to set the template path and filename
|
||||
* @return \TYPO3\CMS\Fluid\View\StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject($templatePathAndFileName = null)
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
if ($templatePathAndFileName) {
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($templatePathAndFileName));
|
||||
}
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Localization\LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of DocumentTemplate
|
||||
*
|
||||
* @return \TYPO3\CMS\Backend\Template\DocumentTemplate
|
||||
*/
|
||||
protected function getDocumentTemplate()
|
||||
{
|
||||
return $GLOBALS['TBE_TEMPLATE'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller\ContentElement;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\History\RecordHistory;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\DataHandling\History\RecordHistoryStore;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\DiffUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Controller for showing the history module of TYPO3s backend
|
||||
* @see \TYPO3\CMS\Backend\History\RecordHistory
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ElementHistoryController
|
||||
{
|
||||
/**
|
||||
* @var StandaloneView
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var RecordHistory
|
||||
*/
|
||||
protected $historyObject;
|
||||
|
||||
/**
|
||||
* Display diff or not (0-no diff, 1-inline)
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $showDiff = 1;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $recordCache = [];
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Restrict editing by non-Admins (0-no, 1-yes)
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $editLock = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$this->view = $this->initializeView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation([]);
|
||||
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$lastHistoryEntry = (int)($parsedBody['historyEntry'] ?? $queryParams['historyEntry'] ?? 0);
|
||||
$rollbackFields = $parsedBody['rollbackFields'] ?? $queryParams['rollbackFields'] ?? null;
|
||||
$element = $parsedBody['element'] ?? $queryParams['element'] ?? null;
|
||||
$displaySettings = $this->prepareDisplaySettings($request);
|
||||
$this->view->assign('currentSelection', $displaySettings);
|
||||
|
||||
$this->showDiff = (int)$displaySettings['showDiff'];
|
||||
|
||||
// Start history object
|
||||
$this->historyObject = GeneralUtility::makeInstance(RecordHistory::class, $element, $rollbackFields);
|
||||
$this->historyObject->setShowSubElements((int)$displaySettings['showSubElements']);
|
||||
$this->historyObject->setLastHistoryEntry($lastHistoryEntry);
|
||||
if ($displaySettings['maxSteps']) {
|
||||
$this->historyObject->setMaxSteps((int)$displaySettings['maxSteps']);
|
||||
}
|
||||
|
||||
// Do the actual logic now (rollback, show a diff for certain changes,
|
||||
// or show the full history of a page or a specific record)
|
||||
$this->historyObject->createChangeLog();
|
||||
if (!empty($this->historyObject->changeLog)) {
|
||||
if ($this->historyObject->shouldPerformRollback()) {
|
||||
$this->historyObject->performRollback();
|
||||
} elseif ($lastHistoryEntry) {
|
||||
$completeDiff = $this->historyObject->createMultipleDiff();
|
||||
$this->displayMultipleDiff($completeDiff);
|
||||
$this->view->assign('showDifferences', true);
|
||||
$this->view->assign('fullViewUrl', $this->buildUrl(['historyEntry' => '']));
|
||||
}
|
||||
if ($this->historyObject->getElementData()) {
|
||||
$this->displayHistory($this->historyObject->changeLog);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var \TYPO3\CMS\Core\Http\NormalizedParams $normalizedParams */
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$elementData = $this->historyObject->getElementData();
|
||||
if ($elementData) {
|
||||
$this->setPagePath($elementData[0], $elementData[1]);
|
||||
$this->editLock = $this->getEditLockFromElement($elementData[0], $elementData[1]);
|
||||
// Get link to page history if the element history is shown
|
||||
if ($elementData[0] !== 'pages') {
|
||||
$this->view->assign('singleElement', true);
|
||||
$parentPage = BackendUtility::getRecord($elementData[0], $elementData[1], '*', '', false);
|
||||
if ($parentPage['pid'] > 0 && BackendUtility::readPageAccess($parentPage['pid'], $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW))) {
|
||||
$this->view->assign('fullHistoryUrl', $this->buildUrl([
|
||||
'element' => 'pages:' . $parentPage['pid'],
|
||||
'historyEntry' => '',
|
||||
'returnUrl' => $normalizedParams->getRequestUri(),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->view->assign('TYPO3_REQUEST_URI', $normalizedParams->getRequestUrl());
|
||||
$this->view->assign('editLock', $this->editLock);
|
||||
|
||||
// Setting up the buttons and markers for docheader
|
||||
$this->getButtons($request);
|
||||
// Build the <body> for the module
|
||||
$this->moduleTemplate->setContent($this->view->render());
|
||||
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the correct path to the current record
|
||||
*
|
||||
* @param string $table
|
||||
* @param int $uid
|
||||
*/
|
||||
protected function setPagePath($table, $uid)
|
||||
{
|
||||
$uid = (int)$uid;
|
||||
|
||||
if ($table === 'pages') {
|
||||
$pageId = $uid;
|
||||
} else {
|
||||
$record = BackendUtility::getRecord($table, $uid, '*', '', false);
|
||||
$pageId = $record['pid'];
|
||||
}
|
||||
|
||||
$pageAccess = BackendUtility::readPageAccess($pageId, $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (is_array($pageAccess)) {
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageAccess);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the panel of buttons for submitting the form or otherwise perform operations.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function getButtons(ServerRequestInterface $request)
|
||||
{
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
|
||||
$helpButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('history_log');
|
||||
$buttonBar->addButton($helpButton);
|
||||
|
||||
// Get returnUrl parameter
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
|
||||
if ($returnUrl) {
|
||||
$backButton = $buttonBar->makeLinkButton()
|
||||
->setHref($returnUrl)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($backButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays settings evaluation
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
protected function prepareDisplaySettings(ServerRequestInterface $request)
|
||||
{
|
||||
// Get current selection from UC, merge data, write it back to UC
|
||||
$currentSelection = is_array($this->getBackendUser()->uc['moduleData']['history'])
|
||||
? $this->getBackendUser()->uc['moduleData']['history']
|
||||
: ['maxSteps' => '', 'showDiff' => 1, 'showSubElements' => 1];
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$currentSelectionOverride = $parsedBody['settings'] ?? $queryParams['settings'] ?? null;
|
||||
|
||||
if (is_array($currentSelectionOverride) && !empty($currentSelectionOverride)) {
|
||||
$currentSelection = array_merge($currentSelection, $currentSelectionOverride);
|
||||
$this->getBackendUser()->uc['moduleData']['history'] = $currentSelection;
|
||||
$this->getBackendUser()->writeUC($this->getBackendUser()->uc);
|
||||
}
|
||||
|
||||
// Display selector for number of history entries
|
||||
$selector['maxSteps'] = [
|
||||
10 => [
|
||||
'value' => 10
|
||||
],
|
||||
20 => [
|
||||
'value' => 20
|
||||
],
|
||||
50 => [
|
||||
'value' => 50
|
||||
],
|
||||
100 => [
|
||||
'value' => 100
|
||||
],
|
||||
999 => [
|
||||
'value' => 'maxSteps_all'
|
||||
]
|
||||
];
|
||||
$selector['showDiff'] = [
|
||||
0 => [
|
||||
'value' => 'showDiff_no'
|
||||
],
|
||||
1 => [
|
||||
'value' => 'showDiff_inline'
|
||||
]
|
||||
];
|
||||
$selector['showSubElements'] = [
|
||||
0 => [
|
||||
'value' => 'no'
|
||||
],
|
||||
1 => [
|
||||
'value' => 'yes'
|
||||
]
|
||||
];
|
||||
|
||||
$scriptUrl = GeneralUtility::linkThisScript();
|
||||
|
||||
foreach ($selector as $key => $values) {
|
||||
foreach ($values as $singleKey => $singleVal) {
|
||||
$selector[$key][$singleKey]['scriptUrl'] = htmlspecialchars(GeneralUtility::quoteJSvalue($scriptUrl . '&settings[' . $key . ']=' . $singleKey));
|
||||
}
|
||||
}
|
||||
$this->view->assign('settings', $selector);
|
||||
return $currentSelection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a diff over multiple fields including rollback links
|
||||
*
|
||||
* @param array $diff Difference array
|
||||
*/
|
||||
protected function displayMultipleDiff(array $diff)
|
||||
{
|
||||
// Get all array keys needed
|
||||
$arrayKeys = array_merge(array_keys($diff['newData']), array_keys($diff['insertsDeletes']), array_keys($diff['oldData']));
|
||||
$arrayKeys = array_unique($arrayKeys);
|
||||
if (!empty($arrayKeys)) {
|
||||
$lines = [];
|
||||
foreach ($arrayKeys as $key) {
|
||||
$singleLine = [];
|
||||
$elParts = explode(':', $key);
|
||||
// Turn around diff because it should be a "rollback preview"
|
||||
if ((int)$diff['insertsDeletes'][$key] === 1) {
|
||||
// insert
|
||||
$singleLine['insertDelete'] = 'delete';
|
||||
} elseif ((int)$diff['insertsDeletes'][$key] === -1) {
|
||||
$singleLine['insertDelete'] = 'insert';
|
||||
}
|
||||
// Build up temporary diff array
|
||||
// turn around diff because it should be a "rollback preview"
|
||||
if ($diff['newData'][$key]) {
|
||||
$tmpArr = [
|
||||
'newRecord' => $diff['oldData'][$key],
|
||||
'oldRecord' => $diff['newData'][$key]
|
||||
];
|
||||
$singleLine['differences'] = $this->renderDiff($tmpArr, $elParts[0], $elParts[1]);
|
||||
}
|
||||
$elParts = explode(':', $key);
|
||||
$singleLine['revertRecordUrl'] = $this->buildUrl(['rollbackFields' => $key]);
|
||||
$singleLine['title'] = $this->generateTitle($elParts[0], $elParts[1]);
|
||||
$lines[] = $singleLine;
|
||||
}
|
||||
$this->view->assign('revertAllUrl', $this->buildUrl(['rollbackFields' => 'ALL']));
|
||||
$this->view->assign('multipleDiff', $lines);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the full change log
|
||||
*
|
||||
* @param array $historyEntries
|
||||
*/
|
||||
protected function displayHistory(array $historyEntries)
|
||||
{
|
||||
if (empty($historyEntries)) {
|
||||
return;
|
||||
}
|
||||
$languageService = $this->getLanguageService();
|
||||
$lines = [];
|
||||
$beUserArray = BackendUtility::getUserNames();
|
||||
|
||||
// Traverse changeLog array:
|
||||
foreach ($historyEntries as $entry) {
|
||||
// Build up single line
|
||||
$singleLine = [];
|
||||
|
||||
// Get user names
|
||||
$singleLine['backendUserUid'] = $entry['userid'];
|
||||
$singleLine['backendUserName'] = $entry['userid'] ? $beUserArray[$entry['userid']]['username'] : '';
|
||||
// Executed by switch user
|
||||
if (!empty($entry['originaluserid'])) {
|
||||
$singleLine['originalBackendUserName'] = $beUserArray[$entry['originaluserid']]['username'];
|
||||
}
|
||||
|
||||
// Diff link
|
||||
$singleLine['diffUrl'] = $this->buildUrl(['historyEntry' => $entry['uid']]);
|
||||
// Add time
|
||||
$singleLine['time'] = BackendUtility::datetime($entry['tstamp']);
|
||||
// Add age
|
||||
$singleLine['age'] = BackendUtility::calcAge($GLOBALS['EXEC_TIME'] - $entry['tstamp'], $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears'));
|
||||
|
||||
$singleLine['title'] = $this->generateTitle($entry['tablename'], $entry['recuid']);
|
||||
$singleLine['elementUrl'] = $this->buildUrl(['element' => $entry['tablename'] . ':' . $entry['recuid']]);
|
||||
$singleLine['actiontype'] = $entry['actiontype'];
|
||||
if ((int)$entry['actiontype'] === RecordHistoryStore::ACTION_MODIFY) {
|
||||
// show changes
|
||||
if (!$this->showDiff) {
|
||||
// Display field names instead of full diff
|
||||
// Re-write field names with labels
|
||||
$tmpFieldList = array_keys($entry['newRecord']);
|
||||
foreach ($tmpFieldList as $key => $value) {
|
||||
$tmp = str_replace(':', '', $languageService->sL(BackendUtility::getItemLabel($entry['tablename'], $value)));
|
||||
if ($tmp) {
|
||||
$tmpFieldList[$key] = $tmp;
|
||||
} else {
|
||||
// remove fields if no label available
|
||||
unset($tmpFieldList[$key]);
|
||||
}
|
||||
}
|
||||
$singleLine['fieldNames'] = implode(',', $tmpFieldList);
|
||||
} else {
|
||||
// Display diff
|
||||
$singleLine['differences'] = $this->renderDiff($entry, $entry['tablename']);
|
||||
}
|
||||
}
|
||||
// put line together
|
||||
$lines[] = $singleLine;
|
||||
}
|
||||
$this->view->assign('history', $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders HTML table-rows with the comparison information of an sys_history entry record
|
||||
*
|
||||
* @param array $entry sys_history entry record.
|
||||
* @param string $table The table name
|
||||
* @param int $rollbackUid If set to UID of record, display rollback links
|
||||
* @return array array of records
|
||||
*/
|
||||
protected function renderDiff($entry, $table, $rollbackUid = 0): array
|
||||
{
|
||||
$lines = [];
|
||||
if (is_array($entry['newRecord'])) {
|
||||
/* @var DiffUtility $diffUtility */
|
||||
$diffUtility = GeneralUtility::makeInstance(DiffUtility::class);
|
||||
$diffUtility->stripTags = false;
|
||||
$fieldsToDisplay = array_keys($entry['newRecord']);
|
||||
$languageService = $this->getLanguageService();
|
||||
foreach ($fieldsToDisplay as $fN) {
|
||||
if (is_array($GLOBALS['TCA'][$table]['columns'][$fN]) && $GLOBALS['TCA'][$table]['columns'][$fN]['config']['type'] !== 'passthrough') {
|
||||
// Create diff-result:
|
||||
$diffres = $diffUtility->makeDiffDisplay(
|
||||
BackendUtility::getProcessedValue($table, $fN, $entry['oldRecord'][$fN], 0, true),
|
||||
BackendUtility::getProcessedValue($table, $fN, $entry['newRecord'][$fN], 0, true)
|
||||
);
|
||||
$rollbackUrl = '';
|
||||
if ($rollbackUid) {
|
||||
$rollbackUrl = $this->buildUrl(['rollbackFields' => $table . ':' . $rollbackUid . ':' . $fN]);
|
||||
}
|
||||
$lines[] = [
|
||||
'title' => $languageService->sL(BackendUtility::getItemLabel($table, $fN)),
|
||||
'rollbackUrl' => $rollbackUrl,
|
||||
'result' => str_replace('\n', PHP_EOL, str_replace('\r\n', '\n', $diffres))
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the URL for a link to the current page
|
||||
*
|
||||
* @param array $overrideParameters
|
||||
* @return string
|
||||
*/
|
||||
protected function buildUrl($overrideParameters = []): string
|
||||
{
|
||||
$params = [];
|
||||
// Setting default values based on GET parameters:
|
||||
if ($this->historyObject->getElementData()) {
|
||||
$params['element'] = $this->historyObject->getElementString();
|
||||
}
|
||||
$params['historyEntry'] = $this->historyObject->lastHistoryEntry;
|
||||
// Merging overriding values:
|
||||
$params = array_merge($params, $overrideParameters);
|
||||
// Make the link:
|
||||
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
return (string)$uriBuilder->buildUriFromRoute('record_history', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the title and puts the record title behind
|
||||
*
|
||||
* @param string $table
|
||||
* @param string $uid
|
||||
* @return string
|
||||
*/
|
||||
protected function generateTitle($table, $uid): string
|
||||
{
|
||||
$title = $table . ':' . $uid;
|
||||
if (!empty($GLOBALS['TCA'][$table]['ctrl']['label'])) {
|
||||
$record = $this->getRecord($table, $uid);
|
||||
$title .= ' (' . BackendUtility::getRecordTitle($table, $record, true) . ')';
|
||||
}
|
||||
return $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a database record (cached).
|
||||
*
|
||||
* @param string $table
|
||||
* @param int $uid
|
||||
* @return array|null
|
||||
*/
|
||||
protected function getRecord($table, $uid)
|
||||
{
|
||||
if (!isset($this->recordCache[$table][$uid])) {
|
||||
$this->recordCache[$table][$uid] = BackendUtility::getRecord($table, $uid, '*', '', false);
|
||||
}
|
||||
return $this->recordCache[$table][$uid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new standalone view, shorthand function
|
||||
*
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function initializeView()
|
||||
{
|
||||
/** @var StandaloneView $view */
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/RecordHistory/Main.html'));
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Localization\LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current backend user.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the editlock value from page of a history element
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param int $elementUid
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function getEditLockFromElement($tableName, $elementUid): bool
|
||||
{
|
||||
// If the user is admin, then he may always edit the page.
|
||||
if ($this->getBackendUser()->isAdmin()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$record = BackendUtility::getRecord($tableName, $elementUid, '*', '', false);
|
||||
// we need the parent page record for the editlock info if element isn't a page
|
||||
if ($tableName !== 'pages') {
|
||||
$pageId = $record['pid'];
|
||||
$record = BackendUtility::getRecord('pages', $pageId, '*', '', false);
|
||||
}
|
||||
|
||||
return (bool)$record['editlock'];
|
||||
}
|
||||
}
|
||||
+830
@@ -0,0 +1,830 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\ContentElement;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Backend\Avatar\Avatar;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Resource\AbstractFile;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script Class for showing information about an item.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ElementInformationController
|
||||
{
|
||||
use PublicMethodDeprecationTrait;
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicMethods = [
|
||||
'getLabelForTableColumn' => 'Using ElementInformationController::getLabelForTableColumn() is deprecated and will not be possible anymore in TYPO3 v10.0.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicProperties = [
|
||||
'table' => 'Using $table of class ElementInformationController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'uid' => 'Using $uid of class ElementInformationController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'access' => 'Using $access of class ElementInformationController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'type' => 'Using $type of class ElementInformationController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'pageInfo' => 'Using $pageInfo of class ElementInformationController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Record table name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* Record uid
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $uid;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $permsClause;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $access = false;
|
||||
|
||||
/**
|
||||
* Which type of element:
|
||||
* - "file"
|
||||
* - "db"
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = '';
|
||||
|
||||
/**
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* For type "db": Set to page record of the parent page of the item set
|
||||
* (if type="db")
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $pageInfo;
|
||||
|
||||
/**
|
||||
* Database records identified by table/uid
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $row;
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Core\Resource\File
|
||||
*/
|
||||
protected $fileObject;
|
||||
|
||||
/**
|
||||
* @var Folder
|
||||
*/
|
||||
protected $folderObject;
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$GLOBALS['SOBE'] = $this;
|
||||
|
||||
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0 with removal of init()
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->main($request);
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if table/uid point to database record or file and
|
||||
* if user has access to view information
|
||||
*
|
||||
* @param ServerRequestInterface|null $request
|
||||
*/
|
||||
public function init(ServerRequestInterface $request = null): void
|
||||
{
|
||||
if ($request === null) {
|
||||
// Missing argument? This method must have been called from outside.
|
||||
// Method will be protected and $request mandatory in TYPO3 v10.0, giving core freedom to move stuff around
|
||||
// New v10 signature: "protected function init(ServerRequestInterface $request): void"
|
||||
// @deprecated since TYPO3 v9, method argument $request will be set to mandatory
|
||||
trigger_error('ElementInformationController->init() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
}
|
||||
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$this->table = $queryParams['table'] ?? null;
|
||||
$this->uid = $queryParams['uid'] ?? null;
|
||||
|
||||
$this->permsClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$this->moduleTemplate->getDocHeaderComponent()->disable();
|
||||
|
||||
if (isset($GLOBALS['TCA'][$this->table])) {
|
||||
$this->initDatabaseRecord();
|
||||
} elseif ($this->table === '_FILE' || $this->table === '_FOLDER' || $this->table === 'sys_file') {
|
||||
$this->initFileOrFolderRecord();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init database records (table)
|
||||
*/
|
||||
protected function initDatabaseRecord()
|
||||
{
|
||||
$this->type = 'db';
|
||||
$this->uid = (int)$this->uid;
|
||||
|
||||
// Check permissions and uid value:
|
||||
if ($this->uid && $this->getBackendUser()->check('tables_select', $this->table)) {
|
||||
if ((string)$this->table === 'pages') {
|
||||
$this->pageInfo = BackendUtility::readPageAccess($this->uid, $this->permsClause);
|
||||
$this->access = is_array($this->pageInfo);
|
||||
$this->row = $this->pageInfo;
|
||||
} else {
|
||||
$this->row = BackendUtility::getRecordWSOL($this->table, $this->uid);
|
||||
if ($this->row) {
|
||||
// Find the correct "pid" when a versionized record is given, otherwise "pid = -1" always fails
|
||||
if (!empty($this->row['t3ver_oid'])) {
|
||||
$t3OrigRow = BackendUtility::getRecord($this->table, (int)$this->row['t3ver_oid']);
|
||||
$this->pageInfo = BackendUtility::readPageAccess((int)$t3OrigRow['pid'], $this->permsClause);
|
||||
} else {
|
||||
$this->pageInfo = BackendUtility::readPageAccess($this->row['pid'], $this->permsClause);
|
||||
}
|
||||
$this->access = is_array($this->pageInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Init file/folder parameters
|
||||
*/
|
||||
protected function initFileOrFolderRecord()
|
||||
{
|
||||
$fileOrFolderObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($this->uid);
|
||||
|
||||
if ($fileOrFolderObject instanceof Folder) {
|
||||
$this->folderObject = $fileOrFolderObject;
|
||||
$this->access = $this->folderObject->checkActionPermission('read');
|
||||
$this->type = 'folder';
|
||||
} elseif ($fileOrFolderObject instanceof File) {
|
||||
$this->fileObject = $fileOrFolderObject;
|
||||
$this->access = $this->fileObject->checkActionPermission('read');
|
||||
$this->type = 'file';
|
||||
$this->table = 'sys_file';
|
||||
|
||||
try {
|
||||
$this->row = BackendUtility::getRecordWSOL($this->table, $fileOrFolderObject->getUid());
|
||||
} catch (\Exception $e) {
|
||||
$this->row = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the whole content to be outputted, which is then set as content to the moduleTemplate
|
||||
* There is a hook to do a custom rendering of a record.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
public function main(ServerRequestInterface $request = null): void
|
||||
{
|
||||
if ($request === null) {
|
||||
// Missing argument? This method must have been called from outside.
|
||||
// @deprecated since TYPO3 v9, method argument $request will be set to mandatory
|
||||
trigger_error('ElementInformationController->main() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
}
|
||||
|
||||
$content = '';
|
||||
|
||||
// Rendering of the output via fluid
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/ContentElement/ElementInformation.html'
|
||||
));
|
||||
|
||||
if ($this->access) {
|
||||
// render type by user func
|
||||
$typeRendered = false;
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/show_item.php']['typeRendering'] ?? [] as $className) {
|
||||
$typeRenderObj = GeneralUtility::makeInstance($className);
|
||||
if (is_object($typeRenderObj) && method_exists($typeRenderObj, 'isValid') && method_exists($typeRenderObj, 'render')) {
|
||||
if ($typeRenderObj->isValid($this->type, $this)) {
|
||||
$content .= $typeRenderObj->render($this->type, $this);
|
||||
$typeRendered = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$typeRendered) {
|
||||
$view->assign('accessAllowed', true);
|
||||
$view->assignMultiple($this->getPageTitle());
|
||||
$view->assignMultiple($this->getPreview());
|
||||
$view->assignMultiple($this->getPropertiesForTable());
|
||||
$view->assignMultiple($this->getReferences($request));
|
||||
$view->assign('returnUrl', GeneralUtility::sanitizeLocalUrl($request->getQueryParams()['returnUrl']));
|
||||
$view->assign('maxTitleLength', $this->getBackendUser()->uc['titleLen'] ?? 20);
|
||||
$content .= $view->render();
|
||||
}
|
||||
} else {
|
||||
$content .= $view->render();
|
||||
}
|
||||
|
||||
$this->moduleTemplate->setContent($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page title with icon, table title and record title
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPageTitle(): array
|
||||
{
|
||||
$pageTitle = [
|
||||
'title' => BackendUtility::getRecordTitle($this->table, $this->row, false)
|
||||
];
|
||||
if ($this->type === 'folder') {
|
||||
$pageTitle['table'] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:folder');
|
||||
$pageTitle['icon'] = $this->iconFactory->getIconForResource($this->folderObject, Icon::SIZE_SMALL)->render();
|
||||
} elseif ($this->type === 'file') {
|
||||
$pageTitle['table'] = $this->getLanguageService()->sL($GLOBALS['TCA'][$this->table]['ctrl']['title']);
|
||||
$pageTitle['icon'] = $this->iconFactory->getIconForResource($this->fileObject, Icon::SIZE_SMALL)->render();
|
||||
} else {
|
||||
$pageTitle['table'] = $this->getLanguageService()->sL($GLOBALS['TCA'][$this->table]['ctrl']['title']);
|
||||
$pageTitle['icon'] = $this->iconFactory->getIconForRecord($this->table, $this->row, Icon::SIZE_SMALL);
|
||||
}
|
||||
$this->moduleTemplate->setTitle($pageTitle['table'] . ': ' . $pageTitle['title']);
|
||||
return $pageTitle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get preview for current record
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPreview(): array
|
||||
{
|
||||
$preview = [];
|
||||
// Perhaps @todo in future: Also display preview for records - without fileObject
|
||||
if (!$this->fileObject) {
|
||||
return $preview;
|
||||
}
|
||||
|
||||
// check if file is marked as missing
|
||||
if ($this->fileObject->isMissing()) {
|
||||
$preview['missingFile'] = $this->fileObject->getName();
|
||||
} else {
|
||||
/** @var \TYPO3\CMS\Core\Resource\Rendering\RendererRegistry $rendererRegistry */
|
||||
$rendererRegistry = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Rendering\RendererRegistry::class);
|
||||
$fileRenderer = $rendererRegistry->getRenderer($this->fileObject);
|
||||
$fileExtension = $this->fileObject->getExtension();
|
||||
$preview['url'] = $this->fileObject->getPublicUrl(true);
|
||||
|
||||
$width = '590m';
|
||||
$heigth = '400m';
|
||||
|
||||
// Check if there is a FileRenderer
|
||||
if ($fileRenderer !== null) {
|
||||
$preview['fileRenderer'] = $fileRenderer->render(
|
||||
$this->fileObject,
|
||||
$width,
|
||||
$heigth,
|
||||
[],
|
||||
true
|
||||
);
|
||||
|
||||
// else check if we can create an Image preview
|
||||
} elseif (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileExtension)) {
|
||||
$preview['fileObject'] = $this->fileObject;
|
||||
$preview['width'] = $width;
|
||||
$preview['heigth'] = $heigth;
|
||||
}
|
||||
}
|
||||
return $preview;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get property array for html table
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getPropertiesForTable(): array
|
||||
{
|
||||
$propertiesForTable = [];
|
||||
$lang = $this->getLanguageService();
|
||||
|
||||
$extraFields = [
|
||||
'uid' => htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:show_item.php.uid'))
|
||||
];
|
||||
|
||||
if (in_array($this->type, ['folder', 'file'], true)) {
|
||||
if ($this->type === 'file') {
|
||||
$extraFields['creation_date'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationDate'));
|
||||
$extraFields['modification_date'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.timestamp'));
|
||||
if ($this->fileObject->getType() === AbstractFile::FILETYPE_IMAGE) {
|
||||
$extraFields['width'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.width'));
|
||||
$extraFields['height'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.height'));
|
||||
}
|
||||
}
|
||||
$extraFields['storage'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:sys_file.storage'));
|
||||
$extraFields['folder'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:folder'));
|
||||
} else {
|
||||
$extraFields['crdate'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationDate'));
|
||||
$extraFields['cruser_id'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.creationUserId'));
|
||||
$extraFields['tstamp'] = htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_general.xlf:LGL.timestamp'));
|
||||
|
||||
// check if the special fields are defined in the TCA ctrl section of the table
|
||||
foreach ($extraFields as $fieldName => $fieldLabel) {
|
||||
if (isset($GLOBALS['TCA'][$this->table]['ctrl'][$fieldName])) {
|
||||
$extraFields[$GLOBALS['TCA'][$this->table]['ctrl'][$fieldName]] = $fieldLabel;
|
||||
} elseif ($fieldName !== 'uid') {
|
||||
unset($extraFields[$fieldName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($extraFields as $name => $fieldLabel) {
|
||||
$rowValue = '';
|
||||
$thisRow = [];
|
||||
if (!isset($this->row[$name])) {
|
||||
$resourceObject = $this->fileObject ?: $this->folderObject;
|
||||
if ($name === 'storage') {
|
||||
$rowValue = $resourceObject->getStorage()->getName();
|
||||
} elseif ($name === 'folder') {
|
||||
$rowValue = $resourceObject->getParentFolder()->getReadablePath();
|
||||
} elseif ($name === 'width') {
|
||||
$rowValue = $this->fileObject->getProperty('width') . 'px';
|
||||
} elseif ($name === 'height') {
|
||||
$rowValue = $this->fileObject->getProperty('height') . 'px';
|
||||
}
|
||||
} elseif ($name === 'creation_date' || $name === 'modification_date' || $name === 'tstamp' || $name === 'crdate') {
|
||||
$rowValue = BackendUtility::datetime($this->row[$name]);
|
||||
} else {
|
||||
$rowValue = BackendUtility::getProcessedValueExtra($this->table, $name, $this->row[$name]);
|
||||
}
|
||||
$thisRow['value'] = $rowValue;
|
||||
$thisRow['fieldLabel'] = rtrim($fieldLabel, ':');
|
||||
// show the backend username who created the issue
|
||||
if ($name === 'cruser_id' && $rowValue) {
|
||||
$creatorRecord = BackendUtility::getRecord('be_users', $rowValue);
|
||||
if ($creatorRecord) {
|
||||
/** @var Avatar $avatar */
|
||||
$avatar = GeneralUtility::makeInstance(Avatar::class);
|
||||
$creatorRecord['icon'] = $avatar->render($creatorRecord);
|
||||
$thisRow['creatorRecord'] = $creatorRecord;
|
||||
$thisRow['value'] = '';
|
||||
}
|
||||
}
|
||||
$propertiesForTable['extraFields'][] = $thisRow;
|
||||
}
|
||||
|
||||
// Traverse the list of fields to display for the record:
|
||||
$fieldList = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$this->table]['interface']['showRecordFieldList'], true);
|
||||
foreach ($fieldList as $name) {
|
||||
$thisRow = [];
|
||||
$name = trim($name);
|
||||
$uid = $this->row['uid'];
|
||||
|
||||
if (!isset($GLOBALS['TCA'][$this->table]['columns'][$name])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Storage is already handled above
|
||||
if ($this->type === 'file' && $name === 'storage') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// format file size as bytes/kilobytes/megabytes
|
||||
if ($this->type === 'file' && $name === 'size') {
|
||||
$this->row[$name] = GeneralUtility::formatSize($this->row[$name], htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:byteSizeUnits')));
|
||||
}
|
||||
|
||||
$isExcluded = !(!$GLOBALS['TCA'][$this->table]['columns'][$name]['exclude'] || $this->getBackendUser()->check('non_exclude_fields', $this->table . ':' . $name));
|
||||
if ($isExcluded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$thisRow['fieldValue'] = BackendUtility::getProcessedValue($this->table, $name, $this->row[$name], 0, 0, false, $uid);
|
||||
$thisRow['fieldLabel'] = htmlspecialchars($lang->sL(BackendUtility::getItemLabel($this->table, $name)));
|
||||
$propertiesForTable['fields'][] = $thisRow;
|
||||
}
|
||||
return $propertiesForTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get references section (references from and references to current record)
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
protected function getReferences(ServerRequestInterface $request): array
|
||||
{
|
||||
$references = [];
|
||||
switch ($this->type) {
|
||||
case 'db': {
|
||||
$references['refLines'] = $this->makeRef($this->table, $this->row['uid'], $request);
|
||||
$references['refFromLines'] = $this->makeRefFrom($this->table, $this->row['uid'], $request);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'file': {
|
||||
if ($this->fileObject && $this->fileObject->isIndexed()) {
|
||||
$references['refLines'] = $this->makeRef('_FILE', $this->fileObject, $request);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $references;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field name for specified table/column name
|
||||
*
|
||||
* @param string $tableName Table name
|
||||
* @param string $fieldName Column name
|
||||
* @return string label
|
||||
*/
|
||||
protected function getLabelForTableColumn($tableName, $fieldName)
|
||||
{
|
||||
if ($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['label'] !== null) {
|
||||
$field = $this->getLanguageService()->sL($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['label']);
|
||||
if (trim($field) === '') {
|
||||
$field = $fieldName;
|
||||
}
|
||||
} else {
|
||||
$field = $fieldName;
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the record actions
|
||||
*
|
||||
* @param string $table
|
||||
* @param int $uid
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
protected function getRecordActions($table, $uid, ServerRequestInterface $request): array
|
||||
{
|
||||
if ($table === '' || $uid < 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$actions = [];
|
||||
// Edit button
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$table => [
|
||||
$uid => 'edit'
|
||||
]
|
||||
],
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()
|
||||
];
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
$actions['recordEditUrl'] = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
|
||||
// History button
|
||||
$urlParameters = [
|
||||
'element' => $table . ':' . $uid,
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()
|
||||
];
|
||||
$actions['recordHistoryUrl'] = (string)$uriBuilder->buildUriFromRoute('record_history', $urlParameters);
|
||||
|
||||
if ($table === 'pages') {
|
||||
// Recordlist button
|
||||
$actions['webListUrl'] = (string)$uriBuilder->buildUriFromRoute('web_list', ['id' => $uid, 'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()]);
|
||||
|
||||
// View page button
|
||||
$actions['viewOnClick'] = BackendUtility::viewOnClick($uid, '', BackendUtility::BEgetRootLine($uid));
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make reference display
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param string|\TYPO3\CMS\Core\Resource\File $ref Filename or uid
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
protected function makeRef($table, $ref, ServerRequestInterface $request)
|
||||
{
|
||||
$refLines = [];
|
||||
$lang = $this->getLanguageService();
|
||||
// Files reside in sys_file table
|
||||
if ($table === '_FILE') {
|
||||
$selectTable = 'sys_file';
|
||||
$selectUid = $ref->getUid();
|
||||
} else {
|
||||
$selectTable = $table;
|
||||
$selectUid = $ref;
|
||||
}
|
||||
/** @var \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder */
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_refindex');
|
||||
|
||||
$predicates = [
|
||||
$queryBuilder->expr()->eq(
|
||||
'ref_table',
|
||||
$queryBuilder->createNamedParameter($selectTable, \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'ref_uid',
|
||||
$queryBuilder->createNamedParameter($selectUid, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'deleted',
|
||||
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
|
||||
)
|
||||
];
|
||||
|
||||
$backendUser = $this->getBackendUser();
|
||||
if (!$backendUser->isAdmin()) {
|
||||
$allowedSelectTables = GeneralUtility::trimExplode(',', $backendUser->groupData['tables_select']);
|
||||
$predicates[] = $queryBuilder->expr()->in(
|
||||
'tablename',
|
||||
$queryBuilder->createNamedParameter($allowedSelectTables, Connection::PARAM_STR_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
$rows = $queryBuilder
|
||||
->select('*')
|
||||
->from('sys_refindex')
|
||||
->where(...$predicates)
|
||||
->execute()
|
||||
->fetchAll();
|
||||
|
||||
// Compile information for title tag:
|
||||
foreach ($rows as $row) {
|
||||
if ($row['tablename'] === 'sys_file_reference') {
|
||||
$row = $this->transformFileReferenceToRecordReference($row);
|
||||
if ($row['tablename'] === null || $row['recuid'] === null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$line = [];
|
||||
$record = BackendUtility::getRecord($row['tablename'], $row['recuid']);
|
||||
if ($record) {
|
||||
BackendUtility::fixVersioningPid($row['tablename'], $record);
|
||||
if (!$this->canAccessPage($row['tablename'], $record)) {
|
||||
continue;
|
||||
}
|
||||
$parentRecord = BackendUtility::getRecord('pages', $record['pid']);
|
||||
$parentRecordTitle = is_array($parentRecord)
|
||||
? BackendUtility::getRecordTitle('pages', $parentRecord)
|
||||
: '';
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$row['tablename'] => [
|
||||
$row['recuid'] => 'edit'
|
||||
]
|
||||
],
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()
|
||||
];
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
$line['url'] = $url;
|
||||
$line['icon'] = $this->iconFactory->getIconForRecord($row['tablename'], $record, Icon::SIZE_SMALL)->render();
|
||||
$line['row'] = $row;
|
||||
$line['record'] = $record;
|
||||
$line['recordTitle'] = BackendUtility::getRecordTitle($row['tablename'], $record, false, true);
|
||||
$line['parentRecordTitle'] = $parentRecordTitle;
|
||||
$line['title'] = $lang->sL($GLOBALS['TCA'][$row['tablename']]['ctrl']['title']);
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($row['tablename'], $row['field']);
|
||||
$line['path'] = BackendUtility::getRecordPath($record['pid'], '', 0, 0);
|
||||
$line['actions'] = $this->getRecordActions($row['tablename'], $row['recuid'], $request);
|
||||
} else {
|
||||
$line['row'] = $row;
|
||||
$line['title'] = $lang->sL($GLOBALS['TCA'][$row['tablename']]['ctrl']['title']) ?: $row['tablename'];
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($row['tablename'], $row['field']);
|
||||
}
|
||||
$refLines[] = $line;
|
||||
}
|
||||
return $refLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make reference display (what this elements points to)
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param string $ref Filename or uid
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
protected function makeRefFrom($table, $ref, ServerRequestInterface $request): array
|
||||
{
|
||||
$refFromLines = [];
|
||||
$lang = $this->getLanguageService();
|
||||
|
||||
/** @var \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder */
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_refindex');
|
||||
|
||||
$predicates = [
|
||||
$queryBuilder->expr()->eq(
|
||||
'tablename',
|
||||
$queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'recuid',
|
||||
$queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT)
|
||||
)
|
||||
];
|
||||
|
||||
$backendUser = $this->getBackendUser();
|
||||
if (!$backendUser->isAdmin()) {
|
||||
$allowedSelectTables = GeneralUtility::trimExplode(',', $backendUser->groupData['tables_select']);
|
||||
$predicates[] = $queryBuilder->expr()->in(
|
||||
'ref_table',
|
||||
$queryBuilder->createNamedParameter($allowedSelectTables, Connection::PARAM_STR_ARRAY)
|
||||
);
|
||||
}
|
||||
|
||||
$rows = $queryBuilder
|
||||
->select('*')
|
||||
->from('sys_refindex')
|
||||
->where(...$predicates)
|
||||
->execute()
|
||||
->fetchAll();
|
||||
|
||||
// Compile information for title tag:
|
||||
foreach ($rows as $row) {
|
||||
$line = [];
|
||||
$record = BackendUtility::getRecord($row['ref_table'], $row['ref_uid']);
|
||||
if ($record) {
|
||||
BackendUtility::fixVersioningPid($row['ref_table'], $record);
|
||||
if (!$this->canAccessPage($row['ref_table'], $record)) {
|
||||
continue;
|
||||
}
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$row['ref_table'] => [
|
||||
$row['ref_uid'] => 'edit'
|
||||
]
|
||||
],
|
||||
'returnUrl' => $request->getAttribute('normalizedParams')->getRequestUri()
|
||||
];
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
$line['url'] = $url;
|
||||
$line['icon'] = $this->iconFactory->getIconForRecord($row['tablename'], $record, Icon::SIZE_SMALL)->render();
|
||||
$line['row'] = $row;
|
||||
$line['record'] = $record;
|
||||
$line['recordTitle'] = BackendUtility::getRecordTitle($row['ref_table'], $record, false, true);
|
||||
$line['title'] = $lang->sL($GLOBALS['TCA'][$row['ref_table']]['ctrl']['title']);
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($table, $row['field']);
|
||||
$line['path'] = BackendUtility::getRecordPath($record['pid'], '', 0, 0);
|
||||
$line['actions'] = $this->getRecordActions($row['ref_table'], $row['ref_uid'], $request);
|
||||
} else {
|
||||
$line['row'] = $row;
|
||||
$line['title'] = $lang->sL($GLOBALS['TCA'][$row['ref_table']]['ctrl']['title']);
|
||||
$line['labelForTableColumn'] = $this->getLabelForTableColumn($table, $row['field']);
|
||||
}
|
||||
$refFromLines[] = $line;
|
||||
}
|
||||
return $refFromLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert FAL file reference (sys_file_reference) to reference index (sys_refindex) table format
|
||||
*
|
||||
* @param array $referenceRecord
|
||||
* @return array
|
||||
*/
|
||||
protected function transformFileReferenceToRecordReference(array $referenceRecord)
|
||||
{
|
||||
/** @var \TYPO3\CMS\Core\Database\Query\QueryBuilder $queryBuilder */
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_file_reference');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
$fileReference = $queryBuilder
|
||||
->select('*')
|
||||
->from('sys_file_reference')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($referenceRecord['recuid'], \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute()
|
||||
->fetch();
|
||||
|
||||
return [
|
||||
'recuid' => $fileReference['uid_foreign'],
|
||||
'tablename' => $fileReference['tablenames'],
|
||||
'field' => $fileReference['fieldname'],
|
||||
'flexpointer' => '',
|
||||
'softref_key' => '',
|
||||
'sorting' => $fileReference['sorting_foreign']
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tableName Name of the table
|
||||
* @param array $record Record to be checked (ensure pid is resolved for workspaces)
|
||||
* @return bool
|
||||
*/
|
||||
protected function canAccessPage(string $tableName, array $record): bool
|
||||
{
|
||||
$recordPid = (int)($tableName === 'pages' ? $record['uid'] : $record['pid']);
|
||||
return $this->getBackendUser()->isInWebMount($tableName === 'pages' ? $record : $record['pid'])
|
||||
|| $recordPid === 0 && !empty($GLOBALS['TCA'][$tableName]['ctrl']['security']['ignoreRootLevelRestriction']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Localization\LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\ContentElement;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Tree\View\ContentMovingPagePositionMap;
|
||||
use TYPO3\CMS\Backend\Tree\View\PageMovingPagePositionMap;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script Class for rendering the move-element wizard display
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class MoveElementController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'sys_language' => 'Using $sys_language of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'page_id' => 'Using $page_id of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'table' => 'Using $table of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'R_URI' => 'Using $R_URI of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'input_moveUid' => 'Using $input_moveUid of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'moveUid' => 'Using $moveUid of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'makeCopy' => 'Using $makeCopy of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'perms_clause' => 'Using $perms_clause of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
'content' => 'Using $content of MoveElementController from the outside is discouraged, as this variable is used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $sys_language = 0;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $page_id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $R_URI;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $input_moveUid;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $moveUid;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $makeCopy;
|
||||
|
||||
/**
|
||||
* Pages-select clause
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $perms_clause;
|
||||
|
||||
/**
|
||||
* Content for module accumulated here.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
|
||||
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0 with removal of init()
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->renderContent();
|
||||
|
||||
return new HtmlResponse($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor, initializing internal variables.
|
||||
*
|
||||
* @param ServerRequestInterface|null $request
|
||||
*/
|
||||
public function init(ServerRequestInterface $request = null)
|
||||
{
|
||||
if ($request === null) {
|
||||
// Method signature in TYPO3 v10.0: protected function init(ServerRequestInterface $request)
|
||||
trigger_error('MoveElementController->init() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
}
|
||||
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
// Setting internal vars:
|
||||
$this->sys_language = (int)($parsedBody['sys_language'] ?? $queryParams['sys_language'] ?? 0);
|
||||
$this->page_id = (int)($parsedBody['uid'] ?? $queryParams['uid'] ?? 0);
|
||||
$this->table = $parsedBody['table'] ?? $queryParams['table'] ?? null;
|
||||
$this->R_URI = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
$this->input_moveUid = $parsedBody['moveUid'] ?? $queryParams['moveUid'] ?? null;
|
||||
$this->moveUid = $this->input_moveUid ? $this->input_moveUid : $this->page_id;
|
||||
$this->makeCopy = $parsedBody['makeCopy'] ?? $queryParams['makeCopy'] ?? 0;
|
||||
// Select-pages where clause for read-access:
|
||||
$this->perms_clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating the module output.
|
||||
*/
|
||||
protected function renderContent(): void
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
|
||||
if ($this->page_id) {
|
||||
$assigns = [];
|
||||
$backendUser = $this->getBackendUser();
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
|
||||
// Get record for element:
|
||||
$elRow = BackendUtility::getRecordWSOL($this->table, $this->moveUid);
|
||||
// Headerline: Icon, record title:
|
||||
$assigns['table'] = $this->table;
|
||||
$assigns['elRow'] = $elRow;
|
||||
$assigns['recordTooltip'] = BackendUtility::getRecordToolTip($elRow, $this->table);
|
||||
$assigns['recordTitle'] = BackendUtility::getRecordTitle($this->table, $elRow, true);
|
||||
// Make-copy checkbox (clicking this will reload the page with the GET var makeCopy set differently):
|
||||
$onClick = 'window.location.href=' . GeneralUtility::quoteJSvalue(GeneralUtility::linkThisScript(['makeCopy' => !$this->makeCopy])) . ';';
|
||||
$assigns['makeCopyChecked'] = $this->makeCopy ? ' checked="checked"' : '';
|
||||
$assigns['makeCopyOnClick'] = $onClick;
|
||||
// IF the table is "pages":
|
||||
if ((string)$this->table === 'pages') {
|
||||
// Get page record (if accessible):
|
||||
$pageInfo = BackendUtility::readPageAccess($this->page_id, $this->perms_clause);
|
||||
if (is_array($pageInfo) && $backendUser->isInWebMount($pageInfo['pid'], $this->perms_clause)) {
|
||||
// Initialize the position map:
|
||||
$posMap = GeneralUtility::makeInstance(PageMovingPagePositionMap::class);
|
||||
$posMap->moveOrCopy = $this->makeCopy ? 'copy' : 'move';
|
||||
$posMap->moveUid = $this->moveUid;
|
||||
// Print a "go-up" link IF there is a real parent page (and if the user has read-access to that page).
|
||||
if ($pageInfo['pid']) {
|
||||
$pidPageInfo = BackendUtility::readPageAccess($pageInfo['pid'], $this->perms_clause);
|
||||
if (is_array($pidPageInfo)) {
|
||||
if ($backendUser->isInWebMount($pidPageInfo['pid'], $this->perms_clause)) {
|
||||
$assigns['pages']['goUpUrl'] = GeneralUtility::linkThisScript([
|
||||
'uid' => (int)$pageInfo['pid'],
|
||||
'moveUid' => $this->moveUid
|
||||
]);
|
||||
} else {
|
||||
$assigns['pages']['pidPageInfo'] = $pidPageInfo;
|
||||
}
|
||||
$assigns['pages']['pidRecordTitle'] = BackendUtility::getRecordTitle('pages', $pidPageInfo, true);
|
||||
}
|
||||
}
|
||||
// Create the position tree:
|
||||
$assigns['pages']['positionTree'] = $posMap->positionTree($this->page_id, $pageInfo, $this->perms_clause, $this->R_URI);
|
||||
}
|
||||
}
|
||||
// IF the table is "tt_content":
|
||||
if ((string)$this->table === 'tt_content') {
|
||||
// First, get the record:
|
||||
$tt_content_rec = BackendUtility::getRecord('tt_content', $this->moveUid);
|
||||
// ?
|
||||
if (!$this->input_moveUid) {
|
||||
$this->page_id = $tt_content_rec['pid'];
|
||||
}
|
||||
// Checking if the parent page is readable:
|
||||
$pageInfo = BackendUtility::readPageAccess($this->page_id, $this->perms_clause);
|
||||
if (is_array($pageInfo) && $backendUser->isInWebMount($pageInfo['pid'], $this->perms_clause)) {
|
||||
// Initialize the position map:
|
||||
$posMap = GeneralUtility::makeInstance(ContentMovingPagePositionMap::class);
|
||||
$posMap->moveOrCopy = $this->makeCopy ? 'copy' : 'move';
|
||||
$posMap->moveUid = $this->moveUid;
|
||||
$posMap->cur_sys_language = $this->sys_language;
|
||||
// Headerline for the parent page: Icon, record title:
|
||||
$assigns['ttContent']['pageInfo'] = $pageInfo;
|
||||
$assigns['ttContent']['recordTooltip'] = BackendUtility::getRecordToolTip($pageInfo, 'pages');
|
||||
$assigns['ttContent']['recordTitle'] = BackendUtility::getRecordTitle('pages', $pageInfo, true);
|
||||
$colPosArray = GeneralUtility::callUserFunction(\TYPO3\CMS\Backend\View\BackendLayoutView::class . '->getColPosListItemsParsed', $this->page_id, $this);
|
||||
$colPosIds = [];
|
||||
foreach ($colPosArray as $colPos) {
|
||||
$colPosIds[] = $colPos[1];
|
||||
}
|
||||
// Removing duplicates, if any
|
||||
$colPosList = implode(',', array_unique($colPosIds));
|
||||
// Adding parent page-header and the content element columns from position-map:
|
||||
$assigns['ttContent']['contentElementColumns'] = $posMap->printContentElementColumns($this->page_id, $this->moveUid, $colPosList, 1, $this->R_URI);
|
||||
// Print a "go-up" link IF there is a real parent page (and if the user has read-access to that page).
|
||||
if ($pageInfo['pid']) {
|
||||
$pidPageInfo = BackendUtility::readPageAccess($pageInfo['pid'], $this->perms_clause);
|
||||
if (is_array($pidPageInfo)) {
|
||||
if ($backendUser->isInWebMount($pidPageInfo['pid'], $this->perms_clause)) {
|
||||
$assigns['ttContent']['goUpUrl'] = GeneralUtility::linkThisScript([
|
||||
'uid' => (int)$pageInfo['pid'],
|
||||
'moveUid' => $this->moveUid
|
||||
]);
|
||||
} else {
|
||||
$assigns['ttContent']['pidPageInfo'] = $pidPageInfo;
|
||||
}
|
||||
$assigns['ttContent']['pidRecordTitle'] = BackendUtility::getRecordTitle('pages', $pidPageInfo, true);
|
||||
}
|
||||
}
|
||||
// Create the position tree (for pages):
|
||||
$assigns['ttContent']['positionTree'] = $posMap->positionTree($this->page_id, $pageInfo, $this->perms_clause, $this->R_URI);
|
||||
}
|
||||
}
|
||||
// Rendering of the output via fluid
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/ContentElement/MoveElement.html'
|
||||
));
|
||||
$view->assignMultiple($assigns);
|
||||
$this->content .= $view->render();
|
||||
}
|
||||
|
||||
// Setting up the buttons and markers for docheader
|
||||
$this->getButtons();
|
||||
// Build the <body> for the module
|
||||
$this->moduleTemplate->setTitle($lang->getLL('movingElement'));
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
|
||||
$this->content = $this->moduleTemplate->renderContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating the module output.
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('MoveElementController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->renderContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the panel of buttons for submitting the form or otherwise perform operations.
|
||||
*/
|
||||
protected function getButtons()
|
||||
{
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
if ($this->page_id) {
|
||||
if ((string)$this->table === 'pages') {
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('move_el_pages');
|
||||
$buttonBar->addButton($cshButton);
|
||||
} elseif ((string)$this->table === 'tt_content') {
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('move_el_cs');
|
||||
$buttonBar->addButton($cshButton);
|
||||
}
|
||||
|
||||
if ($this->R_URI) {
|
||||
$backButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->R_URI)
|
||||
->setTitle($this->getLanguageService()->getLL('goBack'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
|
||||
'actions-view-go-back',
|
||||
Icon::SIZE_SMALL
|
||||
));
|
||||
$buttonBar->addButton($backButton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
+744
@@ -0,0 +1,744 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\ContentElement;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Template\DocumentTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Tree\View\ContentCreationPagePositionMap;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Backend\Wizard\NewContentElementWizardHookInterface;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Service\DependencyOrderingService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script Class for the New Content element wizard
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class NewContentElementController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'id' => 'Using $id of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'sys_language' => 'Using $sys_language of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'R_URI' => 'Using $R_URI of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'colPos' => 'Using $colPos of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'uid_pid' => 'Using $uid_pid of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'modTSconfig' => 'Using $modTSconfig of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'doc' => 'Using $doc of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'content' => 'Using $content of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'access' => 'Using $access of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
'config' => 'Using $config of NewContentElementController from the outside is discouraged as this variable is used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Page id
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Sys language
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $sys_language = 0;
|
||||
|
||||
/**
|
||||
* Return URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $R_URI = '';
|
||||
|
||||
/**
|
||||
* If set, the content is destined for a specific column.
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $colPos;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $uid_pid;
|
||||
|
||||
/**
|
||||
* Module TSconfig.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $modTSconfig = [];
|
||||
|
||||
/**
|
||||
* Internal backend template object
|
||||
*
|
||||
* @var DocumentTemplate
|
||||
*/
|
||||
protected $doc;
|
||||
|
||||
/**
|
||||
* Used to accumulate the content of the module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* Access boolean.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $access;
|
||||
|
||||
/**
|
||||
* config of the wizard
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pageInfo;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $onClickEvent;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $MCONF;
|
||||
|
||||
/**
|
||||
* @var StandaloneView
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var StandaloneView
|
||||
*/
|
||||
protected $menuItemView;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$GLOBALS['SOBE'] = $this;
|
||||
$this->view = $this->getFluidTemplateObject();
|
||||
$this->menuItemView = $this->getFluidTemplateObject('MenuItem.html');
|
||||
|
||||
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0 with removal of init()
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor, initializing internal variables.
|
||||
*
|
||||
* @param ServerRequestInterface|null $request
|
||||
*/
|
||||
public function init(ServerRequestInterface $request = null)
|
||||
{
|
||||
if ($request === null) {
|
||||
// Method signature in TYPO3 v10.0: protected function init(ServerRequestInterface $request)
|
||||
trigger_error('NewContentElementController->init() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
}
|
||||
|
||||
$lang = $this->getLanguageService();
|
||||
$lang->includeLLFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
$LOCAL_LANG_orig = $GLOBALS['LOCAL_LANG'];
|
||||
$lang->includeLLFile('EXT:backend/Resources/Private/Language/locallang_db_new_content_el.xlf');
|
||||
ArrayUtility::mergeRecursiveWithOverrule($LOCAL_LANG_orig, $GLOBALS['LOCAL_LANG']);
|
||||
$GLOBALS['LOCAL_LANG'] = $LOCAL_LANG_orig;
|
||||
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
// Setting internal vars:
|
||||
$this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
|
||||
$this->sys_language = (int)($parsedBody['sys_language_uid'] ?? $queryParams['sys_language_uid'] ?? 0);
|
||||
$this->R_URI = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
$colPos = $parsedBody['colPos'] ?? $queryParams['colPos'] ?? null;
|
||||
$this->colPos = $colPos === null ? null : (int)$colPos;
|
||||
$this->uid_pid = (int)($parsedBody['uid_pid'] ?? $queryParams['uid_pid'] ?? 0);
|
||||
$this->MCONF['name'] = 'xMOD_db_new_content_el';
|
||||
$this->modTSconfig['properties'] = BackendUtility::getPagesTSconfig($this->id)['mod.']['wizards.']['newContentElement.'] ?? [];
|
||||
$config = BackendUtility::getPagesTSconfig($this->id);
|
||||
$this->config = $config['mod.']['wizards.']['newContentElement.'];
|
||||
// Starting the document template object:
|
||||
// We keep this here in case somebody relies on it in a hook or alike
|
||||
$this->doc = GeneralUtility::makeInstance(DocumentTemplate::class);
|
||||
// Setting up the context sensitive menu:
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
// Getting the current page and receiving access information (used in main())
|
||||
$perms_clause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
$this->pageInfo = BackendUtility::readPageAccess($this->id, $perms_clause);
|
||||
$this->access = is_array($this->pageInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->prepareContent('window');
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function wizardAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->prepareContent('list_frame');
|
||||
return new HtmlResponse($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating the module output.
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3v10 without substitute
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('NewContentElementController->main() will be replaced by protected method prepareContent() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->prepareContent('window');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of elements in the wizard display.
|
||||
* For the plugin section there is support for adding elements there from a global variable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function wizardArray()
|
||||
{
|
||||
trigger_error('NewContentElementController->wizardArray() will be replaced by protected method getWizards() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->getWizards();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $wizardElements
|
||||
* @return array
|
||||
*/
|
||||
public function wizard_appendWizards($wizardElements)
|
||||
{
|
||||
trigger_error('NewContentElementController->wizard_appendWizards() will be replaced by protected method getAppendWizards() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->getAppendWizards($wizardElements);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $groupKey Not used
|
||||
* @param string $itemKey Not used
|
||||
* @param array $itemConf
|
||||
* @return array
|
||||
*/
|
||||
public function wizard_getItem($groupKey, $itemKey, $itemConf)
|
||||
{
|
||||
trigger_error('NewContentElementController->wizard_getItem() will be replaced by protected method getWizardItem() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->getWizardItem($groupKey, $itemKey, $itemConf);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $groupKey Not used
|
||||
* @param array $wizardGroup
|
||||
* @return array
|
||||
*/
|
||||
public function wizard_getGroupHeader($groupKey, $wizardGroup)
|
||||
{
|
||||
trigger_error('NewContentElementController->wizard_getGroupHeader() will be replaced by protected method getWizardGroupHeader() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->getWizardGroupHeader($wizardGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the array for elements which might contain unallowed default values and will unset them!
|
||||
* Looks for the "tt_content_defValues" key in each element and if found it will traverse that array as fieldname /
|
||||
* value pairs and check.
|
||||
* The values will be added to the "params" key of the array (which should probably be unset or empty by default).
|
||||
*
|
||||
* @param array $wizardItems Wizard items, passed by reference
|
||||
*/
|
||||
public function removeInvalidElements(&$wizardItems)
|
||||
{
|
||||
trigger_error('NewContentElementController->removeInvalidElements() will be replaced by protected method removeInvalidWizardItems() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->removeInvalidWizardItems($wizardItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating the module output.
|
||||
*
|
||||
* @param string $clientContext JavaScript client context to be used
|
||||
* + 'window', legacy if rendered in current document
|
||||
* + 'list_frame', in case rendered in global modal
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
protected function prepareContent(string $clientContext): void
|
||||
{
|
||||
$hasAccess = true;
|
||||
if ($this->id && $this->access) {
|
||||
|
||||
// Init position map object:
|
||||
$posMap = GeneralUtility::makeInstance(
|
||||
ContentCreationPagePositionMap::class,
|
||||
null,
|
||||
$clientContext
|
||||
);
|
||||
$posMap->cur_sys_language = $this->sys_language;
|
||||
// If a column is pre-set:
|
||||
if (isset($this->colPos)) {
|
||||
if ($this->uid_pid < 0) {
|
||||
$row = [];
|
||||
$row['uid'] = abs($this->uid_pid);
|
||||
} else {
|
||||
$row = '';
|
||||
}
|
||||
$this->onClickEvent = $posMap->onClickInsertRecord(
|
||||
$row,
|
||||
$this->colPos,
|
||||
'',
|
||||
$this->uid_pid,
|
||||
$this->sys_language
|
||||
);
|
||||
} else {
|
||||
$this->onClickEvent = '';
|
||||
}
|
||||
// ***************************
|
||||
// Creating content
|
||||
// ***************************
|
||||
// Wizard
|
||||
$wizardItems = $this->getWizards();
|
||||
// Wrapper for wizards
|
||||
// Hook for manipulating wizardItems, wrapper, onClickEvent etc.
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms']['db_new_content_el']['wizardItemsHook'] ?? [] as $className) {
|
||||
$hookObject = GeneralUtility::makeInstance($className);
|
||||
if (!$hookObject instanceof NewContentElementWizardHookInterface) {
|
||||
throw new \UnexpectedValueException(
|
||||
$className . ' must implement interface ' . NewContentElementWizardHookInterface::class,
|
||||
1227834741
|
||||
);
|
||||
}
|
||||
$hookObject->manipulateWizardItems($wizardItems, $this);
|
||||
}
|
||||
|
||||
// Traverse items for the wizard.
|
||||
// An item is either a header or an item rendered with a radio button and title/description and icon:
|
||||
$cc = ($key = 0);
|
||||
$menuItems = [];
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'hasClickEvent' => $this->onClickEvent !== '',
|
||||
'onClickEvent' => 'function goToalt_doc() { ' . $this->onClickEvent . '}',
|
||||
]);
|
||||
|
||||
foreach ($wizardItems as $wizardKey => $wInfo) {
|
||||
$wizardOnClick = '';
|
||||
if (isset($wInfo['header'])) {
|
||||
$menuItems[] = [
|
||||
'label' => $wInfo['header'] ?: '-',
|
||||
'content' => ''
|
||||
];
|
||||
$key = count($menuItems) - 1;
|
||||
} else {
|
||||
if (!$this->onClickEvent) {
|
||||
// Radio button:
|
||||
$wizardOnClick = 'document.editForm.defValues.value=unescape(' . GeneralUtility::quoteJSvalue(rawurlencode($wInfo['params'])) . '); window.location.hash=\'#sel2\';';
|
||||
// Onclick action for icon/title:
|
||||
$aOnClick = 'document.getElementsByName(\'tempB\')[' . $cc . '].checked=1;' . $wizardOnClick . 'return false;';
|
||||
} else {
|
||||
$aOnClick = "document.editForm.defValues.value=unescape('" . rawurlencode($wInfo['params']) . "');goToalt_doc();";
|
||||
}
|
||||
|
||||
$icon = $this->moduleTemplate->getIconFactory()->getIcon($wInfo['iconIdentifier'])->render();
|
||||
|
||||
$this->menuItemView->assignMultiple([
|
||||
'onClickEvent' => $this->onClickEvent,
|
||||
'aOnClick' => $aOnClick,
|
||||
'wizardInformation' => $wInfo,
|
||||
'icon' => $icon,
|
||||
'wizardOnClick' => $wizardOnClick,
|
||||
'wizardKey' => $wizardKey
|
||||
]);
|
||||
$menuItems[$key]['content'] .= $this->menuItemView->render();
|
||||
$cc++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->view->assign('renderedTabs', $this->moduleTemplate->getDynamicTabMenu(
|
||||
$menuItems,
|
||||
'new-content-element-wizard'
|
||||
));
|
||||
|
||||
// If the user must also select a column:
|
||||
if (!$this->onClickEvent) {
|
||||
|
||||
// Load SHARED page-TSconfig settings and retrieve column list from there, if applicable:
|
||||
$colPosArray = GeneralUtility::callUserFunction(
|
||||
BackendLayoutView::class . '->getColPosListItemsParsed',
|
||||
$this->id,
|
||||
$this
|
||||
);
|
||||
$colPosIds = array_column($colPosArray, 1);
|
||||
// Removing duplicates, if any
|
||||
$colPosList = implode(',', array_unique(array_map('intval', $colPosIds)));
|
||||
// Finally, add the content of the column selector to the content:
|
||||
$this->view->assign('posMap', $posMap->printContentElementColumns($this->id, 0, $colPosList, 1, $this->R_URI));
|
||||
}
|
||||
} else {
|
||||
// In case of no access:
|
||||
$hasAccess = false;
|
||||
}
|
||||
$this->view->assign('hasAccess', $hasAccess);
|
||||
|
||||
$this->content = $this->view->render();
|
||||
// Setting up the buttons and markers for docheader
|
||||
$this->getButtons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the panel of buttons for submitting the form or otherwise perform operations.
|
||||
*/
|
||||
protected function getButtons()
|
||||
{
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
if ($this->R_URI) {
|
||||
$backButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->R_URI)
|
||||
->setTitle($this->getLanguageService()->getLL('goBack'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
|
||||
'actions-view-go-back',
|
||||
Icon::SIZE_SMALL
|
||||
));
|
||||
$buttonBar->addButton($backButton);
|
||||
}
|
||||
$cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('new_ce');
|
||||
$buttonBar->addButton($cshButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the array of elements in the wizard display.
|
||||
* For the plugin section there is support for adding elements there from a global variable.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getWizards(): array
|
||||
{
|
||||
$wizardItems = [];
|
||||
if (is_array($this->config)) {
|
||||
$wizards = $this->config['wizardItems.'] ?? [];
|
||||
$appendWizards = $this->getAppendWizards($wizards['elements.'] ?? []);
|
||||
if (is_array($wizards)) {
|
||||
foreach ($wizards as $groupKey => $wizardGroup) {
|
||||
$this->prepareDependencyOrdering($wizards[$groupKey], 'before');
|
||||
$this->prepareDependencyOrdering($wizards[$groupKey], 'after');
|
||||
}
|
||||
$wizards = GeneralUtility::makeInstance(DependencyOrderingService::class)->orderByDependencies($wizards);
|
||||
|
||||
foreach ($wizards as $groupKey => $wizardGroup) {
|
||||
$groupKey = rtrim($groupKey, '.');
|
||||
$showItems = GeneralUtility::trimExplode(',', $wizardGroup['show'], true);
|
||||
$showAll = in_array('*', $showItems, true);
|
||||
$groupItems = [];
|
||||
if (is_array($appendWizards[$groupKey . '.']['elements.'])) {
|
||||
$wizardElements = array_merge((array)$wizardGroup['elements.'], $appendWizards[$groupKey . '.']['elements.']);
|
||||
} else {
|
||||
$wizardElements = $wizardGroup['elements.'];
|
||||
}
|
||||
if (is_array($wizardElements)) {
|
||||
foreach ($wizardElements as $itemKey => $itemConf) {
|
||||
$itemKey = rtrim($itemKey, '.');
|
||||
if ($showAll || in_array($itemKey, $showItems)) {
|
||||
$tmpItem = $this->getWizardItem($groupKey, $itemKey, $itemConf);
|
||||
if ($tmpItem) {
|
||||
$groupItems[$groupKey . '_' . $itemKey] = $tmpItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!empty($groupItems)) {
|
||||
$wizardItems[$groupKey] = $this->getWizardGroupHeader($wizardGroup);
|
||||
$wizardItems = array_merge($wizardItems, $groupItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove elements where preset values are not allowed:
|
||||
$this->removeInvalidWizardItems($wizardItems);
|
||||
return $wizardItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $wizardElements
|
||||
* @return array
|
||||
*/
|
||||
protected function getAppendWizards(array $wizardElements): array
|
||||
{
|
||||
if (!is_array($wizardElements)) {
|
||||
$wizardElements = [];
|
||||
}
|
||||
if (is_array($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'])) {
|
||||
foreach ($GLOBALS['TBE_MODULES_EXT']['xMOD_db_new_content_el']['addElClasses'] as $class => $path) {
|
||||
if (!class_exists($class) && file_exists($path)) {
|
||||
require_once $path;
|
||||
}
|
||||
$modObj = GeneralUtility::makeInstance($class);
|
||||
if (method_exists($modObj, 'proc')) {
|
||||
$wizardElements = $modObj->proc($wizardElements);
|
||||
}
|
||||
}
|
||||
}
|
||||
$returnElements = [];
|
||||
foreach ($wizardElements as $key => $wizardItem) {
|
||||
preg_match('/^[a-zA-Z0-9]+_/', $key, $group);
|
||||
$wizardGroup = $group[0] ? substr($group[0], 0, -1) . '.' : $key;
|
||||
$returnElements[$wizardGroup]['elements.'][substr($key, strlen($wizardGroup)) . '.'] = $wizardItem;
|
||||
}
|
||||
return $returnElements;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $groupKey Not used
|
||||
* @param string $itemKey Not used
|
||||
* @param array $itemConf
|
||||
* @return array
|
||||
*/
|
||||
protected function getWizardItem(string $groupKey, string $itemKey, array $itemConf): array
|
||||
{
|
||||
$itemConf['title'] = $this->getLanguageService()->sL($itemConf['title']);
|
||||
$itemConf['description'] = $this->getLanguageService()->sL($itemConf['description']);
|
||||
$itemConf['tt_content_defValues'] = $itemConf['tt_content_defValues.'];
|
||||
unset($itemConf['tt_content_defValues.']);
|
||||
return $itemConf;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $wizardGroup
|
||||
* @return array
|
||||
*/
|
||||
protected function getWizardGroupHeader(array $wizardGroup): array
|
||||
{
|
||||
return [
|
||||
'header' => $this->getLanguageService()->sL($wizardGroup['header'])
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the array for elements which might contain unallowed default values and will unset them!
|
||||
* Looks for the "tt_content_defValues" key in each element and if found it will traverse that array as fieldname /
|
||||
* value pairs and check.
|
||||
* The values will be added to the "params" key of the array (which should probably be unset or empty by default).
|
||||
*
|
||||
* @param array $wizardItems Wizard items, passed by reference
|
||||
*/
|
||||
protected function removeInvalidWizardItems(array &$wizardItems): void
|
||||
{
|
||||
// Get TCEFORM from TSconfig of current page
|
||||
$row = ['pid' => $this->id];
|
||||
$TCEFORM_TSconfig = BackendUtility::getTCEFORM_TSconfig('tt_content', $row);
|
||||
$headersUsed = [];
|
||||
// Traverse wizard items:
|
||||
foreach ($wizardItems as $key => $cfg) {
|
||||
// Exploding parameter string, if any (old style)
|
||||
if ($wizardItems[$key]['params']) {
|
||||
// Explode GET vars recursively
|
||||
$tempGetVars = [];
|
||||
parse_str($wizardItems[$key]['params'], $tempGetVars);
|
||||
// If tt_content values are set, merge them into the tt_content_defValues array,
|
||||
// unset them from $tempGetVars and re-implode $tempGetVars into the param string
|
||||
// (in case remaining parameters are around).
|
||||
if (is_array($tempGetVars['defVals']['tt_content'])) {
|
||||
$wizardItems[$key]['tt_content_defValues'] = array_merge(
|
||||
is_array($wizardItems[$key]['tt_content_defValues']) ? $wizardItems[$key]['tt_content_defValues'] : [],
|
||||
$tempGetVars['defVals']['tt_content']
|
||||
);
|
||||
unset($tempGetVars['defVals']['tt_content']);
|
||||
$wizardItems[$key]['params'] = HttpUtility::buildQueryString($tempGetVars, '&');
|
||||
}
|
||||
}
|
||||
// If tt_content_defValues are defined...:
|
||||
if (is_array($wizardItems[$key]['tt_content_defValues'])) {
|
||||
$backendUser = $this->getBackendUser();
|
||||
// Traverse field values:
|
||||
foreach ($wizardItems[$key]['tt_content_defValues'] as $fN => $fV) {
|
||||
if (is_array($GLOBALS['TCA']['tt_content']['columns'][$fN])) {
|
||||
// Get information about if the field value is OK:
|
||||
$config = &$GLOBALS['TCA']['tt_content']['columns'][$fN]['config'];
|
||||
$authModeDeny = $config['type'] === 'select' && $config['authMode']
|
||||
&& !$backendUser->checkAuthMode('tt_content', $fN, $fV, $config['authMode']);
|
||||
// explode TSconfig keys only as needed
|
||||
if (!isset($removeItems[$fN]) && isset($TCEFORM_TSconfig[$fN]['removeItems']) && $TCEFORM_TSconfig[$fN]['removeItems'] !== '') {
|
||||
$removeItems[$fN] = array_flip(GeneralUtility::trimExplode(
|
||||
',',
|
||||
$TCEFORM_TSconfig[$fN]['removeItems'],
|
||||
true
|
||||
));
|
||||
}
|
||||
if (!isset($keepItems[$fN]) && isset($TCEFORM_TSconfig[$fN]['keepItems']) && $TCEFORM_TSconfig[$fN]['keepItems'] !== '') {
|
||||
$keepItems[$fN] = array_flip(GeneralUtility::trimExplode(
|
||||
',',
|
||||
$TCEFORM_TSconfig[$fN]['keepItems'],
|
||||
true
|
||||
));
|
||||
}
|
||||
$isNotInKeepItems = !empty($keepItems[$fN]) && !isset($keepItems[$fN][$fV]);
|
||||
if ($authModeDeny || ($fN === 'CType' && (isset($removeItems[$fN][$fV]) || $isNotInKeepItems))) {
|
||||
// Remove element all together:
|
||||
unset($wizardItems[$key]);
|
||||
break;
|
||||
}
|
||||
// Add the parameter:
|
||||
$wizardItems[$key]['params'] .= '&defVals[tt_content][' . $fN . ']=' . rawurlencode($this->getLanguageService()->sL($fV));
|
||||
$tmp = explode('_', $key);
|
||||
$headersUsed[$tmp[0]] = $tmp[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// remove headers without elements
|
||||
foreach ($wizardItems as $key => $cfg) {
|
||||
$tmp = explode('_', $key);
|
||||
if ($tmp[0] && !$tmp[1] && !in_array($tmp[0], $headersUsed)) {
|
||||
unset($wizardItems[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare a wizard tab configuration for sorting.
|
||||
*
|
||||
* @param array $wizardGroup TypoScript wizard tab configuration
|
||||
* @param string $key Which array key should be prepared
|
||||
*/
|
||||
protected function prepareDependencyOrdering(&$wizardGroup, $key)
|
||||
{
|
||||
if (isset($wizardGroup[$key])) {
|
||||
$wizardGroup[$key] = GeneralUtility::trimExplode(',', $wizardGroup[$key]);
|
||||
$wizardGroup[$key] = array_map(function ($s) {
|
||||
return $s . '.';
|
||||
}, $wizardGroup[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $filename
|
||||
* @return StandaloneView
|
||||
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $filename = 'Main.html'): StandaloneView
|
||||
{
|
||||
/** @var StandaloneView $view */
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/NewContentElement/' . $filename));
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide information about the current page making use of the wizard
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPageInfo(): array
|
||||
{
|
||||
return $this->pageInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide information about the column position of the button that triggered the wizard
|
||||
*
|
||||
* @return int|null
|
||||
*/
|
||||
public function getColPos(): ?int
|
||||
{
|
||||
return $this->colPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide information about the language used while triggering the wizard
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getSysLanguage(): int
|
||||
{
|
||||
return $this->sys_language;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide information about the element to position the new element after (uid) or into (pid)
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getUidPid(): int
|
||||
{
|
||||
return $this->uid_pid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class ContextHelpAjaxController
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ContextHelpAjaxController
|
||||
{
|
||||
/**
|
||||
* The main dispatcher function. Collect data and prepare HTML output.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getHelpAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getParsedBody()['params'] ?? $request->getQueryParams()['params'];
|
||||
if (($params['action'] ?? '') !== 'getContextHelp') {
|
||||
throw new \RuntimeException('Action must be set to "getContextHelp"', 1518787887);
|
||||
}
|
||||
$result = $this->getContextHelp($params['table'], $params['field']);
|
||||
return new JsonResponse([
|
||||
'title' => $result['title'],
|
||||
'content' => $result['description'],
|
||||
'link' => $result['moreInfo']
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the context help for the given table/field parameters
|
||||
*
|
||||
* @param string $table Table identifier
|
||||
* @param string $field Field identifier
|
||||
* @return array complete Help information
|
||||
*/
|
||||
protected function getContextHelp($table, $field)
|
||||
{
|
||||
$helpTextArray = BackendUtility::helpTextArray($table, $field);
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$moreIcon = $helpTextArray['moreInfo'] ? $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render() : '';
|
||||
return [
|
||||
'title' => $helpTextArray['title'],
|
||||
'description' => '<p class="t3-help-short' . ($moreIcon ? ' tipIsLinked' : '') . '">' . $helpTextArray['description'] . $moreIcon . '</p>',
|
||||
'id' => $table . '.' . $field,
|
||||
'moreInfo' => $helpTextArray['moreInfo']
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Clipboard\Clipboard;
|
||||
use TYPO3\CMS\Backend\ContextMenu\ContextMenu;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Script Class for the Context Sensitive Menu in TYPO3
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ContextMenuController
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a context menu
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getContextMenuAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$contextMenu = GeneralUtility::makeInstance(ContextMenu::class);
|
||||
|
||||
$params = $request->getQueryParams();
|
||||
$context = $params['context'] ?? '';
|
||||
$items = $contextMenu->getItems($params['table'], $params['uid'], $context);
|
||||
if (!is_array($items)) {
|
||||
$items = [];
|
||||
}
|
||||
return (new JsonResponse())->setPayload($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function clipboardAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
/** @var Clipboard $clipboard */
|
||||
$clipboard = GeneralUtility::makeInstance(Clipboard::class);
|
||||
$clipboard->initializeClipboard();
|
||||
$clipboard->lockToNormal();
|
||||
|
||||
$clipboard->setCmd($request->getQueryParams()['CB']);
|
||||
$clipboard->cleanCurrent();
|
||||
|
||||
$clipboard->endClipboard();
|
||||
return (new JsonResponse())->setPayload([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Backend\Template\DocumentTemplate;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
|
||||
/**
|
||||
* '/empty' routing target returns dummy content.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class DummyController
|
||||
{
|
||||
/**
|
||||
* Return simple dummy content
|
||||
*
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(): ResponseInterface
|
||||
{
|
||||
$documentTemplate = $this->getDocumentTemplate();
|
||||
$content = $documentTemplate->startPage('Dummy document') . $documentTemplate->endPage();
|
||||
return new HtmlResponse($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of DocumentTemplate
|
||||
*
|
||||
* @return DocumentTemplate
|
||||
*/
|
||||
protected function getDocumentTemplate()
|
||||
{
|
||||
return $GLOBALS['TBE_TEMPLATE'];
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\File;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script class for the create-new script
|
||||
*
|
||||
* Displays forms for creating folders (1 to 10), a media asset or a new file.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class CreateFolderController
|
||||
{
|
||||
use PublicMethodDeprecationTrait;
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicMethods = [
|
||||
'main' => 'Using CreateFolderController::main() is deprecated and will not be possible anymore in TYPO3 v10.0.',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicProperties = [
|
||||
'number' => 'Using $number of class CreateFolderController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'folderNumber' => 'Using $folderNumber of class CreateFolderController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'target' => 'Using $target of class CreateFolderController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'content' => 'Using $content of class CreateFolderController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'returnUrl' => 'Using $content of class CreateFolderController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'title' => 'Using $content of class CreateFolderController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $folderNumber = 10;
|
||||
|
||||
/**
|
||||
* Name of the filemount
|
||||
*
|
||||
* @var string
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $number;
|
||||
|
||||
/**
|
||||
* Set with the target path inputted in &target
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* The folder object which is the target directory
|
||||
*
|
||||
* @var \TYPO3\CMS\Core\Resource\Folder $folderObject
|
||||
*/
|
||||
protected $folderObject;
|
||||
|
||||
/**
|
||||
* Return URL of list module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $returnUrl;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pathInfo;
|
||||
|
||||
/**
|
||||
* Accumulating content
|
||||
*
|
||||
* @var string
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the request, currently everything is handled and put together via "main()"
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->main();
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface|null $request
|
||||
*
|
||||
* @throws InsufficientFolderAccessPermissionsException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$this->number = $parsedBody['number'] ?? $queryParams['number'] ?? 0;
|
||||
$this->target = ($combinedIdentifier = $parsedBody['target'] ?? $queryParams['target'] ?? '');
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
// create the folder object
|
||||
if ($combinedIdentifier) {
|
||||
$this->folderObject = ResourceFactory::getInstance()
|
||||
->getFolderObjectFromCombinedIdentifier($combinedIdentifier);
|
||||
}
|
||||
// Cleaning and checking target directory
|
||||
if (!$this->folderObject) {
|
||||
$title = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:paramError');
|
||||
$message = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:targetNoDir');
|
||||
throw new \RuntimeException($title . ': ' . $message, 1294586845);
|
||||
}
|
||||
if ($this->folderObject->getStorage()->getUid() === 0) {
|
||||
throw new InsufficientFolderAccessPermissionsException(
|
||||
'You are not allowed to access folders outside your storages',
|
||||
1375889838
|
||||
);
|
||||
}
|
||||
|
||||
$pathInfo = [
|
||||
'combined_identifier' => $this->folderObject->getCombinedIdentifier(),
|
||||
];
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pathInfo);
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
$this->moduleTemplate->addJavaScriptCode(
|
||||
'CreateFolderInlineJavaScript',
|
||||
'var path = "' . $this->target . '";
|
||||
var confirmTitle = '
|
||||
. GeneralUtility::quoteJSvalue(
|
||||
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:pleaseConfirm')
|
||||
)
|
||||
. ';
|
||||
var confirmText = '
|
||||
. GeneralUtility::quoteJSvalue(
|
||||
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:mess.redraw')
|
||||
)
|
||||
. ';
|
||||
function reload(a) {
|
||||
var params = "&target="+encodeURIComponent(path)+"&number="+a+"&returnUrl=' . rawurlencode($this->returnUrl) . '";
|
||||
var url = \'' . (string)$uriBuilder->buildUriFromRoute('file_newfolder') . '\';
|
||||
if (!changed) {
|
||||
window.location.href = url + params;
|
||||
} else {
|
||||
var modal = top.TYPO3.Modal.confirm(confirmTitle, confirmText);
|
||||
modal.on(\'confirm.button.cancel\', function(e) {
|
||||
top.TYPO3.Modal.currentModal.trigger(\'modal-dismiss\');
|
||||
});
|
||||
modal.on(\'confirm.button.ok\', function(e) {
|
||||
top.TYPO3.Modal.currentModal.trigger(\'modal-dismiss\');
|
||||
window.location.href = url + params;
|
||||
});
|
||||
}
|
||||
}
|
||||
function backToList() {
|
||||
top.goToModule("file_FilelistList");
|
||||
}
|
||||
var changed = 0;'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the main module content
|
||||
*/
|
||||
protected function main()
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
$assigns = [];
|
||||
$assigns['target'] = $this->target;
|
||||
if ($this->folderObject->checkActionPermission('add')) {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$assigns['moduleUrlTceFile'] = (string)$uriBuilder->buildUriFromRoute('tce_file');
|
||||
$assigns['cshFileNewFolder'] = BackendUtility::cshItem('xMOD_csh_corebe', 'file_newfolder');
|
||||
// Making the selector box for the number of concurrent folder-creations
|
||||
$this->number = MathUtility::forceIntegerInRange($this->number, 1, 10);
|
||||
for ($a = 1; $a <= $this->folderNumber; $a++) {
|
||||
$options = [];
|
||||
$options['value'] = $a;
|
||||
$options['selected'] = ($this->number == $a ? ' selected="selected"' : '');
|
||||
$assigns['options'][] = $options;
|
||||
}
|
||||
// Making the number of new-folder boxes needed:
|
||||
for ($a = 0; $a < $this->number; $a++) {
|
||||
$folder = [];
|
||||
$folder['this'] = $a;
|
||||
$folder['next'] = $a + 1;
|
||||
$assigns['folders'][] = $folder;
|
||||
}
|
||||
// Making submit button for folder creation:
|
||||
$assigns['returnUrl'] = $this->returnUrl;
|
||||
}
|
||||
|
||||
if ($this->folderObject->getStorage()->checkUserActionPermission('add', 'File')) {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$assigns['moduleUrlOnlineMedia'] = (string)$uriBuilder->buildUriFromRoute('online_media');
|
||||
$assigns['cshFileNewMedia'] = BackendUtility::cshItem('xMOD_csh_corebe', 'file_newMedia');
|
||||
// Create a list of allowed file extensions with the readable format "youtube, vimeo" etc.
|
||||
$fileExtList = [];
|
||||
$onlineMediaFileExt = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
|
||||
foreach ($onlineMediaFileExt as $fileExt) {
|
||||
if (GeneralUtility::verifyFilenameAgainstDenyPattern('.' . $fileExt)) {
|
||||
$fileExtList[] = strtoupper(htmlspecialchars($fileExt));
|
||||
}
|
||||
}
|
||||
$assigns['fileExtList'] = $fileExtList;
|
||||
|
||||
$assigns['moduleUrlTceFile'] = (string)$uriBuilder->buildUriFromRoute('tce_file');
|
||||
$assigns['cshFileNewFile'] = BackendUtility::cshItem('xMOD_csh_corebe', 'file_newfile');
|
||||
// Create a list of allowed file extensions with a text format "*.txt, *.css" etc.
|
||||
$fileExtList = [];
|
||||
$textFileExt = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'], true);
|
||||
foreach ($textFileExt as $fileExt) {
|
||||
if (GeneralUtility::verifyFilenameAgainstDenyPattern('.' . $fileExt)) {
|
||||
$fileExtList[] = strtoupper(htmlspecialchars($fileExt));
|
||||
}
|
||||
}
|
||||
$assigns['txtFileExtList'] = $fileExtList;
|
||||
}
|
||||
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
// CSH button
|
||||
$helpButton = $buttonBar->makeHelpButton()
|
||||
->setFieldName('file_new')
|
||||
->setModuleName('xMOD_csh_corebe');
|
||||
$buttonBar->addButton($helpButton);
|
||||
|
||||
// Back
|
||||
if ($this->returnUrl) {
|
||||
$backButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->returnUrl)
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($backButton);
|
||||
}
|
||||
|
||||
// Rendering of the output via fluid
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/File/CreateFolder.html'
|
||||
));
|
||||
$view->assignMultiple($assigns);
|
||||
$this->moduleTemplate->setContent($view->render());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\File;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Form\FormResultCompiler;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\DocumentTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script Class for rendering the file editing screen
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class EditFileController
|
||||
{
|
||||
use PublicMethodDeprecationTrait;
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicMethods = [
|
||||
'main' => 'Using EditFileController::main() is deprecated and will not be possible anymore in TYPO3 v10.0.',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicProperties = [
|
||||
'origTarget' => 'Using $origTarget of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'target' => 'Using $target of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'returnUrl' => 'Using $returnUrl of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'content' => 'Using $content of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'title' => 'Using $title of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
'doc' => 'Using $doc of class EditFileController from outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
/**
|
||||
* Module content accumulated.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* Document template object
|
||||
*
|
||||
* @var DocumentTemplate
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
|
||||
*/
|
||||
protected $doc;
|
||||
|
||||
/**
|
||||
* Original input target
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $origTarget;
|
||||
|
||||
/**
|
||||
* The original target, but validated.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* Return URL of list module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $returnUrl;
|
||||
|
||||
/**
|
||||
* the file that is being edited on
|
||||
*
|
||||
* @var \TYPO3\CMS\Core\Resource\AbstractFile
|
||||
*/
|
||||
protected $fileObject;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the request, currently everything is handled and put together via "main()"
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($response = $this->process()) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the actual content of the editing page
|
||||
*/
|
||||
protected function main()
|
||||
{
|
||||
$response = $this->process();
|
||||
|
||||
if ($response instanceof RedirectResponse) {
|
||||
HttpUtility::redirect($response->getHeaderLine('location'), $response->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize script class
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*
|
||||
* @throws InsufficientFileAccessPermissionsException
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
// Setting target, which must be a file reference to a file within the mounts.
|
||||
$this->target = $this->origTarget = $parsedBody['target'] ?? $queryParams['target'] ?? '';
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
// create the file object
|
||||
if ($this->target) {
|
||||
$this->fileObject = ResourceFactory::getInstance()
|
||||
->retrieveFileOrFolderObject($this->target);
|
||||
}
|
||||
// Cleaning and checking target directory
|
||||
if (!$this->fileObject) {
|
||||
$title = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:paramError');
|
||||
$message = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:targetNoDir');
|
||||
throw new \RuntimeException($title . ': ' . $message, 1294586841);
|
||||
}
|
||||
if ($this->fileObject->getStorage()->getUid() === 0) {
|
||||
throw new InsufficientFileAccessPermissionsException(
|
||||
'You are not allowed to access files outside your storages',
|
||||
1375889832
|
||||
);
|
||||
}
|
||||
|
||||
// Setting the title and the icon
|
||||
$icon = $this->moduleTemplate->getIconFactory()->getIcon('apps-filetree-root', Icon::SIZE_SMALL)->render();
|
||||
$this->title = $icon
|
||||
. htmlspecialchars(
|
||||
$this->fileObject->getStorage()->getName()
|
||||
) . ': ' . htmlspecialchars(
|
||||
$this->fileObject->getIdentifier()
|
||||
);
|
||||
|
||||
// Setting template object
|
||||
$this->moduleTemplate->addJavaScriptCode(
|
||||
'FileEditBackToList',
|
||||
'function backToList() {
|
||||
top.goToModule("file_FilelistList");
|
||||
}'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the actual content of the editing page
|
||||
*
|
||||
* @return ResponseInterface|null Possible redirect response
|
||||
*/
|
||||
protected function process(): ?ResponseInterface
|
||||
{
|
||||
$dataColumnDefinition = [
|
||||
'label' => htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:file'))
|
||||
. ' ' . htmlspecialchars($this->target),
|
||||
'config' => [
|
||||
'type' => 'text',
|
||||
'cols' => 48,
|
||||
'wrap' => 'OFF',
|
||||
],
|
||||
'defaultExtras' => 'fixed-font: enable-tab'
|
||||
];
|
||||
|
||||
$this->getButtonsInternal();
|
||||
// Hook: before compiling the output
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/file_edit.php']['preOutputProcessingHook'] ?? [] as $hookFunction) {
|
||||
$hookParameters = [
|
||||
'content' => &$this->content,
|
||||
'target' => &$this->target,
|
||||
'dataColumnDefinition' => &$dataColumnDefinition,
|
||||
];
|
||||
GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
|
||||
}
|
||||
|
||||
$assigns = [];
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$assigns['moduleUrlTceFile'] = (string)$uriBuilder->buildUriFromRoute('tce_file');
|
||||
$assigns['fileName'] = $this->fileObject->getName();
|
||||
|
||||
$extList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'];
|
||||
try {
|
||||
if (!$extList || !GeneralUtility::inList($extList, $this->fileObject->getExtension())) {
|
||||
// @todo throw a minor exception here, not the global one
|
||||
throw new \Exception('Files with that extension are not editable. Allowed extensions are: ' . $extList, 1476050135);
|
||||
}
|
||||
|
||||
// Making the formfields
|
||||
$hValue = (string)$uriBuilder->buildUriFromRoute('file_edit', [
|
||||
'target' => $this->origTarget,
|
||||
'returnUrl' => $this->returnUrl
|
||||
]);
|
||||
|
||||
$formData = [
|
||||
'databaseRow' => [
|
||||
'uid' => 0,
|
||||
'data' => $this->fileObject->getContents(),
|
||||
'target' => $this->fileObject->getUid(),
|
||||
'redirect' => $hValue,
|
||||
],
|
||||
'tableName' => 'editfile',
|
||||
'processedTca' => [
|
||||
'columns' => [
|
||||
'data' => $dataColumnDefinition,
|
||||
'target' => [
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'renderType' => 'hidden',
|
||||
],
|
||||
],
|
||||
'redirect' => [
|
||||
'config' => [
|
||||
'type' => 'input',
|
||||
'renderType' => 'hidden',
|
||||
],
|
||||
],
|
||||
],
|
||||
'types' => [
|
||||
1 => [
|
||||
'showitem' => 'data,target,redirect',
|
||||
],
|
||||
],
|
||||
],
|
||||
'recordTypeValue' => 1,
|
||||
'inlineStructure' => [],
|
||||
'renderType' => 'fullRecordContainer',
|
||||
];
|
||||
|
||||
$resultArray = GeneralUtility::makeInstance(NodeFactory::class)->create($formData)->render();
|
||||
$formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
|
||||
$formResultCompiler->mergeResult($resultArray);
|
||||
|
||||
$form = $formResultCompiler->addCssFiles()
|
||||
. $resultArray['html']
|
||||
. $formResultCompiler->printNeededJSFunctions();
|
||||
|
||||
$assigns['form'] = $form;
|
||||
} catch (\Exception $e) {
|
||||
// @todo catch dedicated exceptions, not the global one, if possible
|
||||
$flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $e->getMessage(), '', FlashMessage::ERROR, true);
|
||||
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$defaultFlashMessageQueue->enqueue($flashMessage);
|
||||
|
||||
return new RedirectResponse($this->returnUrl, 500);
|
||||
}
|
||||
|
||||
// Rendering of the output via fluid
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/File/EditFile.html'
|
||||
));
|
||||
$view->assignMultiple($assigns);
|
||||
$pageContent = $view->render();
|
||||
|
||||
// Hook: after compiling the output
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['typo3/file_edit.php']['postOutputProcessingHook'] ?? [] as $hookFunction) {
|
||||
$hookParameters = [
|
||||
'pageContent' => &$pageContent,
|
||||
'target' => &$this->target
|
||||
];
|
||||
GeneralUtility::callUserFunction($hookFunction, $hookParameters, $this);
|
||||
}
|
||||
|
||||
$this->content .= $pageContent;
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the buttons for the docheader and returns them as an
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be set protected in TYPO3 v10.0
|
||||
*/
|
||||
public function getButtons()
|
||||
{
|
||||
trigger_error('EditFileController->getButtons() will be replaced by protected method getButtonsInternal() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->getButtonsInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the buttons for the docheader
|
||||
*/
|
||||
protected function getButtonsInternal(): void
|
||||
{
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
|
||||
$lang = $this->getLanguageService();
|
||||
// CSH button
|
||||
$helpButton = $buttonBar->makeHelpButton()
|
||||
->setFieldName('file_edit')
|
||||
->setModuleName('xMOD_csh_corebe');
|
||||
$buttonBar->addButton($helpButton);
|
||||
|
||||
// Save button
|
||||
$saveButton = $buttonBar->makeInputButton()
|
||||
->setName('_save')
|
||||
->setValue('1')
|
||||
->setForm('EditFileController')
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_edit.php.submit'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL));
|
||||
|
||||
// Save and Close button
|
||||
$saveAndCloseButton = $buttonBar->makeInputButton()
|
||||
->setName('_saveandclosedok')
|
||||
->setValue('1')
|
||||
->setForm('EditFileController')
|
||||
->setOnClick(
|
||||
'document.editform.elements.namedItem("data[editfile][0][redirect]").value='
|
||||
. GeneralUtility::quoteJSvalue($this->returnUrl)
|
||||
. ';'
|
||||
)
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_edit.php.saveAndClose'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
|
||||
'actions-document-save-close',
|
||||
Icon::SIZE_SMALL
|
||||
));
|
||||
|
||||
$splitButton = $buttonBar->makeSplitButton()
|
||||
->addItem($saveButton)
|
||||
->addItem($saveAndCloseButton);
|
||||
$buttonBar->addButton($splitButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
|
||||
|
||||
// Cancel button
|
||||
$closeButton = $buttonBar->makeLinkButton()
|
||||
->setHref('#')
|
||||
->setOnClick('backToList(); return false;')
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.cancel'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
|
||||
|
||||
// Make shortcut:
|
||||
$shortButton = $buttonBar->makeShortcutButton()
|
||||
->setModuleName('file_edit')
|
||||
->setGetVariables(['target']);
|
||||
$buttonBar->addButton($shortButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\File;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Clipboard\Clipboard;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Resource\DuplicationBehavior;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
|
||||
/**
|
||||
* Gateway for TCE (TYPO3 Core Engine) file-handling through POST forms.
|
||||
* This script serves as the file administration part of the TYPO3 Core Engine.
|
||||
* Basically it includes two libraries which are used to manipulate files on the server.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class FileController
|
||||
{
|
||||
use PublicMethodDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $deprecatedPublicMethods = [
|
||||
'initClipboard' => 'Using FileController::initClipboard() is deprecated and will not be possible anymore in TYPO3 v10.0.',
|
||||
'main' => 'Using FileController::main() is deprecated and will not be possible anymore in TYPO3 v10.0.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Array of file-operations.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $file;
|
||||
|
||||
/**
|
||||
* Clipboard operations array
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $CB;
|
||||
|
||||
/**
|
||||
* Defines behaviour when uploading files with names that already exist; possible values are
|
||||
* the values of the \TYPO3\CMS\Core\Resource\DuplicationBehavior enumeration
|
||||
*
|
||||
* @var \TYPO3\CMS\Core\Resource\DuplicationBehavior
|
||||
*/
|
||||
protected $overwriteExistingFiles;
|
||||
|
||||
/**
|
||||
* The page where the user should be redirected after everything is done
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirect;
|
||||
|
||||
/**
|
||||
* Internal, dynamic:
|
||||
* File processor object
|
||||
*
|
||||
* @var ExtendedFileUtility
|
||||
*/
|
||||
protected $fileProcessor;
|
||||
|
||||
/**
|
||||
* The result array from the file processor
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fileData;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it just redirects to the given URL afterwards.
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->main();
|
||||
|
||||
BackendUtility::setUpdateSignal('updateFolderTree');
|
||||
|
||||
// go and edit the new created file
|
||||
if ($request->getParsedBody()['edit'] ?? '') {
|
||||
/** @var File $file */
|
||||
$file = $this->fileData['newfile'][0];
|
||||
if ($file !== null) {
|
||||
$this->redirect = $this->getFileEditRedirect($file) ?? $this->redirect;
|
||||
}
|
||||
}
|
||||
if ($this->redirect) {
|
||||
return new RedirectResponse(
|
||||
GeneralUtility::locationHeaderUrl($this->redirect),
|
||||
303
|
||||
);
|
||||
}
|
||||
// empty response
|
||||
return new HtmlResponse('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the actual process from within the ajaxExec function
|
||||
* therefore, it does exactly the same as the real typo3/tce_file.php
|
||||
* but without calling the "finish" method, thus makes it simpler to deal with the
|
||||
* actual return value
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function processAjaxRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->main();
|
||||
$errors = $this->fileProcessor->getErrorMessages();
|
||||
if (!empty($errors)) {
|
||||
return (new HtmlResponse('<t3err>' . implode(',', $errors) . '</t3err>'))->withStatus(500, '(AJAX)');
|
||||
}
|
||||
$flatResult = [];
|
||||
foreach ($this->fileData as $action => $results) {
|
||||
foreach ($results as $result) {
|
||||
if (is_array($result)) {
|
||||
foreach ($result as $subResult) {
|
||||
$flatResult[$action][] = $this->flattenResultDataValue($subResult);
|
||||
}
|
||||
} else {
|
||||
$flatResult[$action][] = $this->flattenResultDataValue($result);
|
||||
}
|
||||
}
|
||||
}
|
||||
return (new JsonResponse())->setPayload($flatResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajax entry point to check if a file exists in a folder
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function fileExistsInFolderAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$fileName = $request->getParsedBody()['fileName'] ?? $request->getQueryParams()['fileName'] ?? null;
|
||||
$fileTarget = $request->getParsedBody()['fileTarget'] ?? $request->getQueryParams()['fileTarget'] ?? null;
|
||||
|
||||
$fileFactory = GeneralUtility::makeInstance(ResourceFactory::class);
|
||||
$fileTargetObject = $fileFactory->retrieveFileOrFolderObject($fileTarget);
|
||||
$processedFileName = $fileTargetObject->getStorage()->sanitizeFileName($fileName, $fileTargetObject);
|
||||
|
||||
$result = [];
|
||||
if ($fileTargetObject->hasFile($processedFileName)) {
|
||||
$result = $this->flattenResultDataValue($fileTargetObject->getStorage()->getFileInFolder($processedFileName, $fileTargetObject));
|
||||
}
|
||||
return (new JsonResponse())->setPayload($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registering incoming data
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
// Set the GPvars from outside
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$this->file = $parsedBody['data'] ?? $queryParams['data'] ?? null;
|
||||
if ($this->file === null) {
|
||||
// This happens in clipboard mode only
|
||||
$this->redirect = GeneralUtility::sanitizeLocalUrl($parsedBody['redirect'] ?? $queryParams['redirect'] ?? '');
|
||||
} else {
|
||||
$mode = key($this->file);
|
||||
$elementKey = key($this->file[$mode]);
|
||||
$this->redirect = GeneralUtility::sanitizeLocalUrl($this->file[$mode][$elementKey]['redirect']);
|
||||
}
|
||||
$this->CB = $parsedBody['CB'] ?? $queryParams['CB'] ?? null;
|
||||
|
||||
if (isset($this->file['rename'][0]['conflictMode'])) {
|
||||
$conflictMode = $this->file['rename'][0]['conflictMode'];
|
||||
unset($this->file['rename'][0]['conflictMode']);
|
||||
$this->overwriteExistingFiles = DuplicationBehavior::cast($conflictMode);
|
||||
} else {
|
||||
$this->overwriteExistingFiles = DuplicationBehavior::cast($parsedBody['overwriteExistingFiles'] ?? $queryParams['overwriteExistingFiles'] ?? null);
|
||||
}
|
||||
$this->initClipboard();
|
||||
$this->fileProcessor = GeneralUtility::makeInstance(ExtendedFileUtility::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the Clipboard. This will fetch the data about files to paste/delete if such an action has been sent.
|
||||
*/
|
||||
protected function initClipboard(): void
|
||||
{
|
||||
if (is_array($this->CB)) {
|
||||
$clipObj = GeneralUtility::makeInstance(Clipboard::class);
|
||||
$clipObj->initializeClipboard();
|
||||
if ($this->CB['paste']) {
|
||||
$clipObj->setCurrentPad($this->CB['pad']);
|
||||
$this->file = $clipObj->makePasteCmdArray_file($this->CB['paste'], $this->file);
|
||||
}
|
||||
if ($this->CB['delete']) {
|
||||
$clipObj->setCurrentPad($this->CB['pad']);
|
||||
$this->file = $clipObj->makeDeleteCmdArray_file($this->file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performing the file admin action:
|
||||
* Initializes the objects, setting permissions, sending data to object.
|
||||
*/
|
||||
protected function main(): void
|
||||
{
|
||||
// Initializing:
|
||||
$this->fileProcessor->setActionPermissions();
|
||||
$this->fileProcessor->setExistingFilesConflictMode($this->overwriteExistingFiles);
|
||||
$this->fileProcessor->start($this->file);
|
||||
$this->fileData = $this->fileProcessor->processData();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets URI to be used for editing given file (if file extension is defined in textfile_ext)
|
||||
*
|
||||
* @param File $file to be edited
|
||||
* @return string|null URI to be redirected to
|
||||
* @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
|
||||
*/
|
||||
protected function getFileEditRedirect(File $file): ?string
|
||||
{
|
||||
$textFileExtensionList = $GLOBALS['TYPO3_CONF_VARS']['SYS']['textfile_ext'] ?? '';
|
||||
if (!GeneralUtility::inList($textFileExtensionList, $file->getExtension())) {
|
||||
return null;
|
||||
}
|
||||
$properties = $file->getProperties();
|
||||
$urlParameters = [
|
||||
'target' => $properties['storage'] . ':' . $properties['identifier']
|
||||
];
|
||||
if ($this->redirect) {
|
||||
$urlParameters['returnUrl'] = $this->redirect;
|
||||
}
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
return (string)$uriBuilder->buildUriFromRoute('file_edit', $urlParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirecting the user after the processing has been done.
|
||||
* Might also display error messages directly, if any.
|
||||
*
|
||||
* @deprecated since TYPO3 v9. Will be removed in TYPO3 v10.0.
|
||||
*/
|
||||
public function finish()
|
||||
{
|
||||
trigger_error('FileController->finish() will be removed in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
BackendUtility::setUpdateSignal('updateFolderTree');
|
||||
if ($this->redirect) {
|
||||
HttpUtility::redirect($this->redirect);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten result value from FileProcessor
|
||||
*
|
||||
* The value can be a File, Folder or boolean
|
||||
*
|
||||
* @param bool|File|Folder $result
|
||||
*
|
||||
* @return bool|string|array
|
||||
*/
|
||||
protected function flattenResultDataValue($result)
|
||||
{
|
||||
if ($result instanceof File) {
|
||||
$thumbUrl = '';
|
||||
if (GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $result->getExtension())) {
|
||||
$processedFile = $result->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, []);
|
||||
if ($processedFile) {
|
||||
$thumbUrl = $processedFile->getPublicUrl(true);
|
||||
}
|
||||
}
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$result = array_merge(
|
||||
$result->toArray(),
|
||||
[
|
||||
'date' => BackendUtility::date($result->getModificationTime()),
|
||||
'icon' => $iconFactory->getIconForFileExtension($result->getExtension(), Icon::SIZE_SMALL)->render(),
|
||||
'thumbUrl' => $thumbUrl
|
||||
]
|
||||
);
|
||||
} elseif ($result instanceof Folder) {
|
||||
$result = $result->getIdentifier();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\File;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFolderAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Script Class for display up to 10 upload fields
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class FileUploadController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'title' => 'Using $title of class FileUploadController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'target' => 'Using $target of class FileUploadController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'returnUrl' => 'Using $returnUrl of class FileUploadController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'content' => 'Using $content of class FileUploadController from outside is discouraged as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Name of the filemount
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* Set with the target path inputted in &target
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* Return URL of list module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $returnUrl;
|
||||
|
||||
/**
|
||||
* Accumulating content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* The folder object which is the target directory for the upload
|
||||
*
|
||||
* @var \TYPO3\CMS\Core\Resource\Folder $folderObject
|
||||
*/
|
||||
protected $folderObject;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the request, currently everything is handled and put together via "renderContent()"
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->renderContent();
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the upload file form fields
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('FileUploadController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->renderContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function renders the upload form
|
||||
*
|
||||
* @return string The HTML form as a string, ready for outputting
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function renderUploadForm()
|
||||
{
|
||||
trigger_error('FileUploadController->renderUploadForm() will be replaced by protected method renderUploadFormInternal() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->renderUploadFormInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws InsufficientFolderAccessPermissionsException
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
// Initialize GPvars:
|
||||
$this->target = $parsedBody['target'] ?? $queryParams['target'] ?? null;
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
if (!$this->returnUrl) {
|
||||
$this->returnUrl = (string)$uriBuilder->buildUriFromRoute('file_list', [
|
||||
'id' => rawurlencode($this->target)
|
||||
]);
|
||||
}
|
||||
// Create the folder object
|
||||
if ($this->target) {
|
||||
$this->folderObject = ResourceFactory::getInstance()
|
||||
->retrieveFileOrFolderObject($this->target);
|
||||
}
|
||||
if ($this->folderObject->getStorage()->getUid() === 0) {
|
||||
throw new InsufficientFolderAccessPermissionsException(
|
||||
'You are not allowed to access folders outside your storages',
|
||||
1375889834
|
||||
);
|
||||
}
|
||||
|
||||
// Cleaning and checking target directory
|
||||
if (!$this->folderObject) {
|
||||
$title = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:paramError');
|
||||
$message = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:targetNoDir');
|
||||
throw new \RuntimeException($title . ': ' . $message, 1294586843);
|
||||
}
|
||||
|
||||
// Setting up the context sensitive menu
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
|
||||
// building pathInfo for metaInformation
|
||||
$pathInfo = [
|
||||
'combined_identifier' => $this->folderObject->getCombinedIdentifier(),
|
||||
];
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pathInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render module content
|
||||
*/
|
||||
protected function renderContent(): void
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
|
||||
// set page title
|
||||
$this->moduleTemplate->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.php.pagetitle'));
|
||||
|
||||
$pageContent = '<form action="'
|
||||
. htmlspecialchars((string)$uriBuilder->buildUriFromRoute('tce_file'))
|
||||
. '" method="post" id="FileUploadController" name="editform" enctype="multipart/form-data">';
|
||||
// Make page header:
|
||||
$pageContent .= '<h1>' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.php.pagetitle') . '</h1>';
|
||||
$pageContent .= $this->renderUploadFormInternal();
|
||||
|
||||
// Header Buttons
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
|
||||
// csh button
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('file_upload');
|
||||
$buttonBar->addButton($cshButton);
|
||||
|
||||
// back button
|
||||
if ($this->returnUrl) {
|
||||
$backButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->returnUrl)
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($backButton);
|
||||
}
|
||||
|
||||
$pageContent .= '</form>';
|
||||
$this->content .= '<div>' . $pageContent . '</div>';
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function renders the upload form
|
||||
*
|
||||
* @return string The HTML form as a string, ready for outputting
|
||||
*/
|
||||
protected function renderUploadFormInternal(): string
|
||||
{
|
||||
$content = '
|
||||
<div class="form-group">
|
||||
<input type="file" multiple="multiple" class="form-control" name="upload_1[]" />
|
||||
<input type="hidden" name="data[upload][1][target]" value="' . htmlspecialchars($this->folderObject->getCombinedIdentifier()) . '" />
|
||||
<input type="hidden" name="data[upload][1][data]" value="1" />
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label for="overwriteExistingFiles">
|
||||
<input type="checkbox" name="overwriteExistingFiles" id="overwriteExistingFiles" value="replace" /> ' . htmlspecialchars($this->getLanguageService()->getLL('overwriteExistingFiles')) . '</label>
|
||||
</div>
|
||||
<div>
|
||||
<input type="hidden" name="data[upload][1][redirect]" value="' . $this->returnUrl . '" />
|
||||
<input class="btn btn-primary" type="submit" value="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.php.submit')) . '" />
|
||||
</div>
|
||||
<div class="callout callout-warning">
|
||||
' . htmlspecialchars($this->getLanguageService()->getLL('uploadMultipleFilesInfo')) . '
|
||||
</div>
|
||||
';
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\File;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\DocumentTemplate;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\DuplicationBehavior;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script Class for the rename-file form.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class RenameFileController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'title' => 'Using $title of class RenameFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'target' => 'Using $target of class RenameFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'returnUrl' => 'Using $returnUrl of class RenameFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'content' => 'Using $content of class RenameFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Name of the filemount
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* Target path
|
||||
*
|
||||
* @var string
|
||||
* @internal
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* The file or folder object that should be renamed
|
||||
*
|
||||
* @var File|Folder $fileOrFolderObject
|
||||
*/
|
||||
protected $fileOrFolderObject;
|
||||
|
||||
/**
|
||||
* Return URL of list module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $returnUrl;
|
||||
|
||||
/**
|
||||
* Accumulating content
|
||||
*
|
||||
* @var string
|
||||
* @internal
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the request, currently everything is handled and put together via "renderContent()"
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->renderContent();
|
||||
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the content of the rename form
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('RenameFileController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->renderContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws InsufficientFileAccessPermissionsException
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
// Initialize GPvars:
|
||||
$this->target = $parsedBody['target'] ?? $queryParams['target'] ?? null;
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
// Cleaning and checking target
|
||||
if ($this->target) {
|
||||
$this->fileOrFolderObject = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->retrieveFileOrFolderObject($this->target);
|
||||
}
|
||||
if (!$this->fileOrFolderObject) {
|
||||
$title = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:paramError');
|
||||
$message = $this->getLanguageService()->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:targetNoDir');
|
||||
throw new \RuntimeException($title . ': ' . $message, 1294586844);
|
||||
}
|
||||
if ($this->fileOrFolderObject->getStorage()->getUid() === 0) {
|
||||
throw new InsufficientFileAccessPermissionsException('You are not allowed to access files outside your storages', 1375889840);
|
||||
}
|
||||
|
||||
// If a folder should be renamed, AND the returnURL should go to the old directory name, the redirect is forced
|
||||
// so the redirect will NOT end in an error message
|
||||
// this case only happens if you select the folder itself in the foldertree and then use the clickmenu to
|
||||
// rename the folder
|
||||
if ($this->fileOrFolderObject instanceof Folder) {
|
||||
$parsedUrl = parse_url($this->returnUrl);
|
||||
$queryParts = GeneralUtility::explodeUrl2Array(urldecode($parsedUrl['query']));
|
||||
if ($queryParts['id'] === $this->fileOrFolderObject->getCombinedIdentifier()) {
|
||||
$this->returnUrl = str_replace(
|
||||
urlencode($queryParts['id']),
|
||||
urlencode($this->fileOrFolderObject->getStorage()->getRootLevelFolder()->getCombinedIdentifier()),
|
||||
$this->returnUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// building pathInfo for metaInformation
|
||||
$pathInfo = [
|
||||
'combined_identifier' => $this->fileOrFolderObject->getCombinedIdentifier(),
|
||||
];
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pathInfo);
|
||||
|
||||
// Setting up the context sensitive menu
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/RenameFile');
|
||||
|
||||
// Add javaScript
|
||||
$this->moduleTemplate->addJavaScriptCode(
|
||||
'RenameFileInlineJavaScript',
|
||||
'function backToList() {top.goToModule("file_FilelistList");}'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render module content
|
||||
*/
|
||||
protected function renderContent(): void
|
||||
{
|
||||
$assigns = [];
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
$assigns['moduleUrlTceFile'] = (string)$uriBuilder->buildUriFromRoute('tce_file');
|
||||
$assigns['returnUrl'] = $this->returnUrl;
|
||||
|
||||
if ($this->fileOrFolderObject instanceof Folder) {
|
||||
$fileIdentifier = $this->fileOrFolderObject->getCombinedIdentifier();
|
||||
$targetLabel = 'file_rename.php.label.target.folder';
|
||||
} else {
|
||||
$fileIdentifier = $this->fileOrFolderObject->getUid();
|
||||
$targetLabel = 'file_rename.php.label.target.file';
|
||||
$assigns['conflictMode'] = DuplicationBehavior::cast(DuplicationBehavior::RENAME);
|
||||
$assigns['destination'] = $this->fileOrFolderObject->getParentFolder()->getCombinedIdentifier();
|
||||
}
|
||||
|
||||
$assigns['fileName'] = $this->fileOrFolderObject->getName();
|
||||
$assigns['fileIdentifier'] = $fileIdentifier;
|
||||
$assigns['fieldLabel'] = $targetLabel;
|
||||
|
||||
// Create buttons
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
|
||||
// csh button
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('file_rename');
|
||||
$buttonBar->addButton($cshButton);
|
||||
|
||||
// back button
|
||||
if ($this->returnUrl) {
|
||||
$backButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->returnUrl)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($backButton);
|
||||
}
|
||||
|
||||
// Save and Close button
|
||||
$saveAndCloseButton = $buttonBar->makeInputButton()
|
||||
->setName('_saveandclose')
|
||||
->setValue('1')
|
||||
->setShowLabelText(true)
|
||||
->setClasses('t3js-submit-file-rename')
|
||||
->setForm('RenameFileController')
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_edit.php.saveAndClose'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-close', Icon::SIZE_SMALL));
|
||||
|
||||
$buttonBar->addButton($saveAndCloseButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
|
||||
|
||||
$this->moduleTemplate->getPageRenderer()->addInlineLanguageLabelArray([
|
||||
'file_rename.actions.cancel' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_rename.actions.cancel'),
|
||||
'file_rename.actions.rename' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_rename.actions.rename'),
|
||||
'file_rename.actions.override' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_rename.actions.override'),
|
||||
'file_rename.exists.title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_rename.exists.title'),
|
||||
'file_rename.exists.description' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_rename.exists.description'),
|
||||
]);
|
||||
|
||||
// Rendering of the output via fluid
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/File/RenameFile.html'
|
||||
));
|
||||
$view->assignMultiple($assigns);
|
||||
$this->content = $view->render();
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DocumentTemplate
|
||||
*/
|
||||
protected function getDocumentTemplate(): DocumentTemplate
|
||||
{
|
||||
return $GLOBALS['TBE_TEMPLATE'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\File;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\Exception\InsufficientFileAccessPermissionsException;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script Class for the replace-file form
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ReplaceFileController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'doc' => 'Using $doc of class ReplaceFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'title' => 'Using $title of class ReplaceFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'uid' => 'Using $uid of class ReplaceFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'returnUrl' => 'Using $returnUrl of class ReplaceFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'content' => 'Using $content of class ReplaceFileController from outside is discouraged as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Document template object
|
||||
*
|
||||
* @var \TYPO3\CMS\Backend\Template\DocumentTemplate
|
||||
*/
|
||||
protected $doc;
|
||||
|
||||
/**
|
||||
* Name of the filemount
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* sys_file uid
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $uid;
|
||||
|
||||
/**
|
||||
* The file or folder object that should be renamed
|
||||
*
|
||||
* @var \TYPO3\CMS\Core\Resource\ResourceInterface $fileOrFolderObject
|
||||
*/
|
||||
protected $fileOrFolderObject;
|
||||
|
||||
/**
|
||||
* Return URL of list module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $returnUrl;
|
||||
|
||||
/**
|
||||
* Accumulating content
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the request, currently everything is handled and put together via "main()"
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->renderContent();
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the content of the replace form
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('ReplaceFileController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->renderContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws \RuntimeException
|
||||
* @throws InsufficientFileAccessPermissionsException
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$lang = $this->getLanguageService();
|
||||
|
||||
// Initialize GPvars:
|
||||
$this->uid = (int)($parsedBody['uid'] ?? $queryParams['uid'] ?? 0);
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
|
||||
// Cleaning and checking uid
|
||||
if ($this->uid > 0) {
|
||||
$this->fileOrFolderObject = ResourceFactory::getInstance()
|
||||
->retrieveFileOrFolderObject('file:' . $this->uid);
|
||||
}
|
||||
if (!$this->fileOrFolderObject) {
|
||||
$title = $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:paramError');
|
||||
$message = $lang->sL('LLL:EXT:filelist/Resources/Private/Language/locallang_mod_file_list.xlf:targetNoDir');
|
||||
throw new \RuntimeException($title . ': ' . $message, 1436895930);
|
||||
}
|
||||
if ($this->fileOrFolderObject->getStorage()->getUid() === 0) {
|
||||
throw new InsufficientFileAccessPermissionsException(
|
||||
'You are not allowed to access files outside your storages',
|
||||
1436895931
|
||||
);
|
||||
}
|
||||
|
||||
// If a folder should be renamed, AND the returnURL should go to the old directory name, the redirect is forced
|
||||
// so the redirect will NOT end in an error message
|
||||
// this case only happens if you select the folder itself in the foldertree and then use the clickmenu to
|
||||
// rename the folder
|
||||
if ($this->fileOrFolderObject instanceof Folder) {
|
||||
$parsedUrl = parse_url($this->returnUrl);
|
||||
$queryParts = GeneralUtility::explodeUrl2Array(urldecode($parsedUrl['query']));
|
||||
if ($queryParts['id'] === $this->fileOrFolderObject->getCombinedIdentifier()) {
|
||||
$this->returnUrl = str_replace(
|
||||
urlencode($queryParts['id']),
|
||||
urlencode($this->fileOrFolderObject->getStorage()->getRootLevelFolder()->getCombinedIdentifier()),
|
||||
$this->returnUrl
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$pathInfo = [
|
||||
'combined_identifier' => $this->fileOrFolderObject->getCombinedIdentifier(),
|
||||
];
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pathInfo);
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
$this->moduleTemplate->addJavaScriptCode(
|
||||
'ReplaceFileOnlineJavaScript',
|
||||
'function backToList() {top.goToModule("file_FilelistList");}'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render module content
|
||||
*/
|
||||
protected function renderContent(): void
|
||||
{
|
||||
// Assign variables used by the fluid template
|
||||
$assigns = [];
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$assigns['moduleUrlTceFile'] = (string)$uriBuilder->buildUriFromRoute('tce_file');
|
||||
$assigns['uid'] = $this->uid;
|
||||
$assigns['returnUrl'] = $this->returnUrl;
|
||||
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
// csh button
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('file_rename');
|
||||
$buttonBar->addButton($cshButton);
|
||||
|
||||
// Back button
|
||||
if ($this->returnUrl) {
|
||||
$returnButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->returnUrl)
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($returnButton);
|
||||
}
|
||||
|
||||
// Rendering of the output via fluid
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/File/ReplaceFile.html'
|
||||
));
|
||||
$view->assignMultiple($assigns);
|
||||
$this->content = $view->render();
|
||||
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\File;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ThumbnailController
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultConfiguration = [
|
||||
'width' => 64,
|
||||
'height' => 64,
|
||||
'crop' => null,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function render(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$parameters = $this->extractParameters($request->getQueryParams());
|
||||
$response = $this->generateThumbnail(
|
||||
$parameters['fileId'] ?? null,
|
||||
$parameters['configuration'] ?? []
|
||||
);
|
||||
} catch (\TYPO3\CMS\Core\Resource\Exception $exception) {
|
||||
// catch and handle only resource related exceptions
|
||||
$response = $this->generateNotFoundResponse();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $queryParameters
|
||||
* @return array|null
|
||||
*/
|
||||
protected function extractParameters(array $queryParameters)
|
||||
{
|
||||
$expectedHash = GeneralUtility::hmac(
|
||||
$queryParameters['parameters'] ?? '',
|
||||
ThumbnailController::class
|
||||
);
|
||||
if (!hash_equals($expectedHash, $queryParameters['hmac'] ?? '')) {
|
||||
throw new \InvalidArgumentException(
|
||||
'HMAC could not be verified',
|
||||
1534484203
|
||||
);
|
||||
}
|
||||
|
||||
return json_decode($queryParameters['parameters'] ?? null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|int $fileId
|
||||
* @param array $configuration
|
||||
* @return Response
|
||||
* @throws \TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException
|
||||
*/
|
||||
protected function generateThumbnail($fileId, array $configuration): ResponseInterface
|
||||
{
|
||||
$file = ResourceFactory::getInstance()->getFileObject($fileId);
|
||||
if (empty($file) || $file->isMissing()) {
|
||||
return $this->generateNotFoundResponse();
|
||||
}
|
||||
|
||||
$processingConfiguration = $this->defaultConfiguration;
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$processingConfiguration,
|
||||
$configuration
|
||||
);
|
||||
|
||||
$processedImage = $file->process(
|
||||
ProcessedFile::CONTEXT_IMAGECROPSCALEMASK,
|
||||
$processingConfiguration
|
||||
);
|
||||
$filePath = $processedImage->getForLocalProcessing(false);
|
||||
return new Response($filePath, 200, [
|
||||
'Content-Type' => $processedImage->getMimeType()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function generateNotFoundResponse(): ResponseInterface
|
||||
{
|
||||
return new Response('', 404);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Tree\View\ElementBrowserFolderTreeView;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Filelist\FileListFolderTree;
|
||||
use TYPO3\CMS\Recordlist\Tree\View\DummyLinkParameterProvider;
|
||||
|
||||
/**
|
||||
* Main script class for rendering of the folder tree
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class FileSystemNavigationFrameController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'content' => 'Using $content of class FileSystemNavigationFrameController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'foldertree' => 'Using $foldertree of class FileSystemNavigationFrameController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'currentSubScript' => 'Using $currentSubScript of class FileSystemNavigationFrameController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'cMR' => 'Using $cMR of class FileSystemNavigationFrameController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Content accumulates in this variable.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var \TYPO3\CMS\Backend\Tree\View\FolderTreeView
|
||||
*/
|
||||
protected $foldertree;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $currentSubScript;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $cMR;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $scopeData;
|
||||
|
||||
/**
|
||||
* ModuleTemplate Container
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0 with removal of init()
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
$GLOBALS['SOBE'] = $this;
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->initializePageTemplate();
|
||||
$this->renderFolderTree($request);
|
||||
return new HtmlResponse($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes the AJAX call to expand or collapse the foldertree.
|
||||
* Called by an AJAX Route, see AjaxRequestHandler
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function ajaxExpandCollapse(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$tree = $this->foldertree->getBrowsableTree();
|
||||
if ($this->foldertree->getAjaxStatus() === false) {
|
||||
return new JsonResponse(null, 500);
|
||||
}
|
||||
return new JsonResponse([$tree]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of the script class
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request = null)
|
||||
{
|
||||
if ($request === null) {
|
||||
// Method signature in TYPO3 v10.0: protected function init(ServerRequestInterface $request)
|
||||
trigger_error('FileSystemNavigationFrameController->init() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
}
|
||||
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$this->currentSubScript = $parsedBody['currentSubScript'] ?? $queryParams['currentSubScript'] ?? null;
|
||||
$this->cMR = (bool)($parsedBody['cMR'] ?? $queryParams['cMR'] ?? false);
|
||||
$scopeData = $parsedBody['scopeData'] ?? $queryParams['scopeData'] ?? '';
|
||||
$scopeHash = $parsedBody['scopeHash'] ?? $queryParams['scopeHash'] ?? '';
|
||||
|
||||
if (!empty($scopeData) && hash_equals(GeneralUtility::hmac($scopeData), $scopeHash)) {
|
||||
$this->scopeData = json_decode($scopeData, true);
|
||||
}
|
||||
|
||||
// Create folder tree object:
|
||||
if (!empty($this->scopeData)) {
|
||||
$this->foldertree = GeneralUtility::makeInstance($this->scopeData['class']);
|
||||
$this->foldertree->thisScript = $this->scopeData['script'];
|
||||
if ($this->foldertree instanceof ElementBrowserFolderTreeView) {
|
||||
// create a fake provider to pass link data along properly
|
||||
$linkParamProvider = GeneralUtility::makeInstance(
|
||||
DummyLinkParameterProvider::class,
|
||||
$this->scopeData['browser'],
|
||||
$this->scopeData['script']
|
||||
);
|
||||
$this->foldertree->setLinkParameterProvider($linkParamProvider);
|
||||
}
|
||||
} else {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$this->foldertree = GeneralUtility::makeInstance(FileListFolderTree::class);
|
||||
$this->foldertree->thisScript = (string)$uriBuilder->buildUriFromRoute('file_navframe');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* initialization for the visual parts of the class
|
||||
* Use template rendering only if this is a non-AJAX call
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function initPage()
|
||||
{
|
||||
trigger_error('FileSystemNavigationFrameController->initPage() will be replaced by protected method initializePageTemplate() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->initializePageTemplate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization for the visual parts of the class
|
||||
* Use template rendering only if this is a non-AJAX call
|
||||
*/
|
||||
protected function initializePageTemplate(): void
|
||||
{
|
||||
$this->moduleTemplate->setBodyTag('<body id="ext-backend-Modules-FileSystemNavigationFrame-index-php">');
|
||||
|
||||
// Adding javascript code for drag&drop and the file tree as well as the click menu code
|
||||
$hlClass = $this->getBackendUser()->workspace === 0 ? 'active' : 'active active-ws wsver' . $GLOBALS['BE_USER']->workspace;
|
||||
$dragDropCode = '
|
||||
Tree.highlightClass = "' . $hlClass . '";
|
||||
Tree.highlightActiveItem("", top.fsMod.navFrameHighlightedID["file"]);
|
||||
';
|
||||
|
||||
// Adding javascript for drag & drop activation and highlighting
|
||||
$pageRenderer = $this->moduleTemplate->getPageRenderer();
|
||||
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/LegacyTree', 'function() {
|
||||
DragDrop.table = "folders";
|
||||
Tree.registerDragDropHandlers();
|
||||
' . $dragDropCode . '
|
||||
}');
|
||||
|
||||
// Setting JavaScript for menu.
|
||||
$inlineJs = ($this->currentSubScript ? 'top.currentSubScript=unescape("' . rawurlencode($this->currentSubScript) . '");' : '') . '
|
||||
// Function, loading the list frame from navigation tree:
|
||||
function jumpTo(id, linkObj, highlightID, bank) {
|
||||
var theUrl = top.currentSubScript;
|
||||
if (theUrl.indexOf("?") != -1) {
|
||||
theUrl += "&id=" + id
|
||||
} else {
|
||||
theUrl += "?id=" + id
|
||||
}
|
||||
top.fsMod.currentBank = bank;
|
||||
top.TYPO3.Backend.ContentContainer.setUrl(theUrl);
|
||||
|
||||
Tree.highlightActiveItem("file", highlightID + "_" + bank);
|
||||
if (linkObj) { linkObj.blur(); }
|
||||
return false;
|
||||
}
|
||||
' . ($this->cMR ? ' jumpTo(top.fsMod.recentIds[\'file\'],\'\');' : '');
|
||||
|
||||
$this->moduleTemplate->getPageRenderer()->addJsInlineCode(
|
||||
'FileSystemNavigationFrame',
|
||||
$inlineJs
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the folder tree
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('FileSystemNavigationFrameController->main() will be replaced by protected method renderFolderTree() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->renderFolderTree($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the folder tree
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function renderFolderTree(ServerRequestInterface $request): void
|
||||
{
|
||||
// Produce browse-tree:
|
||||
$tree = $this->foldertree->getBrowsableTree();
|
||||
// Outputting page tree:
|
||||
$this->moduleTemplate->setContent($tree);
|
||||
// Setting up the buttons
|
||||
$this->getButtons($request);
|
||||
// Build the <body> for the module
|
||||
$this->moduleTemplate->setTitle('TYPO3 Folder Tree');
|
||||
$this->content = $this->moduleTemplate->renderContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register docHeader buttons
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function getButtons(ServerRequestInterface $request): void
|
||||
{
|
||||
/** @var ButtonBar $buttonBar */
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
/** @var IconFactory $iconFactory */
|
||||
$iconFactory = $this->moduleTemplate->getIconFactory();
|
||||
/** @var \TYPO3\CMS\Core\Http\NormalizedParams */
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
|
||||
// Refresh
|
||||
$refreshButton = $buttonBar->makeLinkButton()
|
||||
->setHref($normalizedParams->getRequestUri())
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
|
||||
->setIcon($iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($refreshButton, ButtonBar::BUTTON_POSITION_RIGHT);
|
||||
|
||||
// CSH
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('filetree');
|
||||
$buttonBar->addButton($cshButton);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Render FlashMessages
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class FlashMessageController
|
||||
{
|
||||
/**
|
||||
* Renders the FlashMessages from queue and returns them as JSON.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getQueuedFlashMessagesAction(): ResponseInterface
|
||||
{
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$flashMessages = $defaultFlashMessageQueue->getAllMessagesAndFlush();
|
||||
|
||||
$messages = [];
|
||||
foreach ($flashMessages as $flashMessage) {
|
||||
$messages[] = [
|
||||
'title' => $flashMessage->getTitle(),
|
||||
'message' => $flashMessage->getMessage(),
|
||||
'severity' => $flashMessage->getSeverity()
|
||||
];
|
||||
}
|
||||
|
||||
return (new JsonResponse())->setPayload($messages);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Handle FormEngine flex field ajax calls
|
||||
*/
|
||||
class FormFlexAjaxController extends AbstractFormEngineAjaxController
|
||||
{
|
||||
/**
|
||||
* Render a single flex form section container to add it to the DOM
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function containerAdd(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$queryParameters = $request->getParsedBody();
|
||||
|
||||
$vanillaUid = (int)$queryParameters['vanillaUid'];
|
||||
$databaseRowUid = $queryParameters['databaseRowUid'];
|
||||
$command = $queryParameters['command'];
|
||||
$tableName = $queryParameters['tableName'];
|
||||
$fieldName = $queryParameters['fieldName'];
|
||||
$recordTypeValue = $queryParameters['recordTypeValue'];
|
||||
$dataStructureIdentifier = json_encode($queryParameters['dataStructureIdentifier']);
|
||||
$flexFormSheetName = $queryParameters['flexFormSheetName'];
|
||||
$flexFormFieldName = $queryParameters['flexFormFieldName'];
|
||||
$flexFormContainerName = $queryParameters['flexFormContainerName'];
|
||||
|
||||
// Prepare TCA and data values for a new section container using data providers
|
||||
$processedTca = $GLOBALS['TCA'][$tableName];
|
||||
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
|
||||
$dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
|
||||
$processedTca['columns'][$fieldName]['config']['ds'] = $dataStructure;
|
||||
$processedTca['columns'][$fieldName]['config']['dataStructureIdentifier'] = $dataStructureIdentifier;
|
||||
// Get a new unique id for this container.
|
||||
$flexFormContainerIdentifier = StringUtility::getUniqueId();
|
||||
$flexSectionContainerPreparation = [
|
||||
'flexFormSheetName' => $flexFormSheetName,
|
||||
'flexFormFieldName' => $flexFormFieldName,
|
||||
'flexFormContainerName' => $flexFormContainerName,
|
||||
'flexFormContainerIdentifier' => $flexFormContainerIdentifier,
|
||||
];
|
||||
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'tableName' => $tableName,
|
||||
'vanillaUid' => (int)$vanillaUid,
|
||||
'command' => $command,
|
||||
'recordTypeValue' => $recordTypeValue,
|
||||
'processedTca' => $processedTca,
|
||||
'flexSectionContainerPreparation' => $flexSectionContainerPreparation,
|
||||
];
|
||||
// A new container on a new record needs the 'NEW123' uid here, see comment
|
||||
// in DatabaseUniqueUidNewRow for more information on that.
|
||||
// @todo: Resolve, maybe with a redifinition of vanillaUid to transport the information more clean through this var?
|
||||
// @see issue #80100 for a series of changes in this area
|
||||
if ($command === 'new') {
|
||||
$formDataCompilerInput['databaseRow']['uid'] = $databaseRowUid;
|
||||
}
|
||||
$formData = $formDataCompiler->compile($formDataCompilerInput);
|
||||
|
||||
$dataStructure = $formData['processedTca']['columns'][$fieldName]['config']['ds'];
|
||||
$formData['fieldName'] = $fieldName;
|
||||
$formData['flexFormDataStructureArray'] = $dataStructure['sheets'][$flexFormSheetName]['ROOT']['el'][$flexFormFieldName]['children'][$flexFormContainerIdentifier];
|
||||
$formData['flexFormDataStructureIdentifier'] = $dataStructureIdentifier;
|
||||
$formData['flexFormFieldName'] = $flexFormFieldName;
|
||||
$formData['flexFormSheetName'] = $flexFormSheetName;
|
||||
$formData['flexFormContainerName'] = $flexFormContainerName;
|
||||
$formData['flexFormContainerIdentifier'] = $flexFormContainerIdentifier;
|
||||
$formData['flexFormContainerElementCollapsed'] = false;
|
||||
|
||||
$formData['flexFormFormPrefix'] = '[data][' . $flexFormSheetName . '][lDEF]' . '[' . $flexFormFieldName . ']' . '[el]';
|
||||
|
||||
// Set initialized data of that section container from compiler to the array part used
|
||||
// by flexFormElementContainer which prepares parameterArray. Important for initialized
|
||||
// values of group element.
|
||||
if (isset($formData['databaseRow'][$fieldName]
|
||||
['data'][$flexFormSheetName]
|
||||
['lDEF'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerIdentifier][$flexFormContainerName]['el']
|
||||
)
|
||||
&& is_array(
|
||||
$formData['databaseRow'][$fieldName]
|
||||
['data'][$flexFormSheetName]
|
||||
['lDEF'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerIdentifier][$flexFormContainerName]['el']
|
||||
)
|
||||
) {
|
||||
$formData['flexFormRowData'] = $formData['databaseRow'][$fieldName]
|
||||
['data'][$flexFormSheetName]
|
||||
['lDEF'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerIdentifier][$flexFormContainerName]['el'];
|
||||
}
|
||||
|
||||
$formData['parameterArray']['itemFormElName'] = 'data[' . $tableName . '][' . $formData['databaseRow']['uid'] . '][' . $fieldName . ']';
|
||||
|
||||
// JavaScript code for event handlers:
|
||||
// @todo: see if we can get rid of this - used in group elements, and also for the "reload" on type field changes
|
||||
$formData['parameterArray']['fieldChangeFunc'] = [];
|
||||
$formData['parameterArray']['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
|
||||
. GeneralUtility::quoteJSvalue($tableName)
|
||||
. ',' . GeneralUtility::quoteJSvalue($formData['databaseRow']['uid'])
|
||||
. ',' . GeneralUtility::quoteJSvalue($fieldName)
|
||||
. ',' . GeneralUtility::quoteJSvalue($formData['parameterArray']['itemFormElName'])
|
||||
. ');';
|
||||
|
||||
// @todo: check GroupElement for usage of elementBaseName ... maybe kick that thing?
|
||||
|
||||
// Feed resulting form data to container structure to render HTML and other result data
|
||||
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
|
||||
$formData['renderType'] = 'flexFormContainerContainer';
|
||||
$newContainerResult = $nodeFactory->create($formData)->render();
|
||||
|
||||
$jsonResult = [
|
||||
'html' => $newContainerResult['html'],
|
||||
'stylesheetFiles' => [],
|
||||
'scriptCall' => [],
|
||||
];
|
||||
|
||||
if (!empty($newContainerResult['additionalJavaScriptSubmit'])) {
|
||||
$additionalJavaScriptSubmit = implode('', $newContainerResult['additionalJavaScriptSubmit']);
|
||||
$additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
|
||||
$jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
|
||||
}
|
||||
foreach ($newContainerResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
|
||||
$jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
|
||||
}
|
||||
foreach ($newContainerResult['stylesheetFiles'] as $stylesheetFile) {
|
||||
$jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile);
|
||||
}
|
||||
if (!empty($newContainerResult['additionalInlineLanguageLabelFiles'])) {
|
||||
$labels = [];
|
||||
foreach ($newContainerResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$labels,
|
||||
$this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
|
||||
);
|
||||
}
|
||||
$javaScriptCode = [];
|
||||
$javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
|
||||
$javaScriptCode[] = ' TYPO3.lang = {}';
|
||||
$javaScriptCode[] = '}';
|
||||
$javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
|
||||
$javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
|
||||
$javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
|
||||
$javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
|
||||
$javaScriptCode[] = ' }';
|
||||
$javaScriptCode[] = '}';
|
||||
|
||||
$jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
|
||||
}
|
||||
|
||||
$requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($newContainerResult);
|
||||
$jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
|
||||
|
||||
return new JsonResponse($jsonResult);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,766 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Messaging\AbstractMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Handle FormEngine inline ajax calls
|
||||
*/
|
||||
class FormInlineAjaxController extends AbstractFormEngineAjaxController
|
||||
{
|
||||
/**
|
||||
* Create a new inline child via AJAX.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function createAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
|
||||
$parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
|
||||
|
||||
$domObjectId = $ajaxArguments[0];
|
||||
$inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
|
||||
if (!MathUtility::canBeInterpretedAsInteger($inlineFirstPid) && strpos($inlineFirstPid, 'NEW') !== 0) {
|
||||
throw new \RuntimeException(
|
||||
'inlineFirstPid should either be an integer or a "NEW..." string',
|
||||
1521220491
|
||||
);
|
||||
}
|
||||
$childChildUid = null;
|
||||
if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
|
||||
$childChildUid = (int)$ajaxArguments[1];
|
||||
}
|
||||
|
||||
// Parse the DOM identifier, add the levels to the structure stack
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
|
||||
$inlineStackProcessor->injectAjaxConfiguration($parentConfig);
|
||||
$inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
|
||||
|
||||
// Parent, this table embeds the child table
|
||||
$parent = $inlineStackProcessor->getStructureLevel(-1);
|
||||
|
||||
// Child, a record from this table should be rendered
|
||||
$child = $inlineStackProcessor->getUnstableStructure();
|
||||
if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
|
||||
// If uid comes in, it is the id of the record neighbor record "create after"
|
||||
$childVanillaUid = -1 * abs((int)$child['uid']);
|
||||
} else {
|
||||
// Else inline first Pid is the storage pid of new inline records
|
||||
$childVanillaUid = $inlineFirstPid;
|
||||
}
|
||||
|
||||
$childTableName = $parentConfig['foreign_table'];
|
||||
|
||||
/** @var TcaDatabaseRecord $formDataGroup */
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
|
||||
/** @var FormDataCompiler $formDataCompiler */
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'command' => 'new',
|
||||
'tableName' => $childTableName,
|
||||
'vanillaUid' => $childVanillaUid,
|
||||
'isInlineChild' => true,
|
||||
'inlineStructure' => $inlineStackProcessor->getStructure(),
|
||||
'inlineFirstPid' => $inlineFirstPid,
|
||||
'inlineParentUid' => $parent['uid'],
|
||||
'inlineParentTableName' => $parent['table'],
|
||||
'inlineParentFieldName' => $parent['field'],
|
||||
'inlineParentConfig' => $parentConfig,
|
||||
'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
|
||||
'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
|
||||
'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
|
||||
];
|
||||
if ($childChildUid) {
|
||||
$formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
|
||||
}
|
||||
$childData = $formDataCompiler->compile($formDataCompilerInput);
|
||||
|
||||
if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
|
||||
// We have a foreign_selector. So, we just created a new record on an intermediate table in $childData.
|
||||
// Now, if a valid id is given as second ajax parameter, the intermediate row should be connected to an
|
||||
// existing record of the child-child table specified by the given uid. If there is no such id, user
|
||||
// clicked on "created new" and a new child-child should be created, too.
|
||||
if ($childChildUid) {
|
||||
// Fetch existing child child
|
||||
$childData['databaseRow'][$parentConfig['foreign_selector']] = [
|
||||
$childChildUid,
|
||||
];
|
||||
$childData['combinationChild'] = $this->compileChildChild($childData, $parentConfig, $inlineStackProcessor->getStructure());
|
||||
} else {
|
||||
/** @var TcaDatabaseRecord $formDataGroup */
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
|
||||
/** @var FormDataCompiler $formDataCompiler */
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'command' => 'new',
|
||||
'tableName' => $childData['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'],
|
||||
'vanillaUid' => $inlineFirstPid,
|
||||
'isInlineChild' => true,
|
||||
'isInlineAjaxOpeningContext' => true,
|
||||
'inlineStructure' => $inlineStackProcessor->getStructure(),
|
||||
'inlineFirstPid' => $inlineFirstPid,
|
||||
];
|
||||
$childData['combinationChild'] = $formDataCompiler->compile($formDataCompilerInput);
|
||||
}
|
||||
}
|
||||
|
||||
$childData['inlineParentUid'] = $parent['uid'];
|
||||
$childData['renderType'] = 'inlineRecordContainer';
|
||||
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
|
||||
$childResult = $nodeFactory->create($childData)->render();
|
||||
|
||||
$jsonArray = [
|
||||
'data' => '',
|
||||
'stylesheetFiles' => [],
|
||||
'scriptCall' => [],
|
||||
];
|
||||
|
||||
// The HTML-object-id's prefix of the dynamically created record
|
||||
$objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
|
||||
$objectPrefix = $objectName . '-' . $child['table'];
|
||||
$objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
|
||||
$expandSingle = $parentConfig['appearance']['expandSingle'];
|
||||
if (!$child['uid']) {
|
||||
$jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
|
||||
$jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
|
||||
} else {
|
||||
$jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
|
||||
$jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
|
||||
}
|
||||
$jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
|
||||
if ($parentConfig['appearance']['useSortable']) {
|
||||
$inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
|
||||
$jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
|
||||
}
|
||||
if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
|
||||
$jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
|
||||
}
|
||||
// Fade out and fade in the new record in the browser view to catch the user's eye
|
||||
$jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
|
||||
|
||||
return new JsonResponse($jsonArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the details of a child record.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function detailsAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
|
||||
|
||||
$domObjectId = $ajaxArguments[0] ?? null;
|
||||
$inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
|
||||
$parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
|
||||
|
||||
// Parse the DOM identifier, add the levels to the structure stack
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
|
||||
$inlineStackProcessor->injectAjaxConfiguration($parentConfig);
|
||||
|
||||
// Parent, this table embeds the child table
|
||||
$parent = $inlineStackProcessor->getStructureLevel(-1);
|
||||
$parentFieldName = $parent['field'];
|
||||
|
||||
// Set flag in config so that only the fields are rendered
|
||||
// @todo: Solve differently / rename / whatever
|
||||
$parentConfig['renderFieldsOnly'] = true;
|
||||
|
||||
$parentData = [
|
||||
'processedTca' => [
|
||||
'columns' => [
|
||||
$parentFieldName => [
|
||||
'config' => $parentConfig,
|
||||
],
|
||||
],
|
||||
],
|
||||
'uid' => $parent['uid'],
|
||||
'tableName' => $parent['table'],
|
||||
'inlineFirstPid' => $inlineFirstPid,
|
||||
// Hand over given original return url to compile stack. Needed if inline children compile links to
|
||||
// another view (eg. edit metadata in a nested inline situation like news with inline content element image),
|
||||
// so the back link is still the link from the original request. See issue #82525. This is additionally
|
||||
// given down in TcaInline data provider to compiled children data.
|
||||
'returnUrl' => $parentConfig['originalReturnUrl'],
|
||||
];
|
||||
|
||||
// Child, a record from this table should be rendered
|
||||
$child = $inlineStackProcessor->getUnstableStructure();
|
||||
|
||||
$childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
|
||||
|
||||
$childData['inlineParentUid'] = (int)$parent['uid'];
|
||||
$childData['renderType'] = 'inlineRecordContainer';
|
||||
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
|
||||
$childResult = $nodeFactory->create($childData)->render();
|
||||
|
||||
$jsonArray = [
|
||||
'data' => '',
|
||||
'stylesheetFiles' => [],
|
||||
'scriptCall' => [],
|
||||
];
|
||||
|
||||
// The HTML-object-id's prefix of the dynamically created record
|
||||
$objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
|
||||
$objectId = $objectPrefix . '-' . (int)$child['uid'];
|
||||
$expandSingle = $parentConfig['appearance']['expandSingle'];
|
||||
$jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
|
||||
if ($parentConfig['foreign_unique']) {
|
||||
$jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
|
||||
}
|
||||
$jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
|
||||
if ($parentConfig['appearance']['useSortable']) {
|
||||
$inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
|
||||
$jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
|
||||
}
|
||||
if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
|
||||
$jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
|
||||
}
|
||||
|
||||
return new JsonResponse($jsonArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds localizations or synchronizes the locations of all child records.
|
||||
* Handle AJAX calls to localize all records of a parent, localize a single record or to synchronize with the original language parent.
|
||||
*
|
||||
* @param ServerRequestInterface $request the incoming request
|
||||
* @return ResponseInterface the filled response
|
||||
*/
|
||||
public function synchronizeLocalizeAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
|
||||
$domObjectId = $ajaxArguments[0] ?? null;
|
||||
$type = $ajaxArguments[1] ?? null;
|
||||
$parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
|
||||
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
// Parse the DOM identifier (string), add the levels to the structure stack (array), load the TCA config:
|
||||
$inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
|
||||
$inlineStackProcessor->injectAjaxConfiguration($parentConfig);
|
||||
$inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
|
||||
|
||||
$jsonArray = [
|
||||
'data' => '',
|
||||
'stylesheetFiles' => [],
|
||||
'scriptCall' => [],
|
||||
];
|
||||
if ($type === 'localize' || $type === 'synchronize' || MathUtility::canBeInterpretedAsInteger($type)) {
|
||||
// Parent, this table embeds the child table
|
||||
$parent = $inlineStackProcessor->getStructureLevel(-1);
|
||||
$parentFieldName = $parent['field'];
|
||||
|
||||
$processedTca = $GLOBALS['TCA'][$parent['table']];
|
||||
$processedTca['columns'][$parentFieldName]['config'] = $parentConfig;
|
||||
|
||||
// Child, a record from this table should be rendered
|
||||
$child = $inlineStackProcessor->getUnstableStructure();
|
||||
|
||||
$formDataCompilerInputForParent = [
|
||||
'vanillaUid' => (int)$parent['uid'],
|
||||
'command' => 'edit',
|
||||
'tableName' => $parent['table'],
|
||||
'processedTca' => $processedTca,
|
||||
'inlineFirstPid' => $inlineFirstPid,
|
||||
'columnsToProcess' => [
|
||||
$parentFieldName
|
||||
],
|
||||
// @todo: still needed? NO!
|
||||
'inlineStructure' => $inlineStackProcessor->getStructure(),
|
||||
// Do not compile existing children, we don't need them now
|
||||
'inlineCompileExistingChildren' => false,
|
||||
];
|
||||
// Full TcaDatabaseRecord is required here to have the list of connected uids $oldItemList
|
||||
/** @var TcaDatabaseRecord $formDataGroup */
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
|
||||
/** @var FormDataCompiler $formDataCompiler */
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$parentData = $formDataCompiler->compile($formDataCompilerInputForParent);
|
||||
$parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
|
||||
$parentLanguageField = $parentData['processedTca']['ctrl']['languageField'];
|
||||
$parentLanguage = $parentData['databaseRow'][$parentLanguageField];
|
||||
$oldItemList = $parentData['databaseRow'][$parentFieldName];
|
||||
|
||||
// DataHandler cannot handle arrays as field value
|
||||
if (is_array($parentLanguage)) {
|
||||
$parentLanguage = implode(',', $parentLanguage);
|
||||
}
|
||||
|
||||
$cmd = [];
|
||||
// Localize a single child element from default language of the parent element
|
||||
if (MathUtility::canBeInterpretedAsInteger($type)) {
|
||||
$cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
|
||||
'field' => $parent['field'],
|
||||
'language' => $parentLanguage,
|
||||
'ids' => [$type],
|
||||
];
|
||||
} else {
|
||||
// Either localize or synchronize all child elements from default language of the parent element
|
||||
$cmd[$parent['table']][$parent['uid']]['inlineLocalizeSynchronize'] = [
|
||||
'field' => $parent['field'],
|
||||
'language' => $parentLanguage,
|
||||
'action' => $type,
|
||||
];
|
||||
}
|
||||
|
||||
/** @var DataHandler $tce */
|
||||
$tce = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$tce->start([], $cmd);
|
||||
$tce->process_cmdmap();
|
||||
|
||||
$newItemList = $tce->registerDBList[$parent['table']][$parent['uid']][$parentFieldName];
|
||||
|
||||
$nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
|
||||
$nameObjectForeignTable = $nameObject . '-' . $child['table'];
|
||||
|
||||
$oldItems = $this->getInlineRelatedRecordsUidArray($oldItemList);
|
||||
$newItems = $this->getInlineRelatedRecordsUidArray($newItemList);
|
||||
|
||||
// Render error messages from DataHandler
|
||||
$tce->printLogErrorMessages();
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$messages = $flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush();
|
||||
if (!empty($messages)) {
|
||||
foreach ($messages as $message) {
|
||||
$jsonArray['messages'][] = [
|
||||
'title' => $message->getTitle(),
|
||||
'message' => $message->getMessage(),
|
||||
'severity' => $message->getSeverity()
|
||||
];
|
||||
if ($message->getSeverity() === AbstractMessage::ERROR) {
|
||||
$jsonArray['hasErrors'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the items that should be removed in the forms view:
|
||||
$removedItems = array_diff($oldItems, $newItems);
|
||||
foreach ($removedItems as $childUid) {
|
||||
$jsonArray['scriptCall'][] = 'inline.deleteRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $childUid) . ', {forceDirectRemoval: true});';
|
||||
}
|
||||
|
||||
$localizedItems = array_diff($newItems, $oldItems);
|
||||
foreach ($localizedItems as $childUid) {
|
||||
$childData = $this->compileChild($parentData, $parentFieldName, (int)$childUid, $inlineStackProcessor->getStructure());
|
||||
|
||||
$childData['inlineParentUid'] = (int)$parent['uid'];
|
||||
$childData['renderType'] = 'inlineRecordContainer';
|
||||
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
|
||||
$childResult = $nodeFactory->create($childData)->render();
|
||||
|
||||
$jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
|
||||
|
||||
// Get the name of the field used as foreign selector (if any):
|
||||
$foreignSelector = isset($parentConfig['foreign_selector']) && $parentConfig['foreign_selector'] ? $parentConfig['foreign_selector'] : false;
|
||||
$selectedValue = $foreignSelector ? GeneralUtility::quoteJSvalue($childData['databaseRow'][$foreignSelector]) : 'null';
|
||||
if (is_array($selectedValue)) {
|
||||
$selectedValue = $selectedValue[0];
|
||||
}
|
||||
$jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable) . ', ' . GeneralUtility::quoteJSvalue($childUid) . ', null, ' . $selectedValue . ');';
|
||||
// Remove possible virtual records in the form which showed that a child records could be localized:
|
||||
$transOrigPointerFieldName = $childData['processedTca']['ctrl']['transOrigPointerField'];
|
||||
if (isset($childData['databaseRow'][$transOrigPointerFieldName]) && $childData['databaseRow'][$transOrigPointerFieldName]) {
|
||||
$transOrigPointerField = $childData['databaseRow'][$transOrigPointerFieldName];
|
||||
if (is_array($transOrigPointerField)) {
|
||||
$transOrigPointerField = $transOrigPointerField[0];
|
||||
}
|
||||
$jsonArray['scriptCall'][] = 'inline.fadeAndRemove(' . GeneralUtility::quoteJSvalue($nameObjectForeignTable . '-' . $transOrigPointerField . '_div') . ');';
|
||||
}
|
||||
}
|
||||
// Tell JS to add new HTML of one or multiple (localize all) records to DOM
|
||||
if (!empty($jsonArray['data'])) {
|
||||
$jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\', ' . GeneralUtility::quoteJSvalue($nameObject . '_records')
|
||||
. ', ' . GeneralUtility::quoteJSvalue($nameObjectForeignTable)
|
||||
. ', json.data);';
|
||||
}
|
||||
}
|
||||
return new JsonResponse($jsonArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store status of inline children expand / collapse state in backend user uC.
|
||||
*
|
||||
* @param ServerRequestInterface $request the incoming request
|
||||
* @return ResponseInterface the filled response
|
||||
*/
|
||||
public function expandOrCollapseAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
|
||||
$domObjectId = $ajaxArguments[0];
|
||||
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
// Parse the DOM identifier (string), add the levels to the structure stack (array), don't load TCA config
|
||||
$inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
|
||||
$expand = $ajaxArguments[1];
|
||||
$collapse = $ajaxArguments[2];
|
||||
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
// The current table - for this table we should add/import records
|
||||
$currentTable = $inlineStackProcessor->getUnstableStructure();
|
||||
$currentTable = $currentTable['table'];
|
||||
// The top parent table - this table embeds the current table
|
||||
$top = $inlineStackProcessor->getStructureLevel(0);
|
||||
$topTable = $top['table'];
|
||||
$topUid = $top['uid'];
|
||||
$inlineView = $this->getInlineExpandCollapseStateArray();
|
||||
// Only do some action if the top record and the current record were saved before
|
||||
if (MathUtility::canBeInterpretedAsInteger($topUid)) {
|
||||
$expandUids = GeneralUtility::trimExplode(',', $expand);
|
||||
$collapseUids = GeneralUtility::trimExplode(',', $collapse);
|
||||
// Set records to be expanded
|
||||
foreach ($expandUids as $uid) {
|
||||
$inlineView[$topTable][$topUid][$currentTable][] = $uid;
|
||||
}
|
||||
// Set records to be collapsed
|
||||
foreach ($collapseUids as $uid) {
|
||||
$inlineView[$topTable][$topUid][$currentTable] = $this->removeFromArray($uid, $inlineView[$topTable][$topUid][$currentTable]);
|
||||
}
|
||||
// Save states back to database
|
||||
if (is_array($inlineView[$topTable][$topUid][$currentTable])) {
|
||||
$inlineView[$topTable][$topUid][$currentTable] = array_unique($inlineView[$topTable][$topUid][$currentTable]);
|
||||
$backendUser->uc['inlineView'] = serialize($inlineView);
|
||||
$backendUser->writeUC();
|
||||
}
|
||||
}
|
||||
return (new JsonResponse())->setPayload([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a full child record
|
||||
*
|
||||
* @param array $parentData Result array of parent
|
||||
* @param string $parentFieldName Name of parent field
|
||||
* @param int $childUid Uid of child to compile
|
||||
* @param array $inlineStructure Current inline structure
|
||||
* @return array Full result array
|
||||
*
|
||||
* @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction
|
||||
* @todo: to also encapsulate the more complex scenarios with combination child and friends.
|
||||
*/
|
||||
protected function compileChild(array $parentData, $parentFieldName, $childUid, array $inlineStructure)
|
||||
{
|
||||
$parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
|
||||
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByGivenStructure($inlineStructure);
|
||||
$inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
|
||||
|
||||
// @todo: do not use stack processor here ...
|
||||
$child = $inlineStackProcessor->getUnstableStructure();
|
||||
$childTableName = $child['table'];
|
||||
|
||||
/** @var TcaDatabaseRecord $formDataGroup */
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
|
||||
/** @var FormDataCompiler $formDataCompiler */
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'command' => 'edit',
|
||||
'tableName' => $childTableName,
|
||||
'vanillaUid' => (int)$childUid,
|
||||
'returnUrl' => $parentData['returnUrl'],
|
||||
'isInlineChild' => true,
|
||||
'inlineStructure' => $inlineStructure,
|
||||
'inlineFirstPid' => $parentData['inlineFirstPid'],
|
||||
'inlineParentConfig' => $parentConfig,
|
||||
'isInlineAjaxOpeningContext' => true,
|
||||
|
||||
// values of the current parent element
|
||||
// it is always a string either an id or new...
|
||||
'inlineParentUid' => $parentData['databaseRow']['uid'] ?? $parentData['uid'],
|
||||
'inlineParentTableName' => $parentData['tableName'],
|
||||
'inlineParentFieldName' => $parentFieldName,
|
||||
|
||||
// values of the top most parent element set on first level and not overridden on following levels
|
||||
'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
|
||||
'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
|
||||
'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
|
||||
];
|
||||
// For foreign_selector with useCombination $mainChild is the mm record
|
||||
// and $combinationChild is the child-child. For "normal" relations, $mainChild
|
||||
// is just the normal child record and $combinationChild is empty.
|
||||
$mainChild = $formDataCompiler->compile($formDataCompilerInput);
|
||||
if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
|
||||
// This kicks in if opening an existing mainChild that has a child-child set
|
||||
$mainChild['combinationChild'] = $this->compileChildChild($mainChild, $parentConfig, $inlineStructure);
|
||||
}
|
||||
return $mainChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* With useCombination set, not only content of the intermediate table, but also
|
||||
* the connected child should be rendered in one go. Prepare this here.
|
||||
*
|
||||
* @param array $child Full data array of "mm" record
|
||||
* @param array $parentConfig TCA configuration of "parent"
|
||||
* @param array $inlineStructure Current inline structure
|
||||
* @return array Full data array of child
|
||||
*/
|
||||
protected function compileChildChild(array $child, array $parentConfig, array $inlineStructure)
|
||||
{
|
||||
// foreign_selector on intermediate is probably type=select, so data provider of this table resolved that to the uid already
|
||||
$childChildUid = $child['databaseRow'][$parentConfig['foreign_selector']][0];
|
||||
// child-child table name is set in child tca "the selector field" foreign_table
|
||||
$childChildTableName = $child['processedTca']['columns'][$parentConfig['foreign_selector']]['config']['foreign_table'];
|
||||
/** @var TcaDatabaseRecord $formDataGroup */
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
|
||||
/** @var FormDataCompiler $formDataCompiler */
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'command' => 'edit',
|
||||
'tableName' => $childChildTableName,
|
||||
'vanillaUid' => (int)$childChildUid,
|
||||
'isInlineChild' => true,
|
||||
'isInlineAjaxOpeningContext' => true,
|
||||
// @todo: this is the wrong inline structure, isn't it? Shouldn't contain it the part from child child, too?
|
||||
'inlineStructure' => $inlineStructure,
|
||||
'inlineFirstPid' => $child['inlineFirstPid'],
|
||||
// values of the top most parent element set on first level and not overridden on following levels
|
||||
'inlineTopMostParentUid' => $child['inlineTopMostParentUid'],
|
||||
'inlineTopMostParentTableName' => $child['inlineTopMostParentTableName'],
|
||||
'inlineTopMostParentFieldName' => $child['inlineTopMostParentFieldName'],
|
||||
];
|
||||
return $formDataCompiler->compile($formDataCompilerInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge stuff from child array into json array.
|
||||
* This method is needed since ajax handling methods currently need to put scriptCalls before and after child code.
|
||||
*
|
||||
* @param array $jsonResult Given json result
|
||||
* @param array $childResult Given child result
|
||||
* @return array Merged json array
|
||||
*/
|
||||
protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult)
|
||||
{
|
||||
$jsonResult['data'] .= $childResult['html'];
|
||||
$jsonResult['stylesheetFiles'] = [];
|
||||
foreach ($childResult['stylesheetFiles'] as $stylesheetFile) {
|
||||
$jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile);
|
||||
}
|
||||
if (!empty($childResult['inlineData'])) {
|
||||
$jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
|
||||
}
|
||||
if (!empty($childResult['additionalJavaScriptSubmit'])) {
|
||||
$additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
|
||||
$additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
|
||||
$jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
|
||||
}
|
||||
foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
|
||||
$jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
|
||||
}
|
||||
if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
|
||||
$labels = [];
|
||||
foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$labels,
|
||||
$this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
|
||||
);
|
||||
}
|
||||
$javaScriptCode = [];
|
||||
$javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
|
||||
$javaScriptCode[] = ' TYPO3.lang = {}';
|
||||
$javaScriptCode[] = '}';
|
||||
$javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
|
||||
$javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
|
||||
$javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
|
||||
$javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
|
||||
$javaScriptCode[] = ' }';
|
||||
$javaScriptCode[] = '}';
|
||||
|
||||
$jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
|
||||
}
|
||||
$requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult);
|
||||
$jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
|
||||
|
||||
return $jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array with the uids of related records out of a list of items.
|
||||
* This list could contain more information than required. This methods just
|
||||
* extracts the uids.
|
||||
*
|
||||
* @param string $itemList The list of related child records
|
||||
* @return array An array with uids
|
||||
*/
|
||||
protected function getInlineRelatedRecordsUidArray($itemList)
|
||||
{
|
||||
$itemArray = GeneralUtility::trimExplode(',', $itemList, true);
|
||||
// Perform modification of the selected items array:
|
||||
foreach ($itemArray as &$value) {
|
||||
$parts = explode('|', $value, 2);
|
||||
$value = $parts[0];
|
||||
}
|
||||
unset($value);
|
||||
return $itemArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return expand / collapse state array for a given table / uid combination
|
||||
*
|
||||
* @param string $table Handled table
|
||||
* @param int $uid Handled uid
|
||||
* @return array
|
||||
*/
|
||||
protected function getInlineExpandCollapseStateArrayForTableUid($table, $uid)
|
||||
{
|
||||
$inlineView = $this->getInlineExpandCollapseStateArray();
|
||||
$result = [];
|
||||
if (MathUtility::canBeInterpretedAsInteger($uid)) {
|
||||
if (!empty($inlineView[$table][$uid])) {
|
||||
$result = $inlineView[$table][$uid];
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get expand / collapse state of inline items
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getInlineExpandCollapseStateArray()
|
||||
{
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
if (!$this->backendUserHasUcInlineView($backendUser)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$inlineView = unserialize($backendUser->uc['inlineView'], ['allowed_classes' => false]);
|
||||
if (!is_array($inlineView)) {
|
||||
$inlineView = [];
|
||||
}
|
||||
|
||||
return $inlineView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to check whether the backend user has the property inline view for the current IRRE item.
|
||||
* In existing or old IRRE items the attribute may not exist, then the unserialize will fail.
|
||||
*
|
||||
* @param BackendUserAuthentication $backendUser
|
||||
* @return bool
|
||||
*/
|
||||
protected function backendUserHasUcInlineView(BackendUserAuthentication $backendUser)
|
||||
{
|
||||
return !empty($backendUser->uc['inlineView']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an element from an array.
|
||||
*
|
||||
* @param mixed $needle The element to be removed.
|
||||
* @param array $haystack The array the element should be removed from.
|
||||
* @param bool $strict Search elements strictly.
|
||||
* @return array The array $haystack without the $needle
|
||||
*/
|
||||
protected function removeFromArray($needle, $haystack, $strict = false)
|
||||
{
|
||||
$pos = array_search($needle, $haystack, $strict);
|
||||
if ($pos !== false) {
|
||||
unset($haystack[$pos]);
|
||||
}
|
||||
return $haystack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an error message that transferred as JSON for AJAX calls
|
||||
*
|
||||
* @param string $message The error message to be shown
|
||||
* @return array The error message in a JSON array
|
||||
*/
|
||||
protected function getErrorMessageForAJAX($message)
|
||||
{
|
||||
return [
|
||||
'data' => $message,
|
||||
'scriptCall' => [
|
||||
'alert("' . $message . '");'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inlineFirstPid from a given objectId string
|
||||
*
|
||||
* @param string $domObjectId The id attribute of an element
|
||||
* @return int|null Pid or null
|
||||
*/
|
||||
protected function getInlineFirstPidFromDomObjectId($domObjectId)
|
||||
{
|
||||
// Substitute FlexForm addition and make parsing a bit easier
|
||||
$domObjectId = str_replace('---', ':', $domObjectId);
|
||||
// The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
|
||||
$pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
|
||||
if (preg_match($pattern, $domObjectId, $match)) {
|
||||
return $match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the config that is transferred over the wire to provide the
|
||||
* correct TCA config for the parent table
|
||||
*
|
||||
* @param string $contextString
|
||||
* @throws \RuntimeException
|
||||
* @return array
|
||||
*/
|
||||
protected function extractSignedParentConfigFromRequest(string $contextString): array
|
||||
{
|
||||
if ($contextString === '') {
|
||||
throw new \RuntimeException('Empty context string given', 1489751361);
|
||||
}
|
||||
$context = json_decode($contextString, true);
|
||||
if (empty($context['config'])) {
|
||||
throw new \RuntimeException('Empty context config section given', 1489751362);
|
||||
}
|
||||
if (!hash_equals(GeneralUtility::hmac((string)$context['config'], 'InlineContext'), (string)$context['hmac'])) {
|
||||
throw new \RuntimeException('Hash does not validate', 1489751363);
|
||||
}
|
||||
return json_decode($context['config'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaSelectTreeAjaxFieldData;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Backend controller for selectTree ajax operations
|
||||
*/
|
||||
class FormSelectTreeAjaxController
|
||||
{
|
||||
/**
|
||||
* Returns json representing category tree
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws \RuntimeException
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function fetchDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$tableName = $request->getQueryParams()['tableName'] ?? '';
|
||||
$fieldName = $request->getQueryParams()['fieldName'] ?? '';
|
||||
|
||||
// Prepare processedTca: Remove all column definitions except the one that contains
|
||||
// our tree definition. This way only this field is calculated, everything else is ignored.
|
||||
if (!isset($GLOBALS['TCA'][$tableName]) || !is_array($GLOBALS['TCA'][$tableName])) {
|
||||
throw new \RuntimeException(
|
||||
'TCA for table ' . $tableName . ' not found',
|
||||
1479386729
|
||||
);
|
||||
}
|
||||
$processedTca = $GLOBALS['TCA'][$tableName];
|
||||
if (!isset($processedTca['columns'][$fieldName]) || !is_array($processedTca['columns'][$fieldName])) {
|
||||
throw new \RuntimeException(
|
||||
'TCA for table ' . $tableName . ' and field ' . $fieldName . ' not found',
|
||||
1479386990
|
||||
);
|
||||
}
|
||||
|
||||
// Force given record type and set showitem to our field only
|
||||
$recordTypeValue = $request->getQueryParams()['recordTypeValue'];
|
||||
$processedTca['types'][$recordTypeValue]['showitem'] = $fieldName;
|
||||
// Unset all columns except our field
|
||||
$processedTca['columns'] = [
|
||||
$fieldName => $processedTca['columns'][$fieldName],
|
||||
];
|
||||
|
||||
$dataStructureIdentifier = '';
|
||||
$flexFormSheetName = '';
|
||||
$flexFormFieldName = '';
|
||||
$flexFormContainerIdentifier = '';
|
||||
$flexFormContainerFieldName = '';
|
||||
$flexSectionContainerPreparation = [];
|
||||
if ($processedTca['columns'][$fieldName]['config']['type'] === 'flex') {
|
||||
if (!empty($request->getQueryParams()['dataStructureIdentifier'])) {
|
||||
$dataStructureIdentifier = json_encode($request->getQueryParams()['dataStructureIdentifier']);
|
||||
}
|
||||
$flexFormSheetName = $request->getQueryParams()['flexFormSheetName'];
|
||||
$flexFormFieldName = $request->getQueryParams()['flexFormFieldName'];
|
||||
$flexFormContainerName = $request->getQueryParams()['flexFormContainerName'];
|
||||
$flexFormContainerIdentifier = $request->getQueryParams()['flexFormContainerIdentifier'];
|
||||
$flexFormContainerFieldName = $request->getQueryParams()['flexFormContainerFieldName'];
|
||||
$flexFormSectionContainerIsNew = (bool)$request->getQueryParams()['flexFormSectionContainerIsNew'];
|
||||
|
||||
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
|
||||
$dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
|
||||
|
||||
// Reduce given data structure down to the relevant element only
|
||||
if (empty($flexFormContainerFieldName)) {
|
||||
if (isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName])
|
||||
) {
|
||||
$dataStructure = [
|
||||
'sheets' => [
|
||||
$flexFormSheetName => [
|
||||
'ROOT' => [
|
||||
'type' => 'array',
|
||||
'el' => [
|
||||
$flexFormFieldName => $dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
} else {
|
||||
if (isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerName]
|
||||
['el'][$flexFormContainerFieldName])
|
||||
) {
|
||||
// If this is a tree in a section container that has just been added by the FlexFormAjaxController
|
||||
// "new container" action, then this container is not yet persisted, so we need to trigger the
|
||||
// TcaFlexProcess data provider again to prepare the DS and databaseRow of that container.
|
||||
if ($flexFormSectionContainerIsNew) {
|
||||
$flexSectionContainerPreparation = [
|
||||
'flexFormSheetName' => $flexFormSheetName,
|
||||
'flexFormFieldName' => $flexFormFieldName,
|
||||
'flexFormContainerName' => $flexFormContainerName,
|
||||
'flexFormContainerIdentifier' => $flexFormContainerIdentifier,
|
||||
];
|
||||
}
|
||||
// Now restrict the data structure to our tree element only
|
||||
$dataStructure = [
|
||||
'sheets' => [
|
||||
$flexFormSheetName => [
|
||||
'ROOT' => [
|
||||
'type' => 'array',
|
||||
'el' => [
|
||||
$flexFormFieldName => [
|
||||
'section' => 1,
|
||||
'type' => 'array',
|
||||
'el' => [
|
||||
$flexFormContainerName => [
|
||||
'el' => [
|
||||
$flexFormContainerFieldName => $dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerName]
|
||||
['el'][$flexFormContainerFieldName]
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
$processedTca['columns'][$fieldName]['config']['ds'] = $dataStructure;
|
||||
$processedTca['columns'][$fieldName]['config']['dataStructureIdentifier'] = $dataStructureIdentifier;
|
||||
}
|
||||
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaSelectTreeAjaxFieldData::class);
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'tableName' => $tableName,
|
||||
'vanillaUid' => (int)$request->getQueryParams()['uid'],
|
||||
'command' => $request->getQueryParams()['command'],
|
||||
'processedTca' => $processedTca,
|
||||
'recordTypeValue' => $recordTypeValue,
|
||||
'selectTreeCompileItems' => true,
|
||||
'flexSectionContainerPreparation' => $flexSectionContainerPreparation,
|
||||
];
|
||||
$formData = $formDataCompiler->compile($formDataCompilerInput);
|
||||
|
||||
if ($formData['processedTca']['columns'][$fieldName]['config']['type'] === 'flex') {
|
||||
if (empty($flexFormContainerFieldName)) {
|
||||
$treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds']
|
||||
['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]['config']['items'];
|
||||
} else {
|
||||
$treeData = $formData['processedTca']['columns'][$fieldName]['config']['ds']
|
||||
['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]
|
||||
['children'][$flexFormContainerIdentifier]
|
||||
['el'][$flexFormContainerFieldName]['config']['items'];
|
||||
}
|
||||
} else {
|
||||
$treeData = $formData['processedTca']['columns'][$fieldName]['config']['items'];
|
||||
}
|
||||
return (new JsonResponse())->setPayload($treeData ?? []);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\DataHandling\Model\RecordStateFactory;
|
||||
use TYPO3\CMS\Core\DataHandling\SlugHelper;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle FormEngine AJAX calls for Slug validation and sanitization
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class FormSlugAjaxController extends AbstractFormEngineAjaxController
|
||||
{
|
||||
|
||||
/**
|
||||
* Validates a given slug against the site and give a suggestion when it's already in use
|
||||
*
|
||||
* For new records this will look like this:
|
||||
* - If "slug" field is empty, take the other fields, and generate the slug based on the sent fields.
|
||||
* - JS: adapt the "placeholder" value only, as on save the field will be filled with the value via DataHandler
|
||||
* - If "slug" field is not empty (= "unlocked" and manually typed in)
|
||||
* - sanitize the slug
|
||||
* - If 'uniqueInSite' is set check if it's unique for the site
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
* - If 'uniqueInPid' is set check if it's unique for the pid
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
*
|
||||
* For existing records:
|
||||
* - sanitize the slug
|
||||
* - If 'uniqueInSite' is set check if it's unique for the site
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
* - If 'uniqueInPid' is set check if it's unique for the pid
|
||||
* - If not unique propose another slug and return this with the flag hasConflicts = true
|
||||
* - If the slug has changed from the existing database record (@todo)
|
||||
* - Show a message that the old URL will stop working (possibly add a redirect via checkbox)
|
||||
* - If the page has subpages, show a warning that the subpages WILL NOT BE MODIFIED and keep the OLD url
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function suggestAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->checkRequest($request);
|
||||
|
||||
$queryParameters = $request->getParsedBody() ?? [];
|
||||
$values = $queryParameters['values'];
|
||||
$mode = $queryParameters['mode'];
|
||||
$tableName = $queryParameters['tableName'];
|
||||
$pid = (int)$queryParameters['pageId'];
|
||||
$parentPageId = (int)$queryParameters['parentPageId'];
|
||||
$recordId = (int)$queryParameters['recordId'];
|
||||
$languageId = (int)$queryParameters['language'];
|
||||
$fieldName = $queryParameters['fieldName'];
|
||||
|
||||
$fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'] ?? [];
|
||||
$row = BackendUtility::getRecord($tableName, $recordId);
|
||||
$recordType = BackendUtility::getTCAtypeValue($tableName, $row);
|
||||
$columnsOverridesConfigOfField = $GLOBALS['TCA'][$tableName]['types'][$recordType]['columnsOverrides'][$fieldName]['config'] ?? null;
|
||||
if ($columnsOverridesConfigOfField) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($fieldConfig, $columnsOverridesConfigOfField);
|
||||
}
|
||||
if (empty($fieldConfig)) {
|
||||
throw new \RuntimeException(
|
||||
'No valid field configuration for table ' . $tableName . ' field name ' . $fieldName . ' found.',
|
||||
1535379534
|
||||
);
|
||||
}
|
||||
|
||||
$evalInfo = !empty($fieldConfig['eval']) ? GeneralUtility::trimExplode(',', $fieldConfig['eval'], true) : [];
|
||||
$hasToBeUniqueInDb = in_array('unique', $evalInfo, true);
|
||||
$hasToBeUniqueInSite = in_array('uniqueInSite', $evalInfo, true);
|
||||
$hasToBeUniqueInPid = in_array('uniqueInPid', $evalInfo, true);
|
||||
|
||||
$hasConflict = false;
|
||||
|
||||
$recordData = $values;
|
||||
if (!isset($recordData['uid'])) {
|
||||
$recordData['uid'] = $recordId;
|
||||
}
|
||||
$recordData['pid'] = $pid;
|
||||
if (!empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField'])) {
|
||||
$recordData[$GLOBALS['TCA'][$tableName]['ctrl']['languageField']] = $languageId;
|
||||
}
|
||||
if ($tableName === 'pages' && empty($recordData['is_siteroot'])) {
|
||||
$recordData['is_siteroot'] = $row['is_siteroot'];
|
||||
}
|
||||
|
||||
$slug = GeneralUtility::makeInstance(SlugHelper::class, $tableName, $fieldName, $fieldConfig);
|
||||
if ($mode === 'auto') {
|
||||
// New page - Feed incoming values to generator
|
||||
$proposal = $slug->generate($recordData, $pid);
|
||||
} elseif ($mode === 'recreate') {
|
||||
$proposal = $slug->generate($recordData, $parentPageId);
|
||||
} elseif ($mode === 'manual') {
|
||||
// Existing record - Fetch full record and only validate against the new "slug" field.
|
||||
$proposal = $slug->sanitize($values['manual']);
|
||||
} else {
|
||||
throw new \RuntimeException('mode must be either "auto", "recreate" or "manual"', 1535835666);
|
||||
}
|
||||
|
||||
$state = RecordStateFactory::forName($tableName)
|
||||
->fromArray($recordData, $pid, $recordId);
|
||||
if ($hasToBeUniqueInDb && !$slug->isUniqueInTable($proposal, $state)) {
|
||||
$hasConflict = true;
|
||||
$proposal = $slug->buildSlugForUniqueInTable($proposal, $state);
|
||||
}
|
||||
if ($hasToBeUniqueInSite && !$slug->isUniqueInSite($proposal, $state)) {
|
||||
$hasConflict = true;
|
||||
$proposal = $slug->buildSlugForUniqueInSite($proposal, $state);
|
||||
}
|
||||
if ($hasToBeUniqueInPid && !$slug->isUniqueInPid($proposal, $state)) {
|
||||
$hasConflict = true;
|
||||
$proposal = $slug->buildSlugForUniqueInPid($proposal, $state);
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'hasConflicts' => !$mode && $hasConflict,
|
||||
'manual' => $values['manual'] ?? '',
|
||||
'proposal' => $proposal,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return bool
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function checkRequest(ServerRequestInterface $request): bool
|
||||
{
|
||||
$queryParameters = $request->getParsedBody() ?? [];
|
||||
$expectedHash = GeneralUtility::hmac(
|
||||
implode(
|
||||
'',
|
||||
[
|
||||
$queryParameters['tableName'],
|
||||
$queryParameters['pageId'],
|
||||
$queryParameters['recordId'],
|
||||
$queryParameters['language'],
|
||||
$queryParameters['fieldName'],
|
||||
$queryParameters['command'],
|
||||
$queryParameters['parentPageId'],
|
||||
]
|
||||
),
|
||||
__CLASS__
|
||||
);
|
||||
if (!hash_equals($expectedHash, $queryParameters['signature'])) {
|
||||
throw new \InvalidArgumentException(
|
||||
'HMAC could not be verified',
|
||||
1535137045
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Domain\Repository\TableManualRepository;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
use TYPO3Fluid\Fluid\View\ViewInterface;
|
||||
|
||||
/**
|
||||
* Main "CSH help" module controller
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class HelpController
|
||||
{
|
||||
protected const ALLOWED_ACTIONS = ['index', 'all', 'detail'];
|
||||
|
||||
/**
|
||||
* Section identifiers
|
||||
*/
|
||||
const FULL = 0;
|
||||
|
||||
/**
|
||||
* Show only Table of contents
|
||||
*/
|
||||
const TOC_ONLY = 1;
|
||||
|
||||
/**
|
||||
* @var TableManualRepository
|
||||
*/
|
||||
protected $tableManualRepository;
|
||||
|
||||
/** @var ModuleTemplate */
|
||||
protected $moduleTemplate;
|
||||
|
||||
/** @var ViewInterface */
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* Instantiate the report controller
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$this->tableManualRepository = GeneralUtility::makeInstance(TableManualRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request, and renders correct action
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'index';
|
||||
|
||||
if ($action === 'detail') {
|
||||
$table = $request->getQueryParams()['table'] ?? $request->getParsedBody()['table'];
|
||||
if (!$table) {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
return new RedirectResponse((string)$uriBuilder->buildUriFromRoute('help_cshmanual', [
|
||||
'action' => 'index',
|
||||
]), 303);
|
||||
}
|
||||
}
|
||||
|
||||
if (!in_array($action, self::ALLOWED_ACTIONS, true)) {
|
||||
return new HtmlResponse('Action not allowed', 400);
|
||||
}
|
||||
|
||||
$this->initializeView($action);
|
||||
|
||||
$result = call_user_func_array([$this, $action . 'Action'], [$request]);
|
||||
if ($result instanceof ResponseInterface) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$this->registerDocheaderButtons($request);
|
||||
|
||||
$this->moduleTemplate->setContent($this->view->render());
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $templateName
|
||||
*/
|
||||
protected function initializeView(string $templateName)
|
||||
{
|
||||
$this->view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$this->view->setTemplate($templateName);
|
||||
$this->view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ContextSensitiveHelp']);
|
||||
$this->view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
|
||||
$this->view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$this->view->getRequest()->setControllerExtensionName('Backend');
|
||||
$this->view->assign('copyright', BackendUtility::TYPO3_copyRightNotice());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show table of contents
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
$this->view->assign('toc', $this->tableManualRepository->getSections(self::TOC_ONLY));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the table of contents and all manuals
|
||||
*/
|
||||
public function allAction()
|
||||
{
|
||||
$this->view->assign('all', $this->tableManualRepository->getSections(self::FULL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a single manual
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
public function detailAction(ServerRequestInterface $request)
|
||||
{
|
||||
$table = $request->getQueryParams()['table'] ?? $request->getParsedBody()['table'];
|
||||
$field = $request->getQueryParams()['field'] ?? $request->getParsedBody()['field'] ?? '*';
|
||||
|
||||
$mainKey = $table;
|
||||
|
||||
$this->view->assignMultiple([
|
||||
'table' => $table,
|
||||
'key' => $mainKey,
|
||||
'field' => $field,
|
||||
'manuals' => $field === '*'
|
||||
? $this->tableManualRepository->getTableManual($mainKey)
|
||||
: [$this->tableManualRepository->getSingleManual($mainKey, $field)],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the Icons into the docheader
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function registerDocheaderButtons(ServerRequestInterface $request)
|
||||
{
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
|
||||
if ($this->getBackendUser()->mayMakeShortcut()) {
|
||||
$shortcutButton = $buttonBar->makeShortcutButton()
|
||||
->setModuleName('help_cshmanual')
|
||||
->setGetVariables(['table', 'field', 'route']);
|
||||
$buttonBar->addButton($shortcutButton);
|
||||
}
|
||||
|
||||
$action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'index';
|
||||
if ($action !== 'index') {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$backButton = $buttonBar->makeLinkButton()
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:back'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-up', Icon::SIZE_SMALL))
|
||||
->setHref((string)$uriBuilder->buildUriFromRoute('help_cshmanual'));
|
||||
$buttonBar->addButton($backButton);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently logged in BE user
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\LinkHandling\LinkService;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Frontend\Service\TypoLinkCodecService;
|
||||
use TYPO3\CMS\Recordlist\Controller\AbstractLinkBrowserController;
|
||||
|
||||
/**
|
||||
* Extended controller for link browser
|
||||
*/
|
||||
class LinkBrowserController extends AbstractLinkBrowserController
|
||||
{
|
||||
/**
|
||||
* Initialize $this->currentLinkParts
|
||||
*/
|
||||
protected function initCurrentUrl()
|
||||
{
|
||||
$currentLink = isset($this->parameters['currentValue']) ? trim($this->parameters['currentValue']) : '';
|
||||
$currentLinkParts = GeneralUtility::makeInstance(TypoLinkCodecService::class)->decode($currentLink);
|
||||
$currentLinkParts['params'] = $currentLinkParts['additionalParams'];
|
||||
unset($currentLinkParts['additionalParams']);
|
||||
|
||||
if (!empty($currentLinkParts['url'])) {
|
||||
$linkService = GeneralUtility::makeInstance(LinkService::class);
|
||||
$data = $linkService->resolve($currentLinkParts['url']);
|
||||
$currentLinkParts['type'] = $data['type'];
|
||||
unset($data['type']);
|
||||
$currentLinkParts['url'] = $data;
|
||||
}
|
||||
|
||||
$this->currentLinkParts = $currentLinkParts;
|
||||
|
||||
parent::initCurrentUrl();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize document template object
|
||||
*/
|
||||
protected function initDocumentTemplate()
|
||||
{
|
||||
parent::initDocumentTemplate();
|
||||
|
||||
if (!$this->areFieldChangeFunctionsValid() && !$this->areFieldChangeFunctionsValid(true)) {
|
||||
$this->parameters['fieldChangeFunc'] = [];
|
||||
}
|
||||
unset($this->parameters['fieldChangeFunc']['alert']);
|
||||
$update = [];
|
||||
foreach ($this->parameters['fieldChangeFunc'] as $v) {
|
||||
$update[] = 'parent.opener.' . $v;
|
||||
}
|
||||
$inlineJS = implode(LF, $update);
|
||||
|
||||
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
|
||||
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/FormEngineLinkBrowserAdapter', 'function(FormEngineLinkBrowserAdapter) {
|
||||
FormEngineLinkBrowserAdapter.updateFunctions = function() {' . $inlineJS . '};
|
||||
}');
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typolink via ajax
|
||||
*
|
||||
* This avoids to implement the encoding functionality again in JS for the browser.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function encodeTypoLink(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$typoLinkParts = $request->getQueryParams();
|
||||
if (isset($typoLinkParts['params'])) {
|
||||
$typoLinkParts['additionalParams'] = $typoLinkParts['params'];
|
||||
unset($typoLinkParts['params']);
|
||||
}
|
||||
|
||||
$typoLink = GeneralUtility::makeInstance(TypoLinkCodecService::class)->encode($typoLinkParts);
|
||||
return new JsonResponse(['typoLink' => $typoLink]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether submitted field change functions are valid
|
||||
* and are coming from the system and not from an external abuse.
|
||||
*
|
||||
* @param bool $handleFlexformSections Whether to handle flexform sections differently
|
||||
* @return bool Whether the submitted field change functions are valid
|
||||
*/
|
||||
protected function areFieldChangeFunctionsValid($handleFlexformSections = false)
|
||||
{
|
||||
$result = false;
|
||||
if (isset($this->parameters['fieldChangeFunc']) && is_array($this->parameters['fieldChangeFunc']) && isset($this->parameters['fieldChangeFuncHash'])) {
|
||||
$matches = [];
|
||||
$pattern = '#\\[el\\]\\[(([^]-]+-[^]-]+-)(idx\\d+-)([^]]+))\\]#i';
|
||||
$fieldChangeFunctions = $this->parameters['fieldChangeFunc'];
|
||||
// Special handling of flexform sections:
|
||||
// Field change functions are modified in JavaScript, thus the hash is always invalid
|
||||
if ($handleFlexformSections && preg_match($pattern, $this->parameters['itemName'], $matches)) {
|
||||
$originalName = $matches[1];
|
||||
$cleanedName = $matches[2] . $matches[4];
|
||||
foreach ($fieldChangeFunctions as &$value) {
|
||||
$value = str_replace($originalName, $cleanedName, $value);
|
||||
}
|
||||
unset($value);
|
||||
}
|
||||
$result = hash_equals(GeneralUtility::hmac(serialize($fieldChangeFunctions), 'backend-link-browser'), $this->parameters['fieldChangeFuncHash']);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get attributes for the body tag
|
||||
*
|
||||
* @return string[] Array of body-tag attributes
|
||||
*/
|
||||
protected function getBodyTagAttributes()
|
||||
{
|
||||
$parameters = parent::getBodyTagAttributes();
|
||||
|
||||
$formEngineParameters['fieldChangeFunc'] = $this->parameters['fieldChangeFunc'];
|
||||
$formEngineParameters['fieldChangeFuncHash'] = GeneralUtility::hmac(serialize($this->parameters['fieldChangeFunc']), 'backend-link-browser');
|
||||
|
||||
$parameters['data-add-on-params'] .= HttpUtility::buildQueryString(['P' => $formEngineParameters], '&');
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of current page
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function getCurrentPageId()
|
||||
{
|
||||
$pageId = 0;
|
||||
$browserParameters = $this->parameters;
|
||||
if (isset($browserParameters['pid'])) {
|
||||
$pageId = $browserParameters['pid'];
|
||||
} elseif (isset($browserParameters['itemName'])) {
|
||||
// parse data[<table>][<uid>]
|
||||
if (preg_match('~data\[([^]]*)\]\[([^]]*)\]~', $browserParameters['itemName'], $matches)) {
|
||||
$recordArray = BackendUtility::getRecord($matches['1'], $matches['2']);
|
||||
if (is_array($recordArray)) {
|
||||
$pageId = $recordArray['pid'];
|
||||
}
|
||||
}
|
||||
}
|
||||
return (int)BackendUtility::getTSCpidCached($browserParameters['table'], $browserParameters['uid'], $pageId)[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the configuration
|
||||
* @return array
|
||||
*/
|
||||
public function getConfiguration(): array
|
||||
{
|
||||
$tsConfig = BackendUtility::getPagesTSconfig($this->getCurrentPageId());
|
||||
return $tsConfig['TCEMAIN.']['linkHandler.']['page.']['configuration.'] ?? [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Search\LiveSearch\LiveSearch;
|
||||
use TYPO3\CMS\Backend\Search\LiveSearch\QueryParser;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Returns the results for any live searches, e.g. in the toolbar
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class LiveSearchController
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $searchResults = [];
|
||||
|
||||
/**
|
||||
* Processes all AJAX calls and sends back a JSON object
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function liveSearchAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$queryString = trim($request->getQueryParams()['q']);
|
||||
$liveSearch = GeneralUtility::makeInstance(LiveSearch::class);
|
||||
$queryParser = GeneralUtility::makeInstance(QueryParser::class);
|
||||
|
||||
$searchResults = [];
|
||||
$liveSearch->setQueryString($queryString);
|
||||
// Jump & edit - find page and retrieve an edit link (this is only for pages
|
||||
if ($queryParser->isValidPageJump($queryString)) {
|
||||
$commandQuery = $queryParser->getCommandForPageJump($queryString);
|
||||
if ($commandQuery) {
|
||||
$queryString = $commandQuery;
|
||||
}
|
||||
}
|
||||
// Search through the database and find records who match to the given search string
|
||||
$resultArray = $liveSearch->find($queryString);
|
||||
foreach ($resultArray as $resultFromTable) {
|
||||
foreach ($resultFromTable as $item) {
|
||||
$searchResults[] = $item;
|
||||
}
|
||||
}
|
||||
return (new JsonResponse())->setPayload($searchResults);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,629 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use TYPO3\CMS\Backend\Exception;
|
||||
use TYPO3\CMS\Backend\LoginProvider\LoginProviderInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\DocumentTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\FormProtection\BackendFormProtection;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\NormalizedParams;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Localization\Locales;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Script Class for rendering the login form
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class LoginController implements LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* The URL to redirect to after login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectUrl;
|
||||
|
||||
/**
|
||||
* Set to the redirect URL of the form (may be redirect_url or "index.php?M=main")
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectToURL;
|
||||
|
||||
/**
|
||||
* the active login provider identifier
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $loginProviderIdentifier;
|
||||
|
||||
/**
|
||||
* List of registered and sorted login providers
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $loginProviders = [];
|
||||
|
||||
/**
|
||||
* Login-refresh bool; The backend will call this script
|
||||
* with this value set when the login is close to being expired
|
||||
* and the form needs to be redrawn.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $loginRefresh;
|
||||
|
||||
/**
|
||||
* Value of forms submit button for login.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $submitValue;
|
||||
|
||||
/**
|
||||
* @var StandaloneView
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* Initialize the login box. Will also react on a &L=OUT flag and exit.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$this->validateAndSortLoginProviders();
|
||||
|
||||
$this->redirectUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['redirect_url'] ?? $queryParams['redirect_url'] ?? null);
|
||||
$this->loginProviderIdentifier = $this->detectLoginProvider($request);
|
||||
|
||||
$this->loginRefresh = (bool)($parsedBody['loginRefresh'] ?? $queryParams['loginRefresh'] ?? false);
|
||||
// Value of "Login" button. If set, the login button was pressed.
|
||||
$this->submitValue = $parsedBody['commandLI'] ?? $queryParams['commandLI'] ?? null;
|
||||
// Try to get the preferred browser language
|
||||
/** @var Locales $locales */
|
||||
$locales = GeneralUtility::makeInstance(Locales::class);
|
||||
$httpAcceptLanguage = $request->getServerParams()['HTTP_ACCEPT_LANGUAGE'];
|
||||
$preferredBrowserLanguage = $locales
|
||||
->getPreferredClientLanguage($httpAcceptLanguage);
|
||||
|
||||
// If we found a $preferredBrowserLanguage and it is not the default language and no be_user is logged in
|
||||
// initialize $this->getLanguageService() again with $preferredBrowserLanguage
|
||||
if ($preferredBrowserLanguage !== 'default' && empty($this->getBackendUserAuthentication()->user['uid'])) {
|
||||
$this->getLanguageService()->init($preferredBrowserLanguage);
|
||||
GeneralUtility::makeInstance(PageRenderer::class)->setLanguage($preferredBrowserLanguage);
|
||||
}
|
||||
|
||||
$this->getLanguageService()->includeLLFile('EXT:backend/Resources/Private/Language/locallang_login.xlf');
|
||||
|
||||
// Setting the redirect URL to "index.php?M=main" if no alternative input is given
|
||||
if ($this->redirectUrl) {
|
||||
$this->redirectToURL = $this->redirectUrl;
|
||||
} else {
|
||||
// (consolidate RouteDispatcher::evaluateReferrer() when changing 'main' to something different)
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$this->redirectToURL = (string)$uriBuilder->buildUriFromRoute('main');
|
||||
}
|
||||
|
||||
// If "L" is "OUT", then any logged in is logged out. If redirect_url is given, we redirect to it
|
||||
if (($parsedBody['L'] ?? $queryParams['L'] ?? null) === 'OUT' && is_object($this->getBackendUserAuthentication())) {
|
||||
$this->getBackendUserAuthentication()->logoff();
|
||||
$this->redirectToUrl();
|
||||
}
|
||||
|
||||
$this->view = $this->getFluidTemplateObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request and response objects for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the finished response with the content
|
||||
*/
|
||||
public function formAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return new HtmlResponse($this->createLoginLogoutForm($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the main function but with loginRefresh enabled at any time
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the finished response with the content
|
||||
*/
|
||||
public function refreshAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->loginRefresh = true;
|
||||
return new HtmlResponse($this->createLoginLogoutForm($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function - creating the login/logout form
|
||||
*
|
||||
* @throws Exception
|
||||
* @return string The content to output
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main(): string
|
||||
{
|
||||
trigger_error('LoginController->main() will be replaced by protected method createLoginLogoutForm() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->createLoginLogoutForm($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function - creating the login/logout form
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return string $content
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function createLoginLogoutForm(ServerRequestInterface $request): string
|
||||
{
|
||||
/** @var PageRenderer $pageRenderer */
|
||||
$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
|
||||
$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Login');
|
||||
|
||||
// Checking, if we should make a redirect.
|
||||
// Might set JavaScript in the header to close window.
|
||||
$this->checkRedirect($request);
|
||||
|
||||
// Extension Configuration
|
||||
$extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('backend');
|
||||
|
||||
// Background Image
|
||||
if (!empty($extConf['loginBackgroundImage'])) {
|
||||
$backgroundImage = $this->getUriForFileName($extConf['loginBackgroundImage']);
|
||||
if ($backgroundImage === '') {
|
||||
$this->logger->warning(
|
||||
'The configured TYPO3 backend login background image "' . htmlspecialchars($extConf['loginBackgroundImage']) .
|
||||
'" can\'t be resolved. Please check if the file exists and the extension is activated.'
|
||||
);
|
||||
}
|
||||
$this->getDocumentTemplate()->inDocStylesArray[] = '
|
||||
.typo3-login-carousel-control.right,
|
||||
.typo3-login-carousel-control.left,
|
||||
.panel-login { border: 0; }
|
||||
.typo3-login { background-image: url("' . $backgroundImage . '"); }
|
||||
.typo3-login-footnote { background-color: #000000; color: #ffffff; opacity: 0.5; }
|
||||
';
|
||||
}
|
||||
|
||||
// Login Footnote
|
||||
if (!empty($extConf['loginFootnote'])) {
|
||||
$this->view->assign('loginFootnote', strip_tags(trim($extConf['loginFootnote'])));
|
||||
}
|
||||
|
||||
// Add additional css to use the highlight color in the login screen
|
||||
if (!empty($extConf['loginHighlightColor'])) {
|
||||
$this->getDocumentTemplate()->inDocStylesArray[] = '
|
||||
.btn-login.disabled, .btn-login[disabled], fieldset[disabled] .btn-login,
|
||||
.btn-login.disabled:hover, .btn-login[disabled]:hover, fieldset[disabled] .btn-login:hover,
|
||||
.btn-login.disabled:focus, .btn-login[disabled]:focus, fieldset[disabled] .btn-login:focus,
|
||||
.btn-login.disabled.focus, .btn-login[disabled].focus, fieldset[disabled] .btn-login.focus,
|
||||
.btn-login.disabled:active, .btn-login[disabled]:active, fieldset[disabled] .btn-login:active,
|
||||
.btn-login.disabled.active, .btn-login[disabled].active, fieldset[disabled] .btn-login.active,
|
||||
.btn-login:hover, .btn-login:focus, .btn-login:active,
|
||||
.btn-login:active:hover, .btn-login:active:focus,
|
||||
.btn-login { background-color: ' . $extConf['loginHighlightColor'] . '; }
|
||||
.panel-login .panel-body { border-color: ' . $extConf['loginHighlightColor'] . '; }
|
||||
';
|
||||
}
|
||||
|
||||
// Logo
|
||||
if (!empty($extConf['loginLogo'])) {
|
||||
if ($this->getUriForFileName($extConf['loginLogo']) === '') {
|
||||
$this->logger->warning(
|
||||
'The configured TYPO3 backend login logo "' . htmlspecialchars($extConf['loginLogo']) .
|
||||
'" can\'t be resolved. Please check if the file exists and the extension is activated.'
|
||||
);
|
||||
}
|
||||
$logo = $extConf['loginLogo'];
|
||||
} else {
|
||||
// Use TYPO3 logo depending on highlight color
|
||||
if (!empty($extConf['loginHighlightColor'])) {
|
||||
$logo = 'EXT:backend/Resources/Public/Images/typo3_black.svg';
|
||||
} else {
|
||||
$logo = 'EXT:backend/Resources/Public/Images/typo3_orange.svg';
|
||||
}
|
||||
$this->getDocumentTemplate()->inDocStylesArray[] = '
|
||||
.typo3-login-logo .typo3-login-image { max-width: 150px; height:100%;}
|
||||
';
|
||||
}
|
||||
$logo = $this->getUriForFileName($logo);
|
||||
|
||||
// Start form
|
||||
$formType = empty($this->getBackendUserAuthentication()->user['uid']) ? 'LoginForm' : 'LogoutForm';
|
||||
$this->view->assignMultiple([
|
||||
'backendUser' => $this->getBackendUserAuthentication()->user,
|
||||
'hasLoginError' => $this->isLoginInProgress($request),
|
||||
'formType' => $formType,
|
||||
'logo' => $logo,
|
||||
'images' => [
|
||||
'capslock' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/icon_capslock.svg'),
|
||||
'typo3' => $this->getUriForFileName('EXT:backend/Resources/Public/Images/typo3_orange.svg'),
|
||||
],
|
||||
'copyright' => BackendUtility::TYPO3_copyRightNotice(),
|
||||
'redirectUrl' => $this->redirectUrl,
|
||||
'loginRefresh' => $this->loginRefresh,
|
||||
'loginNewsItems' => $this->getSystemNews(),
|
||||
'referrerCheckEnabled' => GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('security.backend.enforceReferrer'),
|
||||
'loginUrl' => (string)$request->getUri(),
|
||||
'loginProviderIdentifier' => $this->loginProviderIdentifier,
|
||||
'loginProviders' => $this->loginProviders
|
||||
]);
|
||||
|
||||
// Initialize interface selectors:
|
||||
$this->makeInterfaceSelector($request);
|
||||
|
||||
/** @var LoginProviderInterface $loginProvider */
|
||||
$loginProvider = GeneralUtility::makeInstance($this->loginProviders[$this->loginProviderIdentifier]['provider']);
|
||||
$loginProvider->render($this->view, $pageRenderer, $this);
|
||||
|
||||
$content = $this->getDocumentTemplate()->startPage('TYPO3 CMS Login: ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename']);
|
||||
$content .= $this->view->render();
|
||||
$content .= $this->getDocumentTemplate()->endPage();
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checking, if we should perform some sort of redirection OR closing of windows.
|
||||
*
|
||||
* Do a redirect if a user is logged in
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws \RuntimeException
|
||||
* @throws \UnexpectedValueException
|
||||
* @throws \TYPO3\CMS\Backend\Routing\Exception\RouteNotFoundException
|
||||
*/
|
||||
protected function checkRedirect(ServerRequestInterface $request): void
|
||||
{
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
if (empty($backendUser->user['uid'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If no cookie has been set previously, we tell people that this is a problem.
|
||||
* This assumes that a cookie-setting script (like this one) has been hit at
|
||||
* least once prior to this instance.
|
||||
*/
|
||||
if (!isset($_COOKIE[BackendUserAuthentication::getCookieName()])) {
|
||||
if ($this->submitValue === 'setCookie') {
|
||||
/*
|
||||
* we tried it a second time but still no cookie
|
||||
* 26/4 2005: This does not work anymore, because the saving of challenge values
|
||||
* in $_SESSION means the system will act as if the password was wrong.
|
||||
*/
|
||||
throw new \RuntimeException('Login-error: Yeah, that\'s a classic. No cookies, no TYPO3. ' .
|
||||
'Please accept cookies from TYPO3 - otherwise you\'ll not be able to use the system.', 1294586846);
|
||||
}
|
||||
// try it once again - that might be needed for auto login
|
||||
$this->redirectToURL = 'index.php?commandLI=setCookie';
|
||||
}
|
||||
$redirectToUrl = (string)($backendUser->getTSConfig()['auth.']['BE.']['redirectToURL'] ?? '');
|
||||
if (empty($redirectToUrl)) {
|
||||
// Based on the interface we set the redirect script
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$interface = $parsedBody['interface'] ?? $queryParams['interface'] ?? '';
|
||||
switch ($interface) {
|
||||
case 'frontend':
|
||||
$this->redirectToURL = '../';
|
||||
break;
|
||||
case 'backend':
|
||||
// (consolidate RouteDispatcher::evaluateReferrer() when changing 'main' to something different)
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$this->redirectToURL = (string)$uriBuilder->buildUriFromRoute('main');
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$this->redirectToURL = $redirectToUrl;
|
||||
$interface = '';
|
||||
}
|
||||
// store interface
|
||||
$backendUser->uc['interfaceSetup'] = $interface;
|
||||
$backendUser->writeUC();
|
||||
|
||||
$formProtection = FormProtectionFactory::get();
|
||||
if (!$formProtection instanceof BackendFormProtection) {
|
||||
throw new \RuntimeException('The Form Protection retrieved does not match the expected one.', 1432080411);
|
||||
}
|
||||
if ($this->loginRefresh) {
|
||||
$formProtection->setSessionTokenFromRegistry();
|
||||
$formProtection->persistSessionToken();
|
||||
$this->getDocumentTemplate()->JScode .= GeneralUtility::wrapJS('
|
||||
if (window.opener && window.opener.TYPO3 && window.opener.TYPO3.LoginRefresh) {
|
||||
window.opener.TYPO3.LoginRefresh.startTask();
|
||||
window.close();
|
||||
}
|
||||
');
|
||||
} else {
|
||||
$formProtection->storeSessionTokenInRegistry();
|
||||
$this->redirectToUrl();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Making interface selector
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function makeInterfaceSelectorBox(): void
|
||||
{
|
||||
trigger_error('LoginController->makeInterfaceSelectorBox() will be replaced by protected method makeInterfaceSelector() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->makeInterfaceSelector($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Making interface selector
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function makeInterfaceSelector(ServerRequestInterface $request): void
|
||||
{
|
||||
// If interfaces are defined AND no input redirect URL in GET vars:
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['interfaces'] && ($this->isLoginInProgress($request) || !$this->redirectUrl)) {
|
||||
$parts = GeneralUtility::trimExplode(',', $GLOBALS['TYPO3_CONF_VARS']['BE']['interfaces']);
|
||||
if (count($parts) > 1) {
|
||||
// Only if more than one interface is defined we will show the selector
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$interfaces = [
|
||||
'backend' => [
|
||||
'label' => $this->getLanguageService()->getLL('interface.backend'),
|
||||
'jumpScript' => (string)$uriBuilder->buildUriFromRoute('main'),
|
||||
'interface' => 'backend'
|
||||
],
|
||||
'frontend' => [
|
||||
'label' => $this->getLanguageService()->getLL('interface.frontend'),
|
||||
'jumpScript' => '../',
|
||||
'interface' => 'frontend'
|
||||
]
|
||||
];
|
||||
|
||||
$this->view->assign('showInterfaceSelector', true);
|
||||
$this->view->assign('interfaces', $interfaces);
|
||||
} elseif (!$this->redirectUrl) {
|
||||
// If there is only ONE interface value set and no redirect_url is present
|
||||
$this->view->assign('showInterfaceSelector', false);
|
||||
$this->view->assign('interface', $parts[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets news from sys_news and converts them into a format suitable for
|
||||
* showing them at the login screen.
|
||||
*
|
||||
* @return array An array of login news.
|
||||
*/
|
||||
protected function getSystemNews(): array
|
||||
{
|
||||
$systemNewsTable = 'sys_news';
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable($systemNewsTable);
|
||||
$systemNews = [];
|
||||
$systemNewsRecords = $queryBuilder
|
||||
->select('title', 'content', 'crdate')
|
||||
->from($systemNewsTable)
|
||||
->orderBy('crdate', 'DESC')
|
||||
->execute()
|
||||
->fetchAll();
|
||||
foreach ($systemNewsRecords as $systemNewsRecord) {
|
||||
$systemNews[] = [
|
||||
'date' => date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$systemNewsRecord['crdate']),
|
||||
'header' => $systemNewsRecord['title'],
|
||||
'content' => $systemNewsRecord['content']
|
||||
];
|
||||
}
|
||||
return $systemNews;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the uri of a relative reference, resolves the "EXT:" prefix
|
||||
* (way of referring to files inside extensions) and checks that the file is inside
|
||||
* the project root of the TYPO3 installation
|
||||
*
|
||||
* @param string $filename The input filename/filepath to evaluate
|
||||
* @return string Returns the filename of $filename if valid, otherwise blank string.
|
||||
* @internal
|
||||
*/
|
||||
private function getUriForFileName($filename): string
|
||||
{
|
||||
// Check if it's already a URL
|
||||
if (preg_match('/^(https?:)?\/\//', $filename)) {
|
||||
return $filename;
|
||||
}
|
||||
$absoluteFilename = GeneralUtility::getFileAbsFileName(ltrim($filename, '/'));
|
||||
$filename = '';
|
||||
if ($absoluteFilename !== '' && @is_file($absoluteFilename)) {
|
||||
$filename = PathUtility::getAbsoluteWebPath($absoluteFilename);
|
||||
}
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if login credentials are currently submitted
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return bool
|
||||
*/
|
||||
protected function isLoginInProgress(ServerRequestInterface $request): bool
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$username = $parsedBody['username'] ?? $queryParams['username'] ?? null;
|
||||
return !empty($username) || !empty($this->submitValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a new standalone view, shorthand function
|
||||
*
|
||||
* @return StandaloneView
|
||||
*/
|
||||
protected function getFluidTemplateObject()
|
||||
{
|
||||
/** @var StandaloneView $view */
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts')]);
|
||||
$view->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials')]);
|
||||
$view->setTemplateRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates')]);
|
||||
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper method to redirect to configured redirect URL
|
||||
*/
|
||||
protected function redirectToUrl(): void
|
||||
{
|
||||
HttpUtility::redirect($this->redirectToURL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the registered login providers
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function validateAndSortLoginProviders()
|
||||
{
|
||||
$providers = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['backend']['loginProviders'] ?? [];
|
||||
if (empty($providers) || !is_array($providers)) {
|
||||
throw new \RuntimeException('No login providers are registered in $GLOBALS[\'TYPO3_CONF_VARS\'][\'EXTCONF\'][\'backend\'][\'loginProviders\'].', 1433417281);
|
||||
}
|
||||
foreach ($providers as $identifier => $configuration) {
|
||||
if (empty($configuration) || !is_array($configuration)) {
|
||||
throw new \RuntimeException('Missing configuration for login provider "' . $identifier . '".', 1433416043);
|
||||
}
|
||||
if (!is_string($configuration['provider']) || empty($configuration['provider']) || !class_exists($configuration['provider']) || !is_subclass_of($configuration['provider'], LoginProviderInterface::class)) {
|
||||
throw new \RuntimeException('The login provider "' . $identifier . '" defines an invalid provider. Ensure the class exists and implements the "' . LoginProviderInterface::class . '".', 1460977275);
|
||||
}
|
||||
if (empty($configuration['label'])) {
|
||||
throw new \RuntimeException('Missing label definition for login provider "' . $identifier . '".', 1433416044);
|
||||
}
|
||||
if (empty($configuration['icon-class'])) {
|
||||
throw new \RuntimeException('Missing icon definition for login provider "' . $identifier . '".', 1433416045);
|
||||
}
|
||||
if (!isset($configuration['sorting'])) {
|
||||
throw new \RuntimeException('Missing sorting definition for login provider "' . $identifier . '".', 1433416046);
|
||||
}
|
||||
}
|
||||
// sort providers
|
||||
uasort($providers, function ($a, $b) {
|
||||
return $b['sorting'] - $a['sorting'];
|
||||
});
|
||||
$this->loginProviders = $providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect the login provider, get from request or choose the
|
||||
* first one as default
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return string
|
||||
*/
|
||||
protected function detectLoginProvider(ServerRequestInterface $request): string
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$loginProvider = $parsedBody['loginProvider'] ?? $queryParams['loginProvider'] ?? '';
|
||||
if ((empty($loginProvider) || !isset($this->loginProviders[$loginProvider])) && !empty($_COOKIE['be_lastLoginProvider'])) {
|
||||
$loginProvider = $_COOKIE['be_lastLoginProvider'];
|
||||
}
|
||||
if (empty($loginProvider) || !isset($this->loginProviders[$loginProvider])) {
|
||||
reset($this->loginProviders);
|
||||
$loginProvider = key($this->loginProviders);
|
||||
}
|
||||
// Use the secure option when the current request is served by a secure connection
|
||||
/** @var NormalizedParams $normalizedParams */
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$isHttps = $normalizedParams->isHttps();
|
||||
$cookieSecure = (bool)$GLOBALS['TYPO3_CONF_VARS']['SYS']['cookieSecure'] && $isHttps;
|
||||
$cookie = new Cookie(
|
||||
'be_lastLoginProvider',
|
||||
(string)$loginProvider,
|
||||
$GLOBALS['EXEC_TIME'] + 7776000, // 90 days
|
||||
$normalizedParams->getSitePath() . TYPO3_mainDir,
|
||||
'',
|
||||
$cookieSecure,
|
||||
true,
|
||||
false,
|
||||
Cookie::SAMESITE_STRICT
|
||||
);
|
||||
header('Set-Cookie: ' . $cookie->__toString(), false);
|
||||
|
||||
return (string)$loginProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLoginProviderIdentifier()
|
||||
{
|
||||
return $this->loginProviderIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of DocumentTemplate
|
||||
*
|
||||
* @return DocumentTemplate
|
||||
*/
|
||||
protected function getDocumentTemplate(): DocumentTemplate
|
||||
{
|
||||
return $GLOBALS['TBE_TEMPLATE'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\DocumentTemplate;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Page\PageRenderer;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Script Class, putting the frameset together.
|
||||
* @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0. All logic is moved into LoginController.
|
||||
*/
|
||||
class LoginFramesetController
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
trigger_error(__CLASS__ . ' will be removed in TYPO3 v10.0. Request "index.php?loginRefresh=1" directly to work without the frameset.', E_USER_DEPRECATED);
|
||||
$GLOBALS['SOBE'] = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->createFrameset();
|
||||
return new HtmlResponse($this->content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function.
|
||||
* Creates the header code and the frameset for the two frames.
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
$this->createFrameset();
|
||||
}
|
||||
/**
|
||||
* Main function.
|
||||
* Creates the header code and the frameset for the two frames.
|
||||
*/
|
||||
protected function createFrameset(): void
|
||||
{
|
||||
$title = 'TYPO3 Re-Login (' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] . ')';
|
||||
$this->getDocumentTemplate()->startPage($title);
|
||||
/** @var UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
// Create the frameset for the window
|
||||
$this->content = $this->getPageRenderer()->render(PageRenderer::PART_HEADER) . '
|
||||
<frameset rows="*,1">
|
||||
<frame name="login" src="index.php?loginRefresh=1" marginwidth="0" marginheight="0" scrolling="no" noresize="noresize" />
|
||||
<frame name="dummy" src="' . htmlspecialchars((string)$uriBuilder->buildUriFromRoute('dummy')) . '" marginwidth="0" marginheight="0" scrolling="auto" noresize="noresize" />
|
||||
</frameset>
|
||||
</html>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of DocumentTemplate
|
||||
*
|
||||
* @return DocumentTemplate
|
||||
*/
|
||||
protected function getDocumentTemplate(): DocumentTemplate
|
||||
{
|
||||
return $GLOBALS['TBE_TEMPLATE'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return PageRenderer
|
||||
*/
|
||||
protected function getPageRenderer(): PageRenderer
|
||||
{
|
||||
return GeneralUtility::makeInstance(PageRenderer::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\FormProtection\FormProtectionFactory;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Script Class for logging a user out.
|
||||
* Does not display any content, just calls the logout-function for the current user and then makes a redirect.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class LogoutController
|
||||
{
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
* This will be split up in an abstract controller once proper routing/dispatcher is in place.
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function logoutAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->processLogout();
|
||||
|
||||
$redirectUrl = $request->getParsedBody()['redirect'] ?? $request->getQueryParams()['redirect'];
|
||||
$redirectUrl = GeneralUtility::sanitizeLocalUrl($redirectUrl);
|
||||
if (empty($redirectUrl)) {
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$redirectUrl = (string)$uriBuilder->buildUriFromRoute('login', [], $uriBuilder::ABSOLUTE_URL);
|
||||
}
|
||||
return new RedirectResponse(GeneralUtility::locationHeaderUrl($redirectUrl), 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the logout processing
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function logout()
|
||||
{
|
||||
trigger_error('LogoutController->logout() will be replaced by protected method processLogout() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->processLogout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the logout processing
|
||||
*/
|
||||
protected function processLogout(): void
|
||||
{
|
||||
if (empty($this->getBackendUser()->user['username'])) {
|
||||
return;
|
||||
}
|
||||
// Logout written to log
|
||||
$this->getBackendUser()->writelog(255, 2, 0, 1, 'User %s logged out from TYPO3 Backend', [$this->getBackendUser()->user['username']]);
|
||||
/** @var \TYPO3\CMS\Core\FormProtection\BackendFormProtection $backendFormProtection */
|
||||
$backendFormProtection = FormProtectionFactory::get();
|
||||
$backendFormProtection->removeSessionTokenFromRegistry();
|
||||
$this->getBackendUser()->logoff();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,941 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Tree\View\NewRecordPageTreeView;
|
||||
use TYPO3\CMS\Backend\Tree\View\PagePositionMap;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Frontend\Page\PageRepository;
|
||||
|
||||
/**
|
||||
* Script class for 'db_new'
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class NewRecordController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'pageinfo' => 'Using $pageinfo of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'pidInfo' => 'Using $pidInfo of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'newPagesInto' => 'Using $newPagesInto of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'newContentInto' => 'Using $newContentInto of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'newPagesAfter' => 'Using $newPagesAfter of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'web_list_modTSconfig' => 'Using $web_list_modTSconfig of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'allowedNewTables' => 'Using $allowedNewTables of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'deniedNewTables' => 'Using $deniedNewTables of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'web_list_modTSconfig_pid' => 'Using $web_list_modTSconfig_pid of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'allowedNewTables_pid' => 'Using $allowedNewTables_pid of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'deniedNewTables_pid' => 'Using $deniedNewTables_pid of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'code' => 'Using $code of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'R_URI' => 'Using $R_URI of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'returnUrl' => 'Using $returnUrl of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'pagesOnly' => 'Using $pagesOnly of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'perms_clause' => 'Using $perms_clause of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'content' => 'Using $content of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
'tRows' => 'Using $tRows of class NewRecordController from outside is discouraged as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pageinfo = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $pidInfo = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $newRecordSortList;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $newPagesInto;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $newContentInto;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $newPagesAfter;
|
||||
|
||||
/**
|
||||
* Determines, whether "Select Position" for new page should be shown
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $newPagesSelectPosition = true;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $web_list_modTSconfig;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $allowedNewTables;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $deniedNewTables;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $web_list_modTSconfig_pid;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $allowedNewTables_pid;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $deniedNewTables_pid;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $R_URI;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @see \TYPO3\CMS\Backend\Tree\View\NewRecordPageTreeView::expandNext()
|
||||
* @internal
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $returnUrl;
|
||||
|
||||
/**
|
||||
* pagesOnly flag.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $pagesOnly;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $perms_clause;
|
||||
|
||||
/**
|
||||
* Accumulated HTML output
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $tRows;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_misc.xlf');
|
||||
|
||||
// @see \TYPO3\CMS\Backend\Tree\View\NewRecordPageTreeView::expandNext()
|
||||
$GLOBALS['SOBE'] = $this;
|
||||
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$response = $this->renderContent($request);
|
||||
|
||||
if (empty($response)) {
|
||||
$response = new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main processing, creating the list of new record tables to select from
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('NewRecordController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
|
||||
$response = $this->renderContent($GLOBALS['TYPO3_REQUEST']);
|
||||
|
||||
if ($response instanceof RedirectResponse) {
|
||||
HttpUtility::redirect($response->getHeaders()['location'][0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the position map for pages wizard
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function pagesOnly()
|
||||
{
|
||||
trigger_error('NewRecordController->pagesOnly() will be replaced by protected method renderPositionTree() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->renderPositionTree();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a regular new element (pages and records)
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function regularNew()
|
||||
{
|
||||
trigger_error('NewRecordController->regularNew() will be replaced by protected method renderNewRecordControls() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->renderNewRecordControls($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* User array sort function used by renderNewRecordControls
|
||||
*
|
||||
* @param string $a First array element for compare
|
||||
* @param string $b First array element for compare
|
||||
* @return int -1 for lower, 0 for equal, 1 for greater
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function sortNewRecordsByConfig($a, $b)
|
||||
{
|
||||
trigger_error('NewRecordController->sortNewRecordsByConfig() will be replaced by protected method sortTableRows() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->sortTableRows($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the string $code to a create-new form for a record in $table created on page $pid
|
||||
*
|
||||
* @param string $linkText Link text
|
||||
* @param string $table Table name (in which to create new record)
|
||||
* @param int $pid PID value for the "&edit['.$table.']['.$pid.']=new" command (positive/negative)
|
||||
* @param bool $addContentTable If $addContentTable is set, then a new tt_content record is created together with pages
|
||||
* @return string The link.
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function linkWrap($linkText, $table, $pid, $addContentTable = false)
|
||||
{
|
||||
trigger_error('NewRecordController->linkWrap() will be replaced by protected method renderLink() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->renderLink($linkText, $table, $pid, $addContentTable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row
|
||||
*
|
||||
* @param array $pid_row Record for parent page.
|
||||
* @param string $checkTable Table name to check
|
||||
* @return bool Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function isTableAllowedForThisPage($pid_row, $checkTable)
|
||||
{
|
||||
trigger_error('NewRecordController->isTableAllowedForThisPage() will be replaced by protected method isTableAllowedOnPage() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->isTableAllowedOnPage($checkTable, $pid_row);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if:
|
||||
* - $allowedNewTables and $deniedNewTables are empty
|
||||
* - the table is not found in $deniedNewTables and $allowedNewTables is not set or the $table tablename is found in
|
||||
* $allowedNewTables
|
||||
*
|
||||
* If $table tablename is found in $allowedNewTables and $deniedNewTables, $deniedNewTables
|
||||
* has priority over $allowedNewTables.
|
||||
*
|
||||
* @param string $table Table name to test if in allowedTables
|
||||
* @param array $allowedNewTables Array of new tables that are allowed.
|
||||
* @param array $deniedNewTables Array of new tables that are not allowed.
|
||||
* @return bool Returns TRUE if a link for creating new records should be displayed for $table
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function showNewRecLink($table, array $allowedNewTables = [], array $deniedNewTables = [])
|
||||
{
|
||||
trigger_error('NewRecordController->showNewRecLink() will be replaced by protected method isRecordCreationAllowedForTable() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
return $this->isRecordCreationAllowedForTable($table, $allowedNewTables, $deniedNewTables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor function for the class
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$beUser = $this->getBackendUserAuthentication();
|
||||
// Page-selection permission clause (reading)
|
||||
$this->perms_clause = $beUser->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
// This will hide records from display - it has nothing to do with user rights!!
|
||||
$pidList = $beUser->getTSConfig()['options.']['hideRecords.']['pages'] ?? '';
|
||||
if (!empty($pidList)) {
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('pages');
|
||||
$this->perms_clause .= ' AND ' . $queryBuilder->expr()->notIn(
|
||||
'pages.uid',
|
||||
GeneralUtility::intExplode(',', $pidList)
|
||||
);
|
||||
}
|
||||
// Setting GPvars:
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
// The page id to operate from
|
||||
$this->id = (int)($parsedBody['id'] ?? $queryParams['id'] ?? 0);
|
||||
$this->returnUrl = GeneralUtility::sanitizeLocalUrl($parsedBody['returnUrl'] ?? $queryParams['returnUrl'] ?? '');
|
||||
$this->pagesOnly = $parsedBody['pagesOnly'] ?? $queryParams['pagesOnly'] ?? null;
|
||||
// Setting up the context sensitive menu:
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/PageActions');
|
||||
// Creating content
|
||||
$this->content = '';
|
||||
$this->content .= '<h1>'
|
||||
. $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.pagetitle')
|
||||
. '</h1>';
|
||||
// Id a positive id is supplied, ask for the page record with permission information contained:
|
||||
if ($this->id > 0) {
|
||||
$this->pageinfo = BackendUtility::readPageAccess($this->id, $this->perms_clause);
|
||||
}
|
||||
// If a page-record was returned, the user had read-access to the page.
|
||||
if ($this->pageinfo['uid']) {
|
||||
// Get record of parent page
|
||||
$this->pidInfo = BackendUtility::getRecord('pages', $this->pageinfo['pid']) ?: [];
|
||||
// Checking the permissions for the user with regard to the parent page: Can he create new pages, new
|
||||
// content record, new page after?
|
||||
if ($beUser->doesUserHaveAccess($this->pageinfo, 8)) {
|
||||
$this->newPagesInto = 1;
|
||||
}
|
||||
if ($beUser->doesUserHaveAccess($this->pageinfo, 16)) {
|
||||
$this->newContentInto = 1;
|
||||
}
|
||||
if (($beUser->isAdmin() || !empty($this->pidInfo)) && $beUser->doesUserHaveAccess($this->pidInfo, 8)) {
|
||||
$this->newPagesAfter = 1;
|
||||
}
|
||||
} elseif ($beUser->isAdmin()) {
|
||||
// Admins can do it all
|
||||
$this->newPagesInto = 1;
|
||||
$this->newContentInto = 1;
|
||||
$this->newPagesAfter = 0;
|
||||
} else {
|
||||
// People with no permission can do nothing
|
||||
$this->newPagesInto = 0;
|
||||
$this->newContentInto = 0;
|
||||
$this->newPagesAfter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main processing, creating the list of new record tables to select from
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface|null
|
||||
*/
|
||||
protected function renderContent(ServerRequestInterface $request): ?ResponseInterface
|
||||
{
|
||||
// If there was a page - or if the user is admin (admins has access to the root) we proceed:
|
||||
if (!empty($this->pageinfo['uid']) || $this->getBackendUserAuthentication()->isAdmin()) {
|
||||
if (empty($this->pageinfo)) {
|
||||
// Explicitly pass an empty array to the docHeader
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation([]);
|
||||
} else {
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($this->pageinfo);
|
||||
}
|
||||
// Acquiring TSconfig for this module/current page:
|
||||
$this->web_list_modTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid'])['mod.']['web_list.'] ?? [];
|
||||
$this->allowedNewTables = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig['allowedNewTables'] ?? '', true);
|
||||
$this->deniedNewTables = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig['deniedNewTables'] ?? '', true);
|
||||
// Acquiring TSconfig for this module/parent page:
|
||||
$this->web_list_modTSconfig_pid = BackendUtility::getPagesTSconfig($this->pageinfo['pid'])['mod.']['web_list.'] ?? [];
|
||||
$this->allowedNewTables_pid = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig_pid['allowedNewTables'] ?? '', true);
|
||||
$this->deniedNewTables_pid = GeneralUtility::trimExplode(',', $this->web_list_modTSconfig_pid['deniedNewTables'] ?? '', true);
|
||||
// More init:
|
||||
if (!$this->isRecordCreationAllowedForTable('pages')) {
|
||||
$this->newPagesInto = 0;
|
||||
}
|
||||
if (!$this->isRecordCreationAllowedForTable('pages', $this->allowedNewTables_pid, $this->deniedNewTables_pid)) {
|
||||
$this->newPagesAfter = 0;
|
||||
}
|
||||
// Set header-HTML and return_url
|
||||
if (is_array($this->pageinfo) && $this->pageinfo['uid']) {
|
||||
$title = strip_tags($this->pageinfo[$GLOBALS['TCA']['pages']['ctrl']['label']]);
|
||||
} else {
|
||||
$title = $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'];
|
||||
}
|
||||
$this->moduleTemplate->setTitle($title);
|
||||
// GENERATE the HTML-output depending on mode (pagesOnly is the page wizard)
|
||||
// Regular new element:
|
||||
if (!$this->pagesOnly) {
|
||||
$this->renderNewRecordControls($request);
|
||||
} elseif ($this->isRecordCreationAllowedForTable('pages')) {
|
||||
// Pages only wizard
|
||||
$response = $this->renderPositionTree();
|
||||
|
||||
if (!empty($response)) {
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
// Add all the content to an output section
|
||||
$this->content .= '<div>' . $this->code . '</div>';
|
||||
// Setting up the buttons and markers for docheader
|
||||
$this->getButtons();
|
||||
// Build the <body> for the module
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the panel of buttons for submitting the form or otherwise perform operations.
|
||||
*/
|
||||
protected function getButtons(): void
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
// Regular new element:
|
||||
if (!$this->pagesOnly) {
|
||||
// New page
|
||||
if ($this->isRecordCreationAllowedForTable('pages')) {
|
||||
$newPageButton = $buttonBar->makeLinkButton()
|
||||
->setHref(GeneralUtility::linkThisScript(['pagesOnly' => '1']))
|
||||
->setTitle($lang->sL('LLL:EXT:backend/Resources/Private/Language/locallang_layout.xlf:newPage'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($newPageButton, ButtonBar::BUTTON_POSITION_LEFT, 20);
|
||||
}
|
||||
// CSH
|
||||
$cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('new_regular');
|
||||
$buttonBar->addButton($cshButton);
|
||||
} elseif ($this->isRecordCreationAllowedForTable('pages')) {
|
||||
// Pages only wizard
|
||||
// CSH
|
||||
$buttons['csh'] = BackendUtility::cshItem('xMOD_csh_corebe', 'new_pages');
|
||||
$cshButton = $buttonBar->makeHelpButton()->setModuleName('xMOD_csh_corebe')->setFieldName('new_pages');
|
||||
$buttonBar->addButton($cshButton);
|
||||
}
|
||||
// Back
|
||||
if ($this->returnUrl) {
|
||||
$returnButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->returnUrl)
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.goBack'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-view-go-back', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($returnButton, ButtonBar::BUTTON_POSITION_LEFT, 10);
|
||||
}
|
||||
|
||||
if (is_array($this->pageinfo) && $this->pageinfo['uid']) {
|
||||
// View
|
||||
$pagesTSconfig = BackendUtility::getPagesTSconfig($this->pageinfo['uid']);
|
||||
if (isset($pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'])) {
|
||||
$excludeDokTypes = GeneralUtility::intExplode(
|
||||
',',
|
||||
$pagesTSconfig['TCEMAIN.']['preview.']['disableButtonForDokType'],
|
||||
true
|
||||
);
|
||||
} else {
|
||||
// exclude sysfolders and recycler by default
|
||||
$excludeDokTypes = [
|
||||
PageRepository::DOKTYPE_RECYCLER,
|
||||
PageRepository::DOKTYPE_SYSFOLDER,
|
||||
PageRepository::DOKTYPE_SPACER
|
||||
];
|
||||
}
|
||||
if (!in_array((int)$this->pageinfo['doktype'], $excludeDokTypes, true)) {
|
||||
$viewButton = $buttonBar->makeLinkButton()
|
||||
->setHref('#')
|
||||
->setOnClick(BackendUtility::viewOnClick(
|
||||
$this->pageinfo['uid'],
|
||||
'',
|
||||
BackendUtility::BEgetRootLine($this->pageinfo['uid'])
|
||||
))
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon(
|
||||
'actions-view-page',
|
||||
Icon::SIZE_SMALL
|
||||
));
|
||||
$buttonBar->addButton($viewButton, ButtonBar::BUTTON_POSITION_LEFT, 30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the position map for pages wizard
|
||||
*
|
||||
* @return ResponseInterface|null
|
||||
*/
|
||||
protected function renderPositionTree(): ?ResponseInterface
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
$numberOfPages = $queryBuilder
|
||||
->count('*')
|
||||
->from('pages')
|
||||
->execute()
|
||||
->fetchColumn(0);
|
||||
|
||||
if ($numberOfPages > 0) {
|
||||
$this->code .= '<h3>' . htmlspecialchars($this->getLanguageService()->getLL('selectPosition')) . ':</h3>';
|
||||
/** @var \TYPO3\CMS\Backend\Tree\View\PagePositionMap $positionMap */
|
||||
$positionMap = GeneralUtility::makeInstance(PagePositionMap::class, NewRecordPageTreeView::class);
|
||||
$this->code .= $positionMap->positionTree(
|
||||
$this->id,
|
||||
$this->pageinfo,
|
||||
$this->perms_clause,
|
||||
$this->returnUrl
|
||||
);
|
||||
} else {
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
// No pages yet, no need to prompt for position, redirect to page creation.
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
'pages' => [
|
||||
0 => 'new'
|
||||
]
|
||||
],
|
||||
'returnNewPageId' => 1,
|
||||
'returnUrl' => (string)$uriBuilder->buildUriFromRoute('db_new', ['id' => $this->id, 'pagesOnly' => '1'])
|
||||
];
|
||||
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
@ob_end_clean();
|
||||
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render controls for creating a regular new element (pages or records)
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function renderNewRecordControls(ServerRequestInterface $request): void
|
||||
{
|
||||
$lang = $this->getLanguageService();
|
||||
// Initialize array for accumulating table rows:
|
||||
$this->tRows = [];
|
||||
// Get TSconfig for current page
|
||||
$pageTS = BackendUtility::getPagesTSconfig($this->id);
|
||||
// Finish initializing new pages options with TSconfig
|
||||
// Each new page option may be hidden by TSconfig
|
||||
// Enabled option for the position of a new page
|
||||
$this->newPagesSelectPosition = !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageSelectPosition']);
|
||||
// Pseudo-boolean (0/1) for backward compatibility
|
||||
$displayNewPagesIntoLink = $this->newPagesInto && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageInside']);
|
||||
$displayNewPagesAfterLink = $this->newPagesAfter && !empty($pageTS['mod.']['wizards.']['newRecord.']['pages.']['show.']['pageAfter']);
|
||||
// Slight spacer from header:
|
||||
$this->code .= '';
|
||||
// New Page
|
||||
$table = 'pages';
|
||||
$v = $GLOBALS['TCA'][$table];
|
||||
$pageIcon = $this->moduleTemplate->getIconFactory()->getIconForRecord(
|
||||
$table,
|
||||
[],
|
||||
Icon::SIZE_SMALL
|
||||
)->render();
|
||||
$newPageIcon = $this->moduleTemplate->getIconFactory()->getIcon('actions-page-new', Icon::SIZE_SMALL)->render();
|
||||
$rowContent = '';
|
||||
// New pages INSIDE this pages
|
||||
$newPageLinks = [];
|
||||
if ($displayNewPagesIntoLink
|
||||
&& $this->isTableAllowedOnPage('pages', $this->pageinfo)
|
||||
&& $this->getBackendUserAuthentication()->check('tables_modify', 'pages')
|
||||
&& $this->getBackendUserAuthentication()->workspaceCreateNewRecord(($this->pageinfo['_ORIG_uid'] ?: $this->id), 'pages')
|
||||
) {
|
||||
// Create link to new page inside:
|
||||
$recordIcon = $this->moduleTemplate->getIconFactory()->getIconForRecord($table, [], Icon::SIZE_SMALL)->render();
|
||||
$newPageLinks[] = $this->renderLink(
|
||||
$recordIcon . htmlspecialchars($lang->sL($v['ctrl']['title'])) . ' (' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.inside')) . ')',
|
||||
$table,
|
||||
$this->id
|
||||
);
|
||||
}
|
||||
// New pages AFTER this pages
|
||||
if ($displayNewPagesAfterLink
|
||||
&& $this->isTableAllowedOnPage('pages', $this->pidInfo)
|
||||
&& $this->getBackendUserAuthentication()->check('tables_modify', 'pages')
|
||||
&& $this->getBackendUserAuthentication()->workspaceCreateNewRecord($this->pidInfo['uid'], 'pages')
|
||||
) {
|
||||
$newPageLinks[] = $this->renderLink(
|
||||
$pageIcon . htmlspecialchars($lang->sL($v['ctrl']['title'])) . ' (' . htmlspecialchars($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:db_new.php.after')) . ')',
|
||||
'pages',
|
||||
-$this->id
|
||||
);
|
||||
}
|
||||
// New pages at selection position
|
||||
if ($this->newPagesSelectPosition && $this->isRecordCreationAllowedForTable('pages')) {
|
||||
// Link to page-wizard:
|
||||
$newPageLinks[] = '<a href="' . htmlspecialchars(GeneralUtility::linkThisScript(['pagesOnly' => 1])) . '">' . $pageIcon . htmlspecialchars($lang->getLL('pageSelectPosition')) . '</a>';
|
||||
}
|
||||
// Assemble all new page links
|
||||
$numPageLinks = count($newPageLinks);
|
||||
for ($i = 0; $i < $numPageLinks; $i++) {
|
||||
$rowContent .= '<li>' . $newPageLinks[$i] . '</li>';
|
||||
}
|
||||
if ($this->isRecordCreationAllowedForTable('pages')) {
|
||||
$rowContent = '<ul class="list-tree"><li>' . $newPageIcon . '<strong>' .
|
||||
$lang->getLL('createNewPage') . '</strong><ul>' . $rowContent . '</ul></li>';
|
||||
} else {
|
||||
$rowContent = '<ul class="list-tree"><li><ul>' . $rowContent . '</li></ul>';
|
||||
}
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
// Compile table row
|
||||
$startRows = [$rowContent];
|
||||
$iconFile = [];
|
||||
// New tables (but not pages) INSIDE this pages
|
||||
$isAdmin = $this->getBackendUserAuthentication()->isAdmin();
|
||||
$newContentIcon = $this->moduleTemplate->getIconFactory()->getIcon('actions-document-new', Icon::SIZE_SMALL)->render();
|
||||
if ($this->newContentInto) {
|
||||
if (is_array($GLOBALS['TCA'])) {
|
||||
$groupName = '';
|
||||
foreach ($GLOBALS['TCA'] as $table => $v) {
|
||||
$rootLevelConfiguration = isset($v['ctrl']['rootLevel']) ? (int)$v['ctrl']['rootLevel'] : 0;
|
||||
if ($table !== 'pages'
|
||||
&& $this->isRecordCreationAllowedForTable($table)
|
||||
&& $this->isTableAllowedOnPage($table, $this->pageinfo)
|
||||
&& $this->getBackendUserAuthentication()->check('tables_modify', $table)
|
||||
&& ($rootLevelConfiguration === -1 || ($this->id xor $rootLevelConfiguration))
|
||||
&& $this->getBackendUserAuthentication()->workspaceCreateNewRecord(($this->pageinfo['_ORIG_uid'] ? $this->pageinfo['_ORIG_uid'] : $this->id), $table)
|
||||
) {
|
||||
$newRecordIcon = $this->moduleTemplate->getIconFactory()->getIconForRecord($table, [], Icon::SIZE_SMALL)->render();
|
||||
$rowContent = '';
|
||||
$thisTitle = '';
|
||||
// Create new link for record:
|
||||
$newLink = $this->renderLink(
|
||||
$newRecordIcon . htmlspecialchars($lang->sL($v['ctrl']['title'])),
|
||||
$table,
|
||||
$this->id
|
||||
);
|
||||
// If the table is 'tt_content', add link to wizard
|
||||
if ($table === 'tt_content') {
|
||||
$groupName = $lang->getLL('createNewContent');
|
||||
$rowContent = $newContentIcon
|
||||
. '<strong>' . $lang->getLL('createNewContent') . '</strong>'
|
||||
. '<ul>';
|
||||
// If mod.newContentElementWizard.override is set, use that extension's wizard instead:
|
||||
$moduleName = BackendUtility::getPagesTSconfig($this->id)['mod.']['newContentElementWizard.']['override']
|
||||
?? 'new_content_element_wizard';
|
||||
/** @var \TYPO3\CMS\Core\Http\NormalizedParams */
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$url = (string)$uriBuilder->buildUriFromRoute($moduleName, ['id' => $this->id, 'returnUrl' => $normalizedParams->getRequestUri()]);
|
||||
$rowContent .= '<li>' . $newLink . ' ' . BackendUtility::wrapInHelp($table, '') . '</li>'
|
||||
. '<li>'
|
||||
. '<a href="' . htmlspecialchars($url) . '" data-title="' . htmlspecialchars($this->getLanguageService()->getLL('newContentElement')) . '" class="t3js-toggle-new-content-element-wizard disabled">'
|
||||
. $newContentIcon . htmlspecialchars($lang->getLL('clickForWizard'))
|
||||
. '</a>'
|
||||
. '</li>'
|
||||
. '</ul>';
|
||||
} else {
|
||||
// Get the title
|
||||
if ($v['ctrl']['readOnly'] || $v['ctrl']['hideTable'] || $v['ctrl']['is_static']) {
|
||||
continue;
|
||||
}
|
||||
if ($v['ctrl']['adminOnly'] && !$isAdmin) {
|
||||
continue;
|
||||
}
|
||||
$nameParts = explode('_', $table);
|
||||
$thisTitle = '';
|
||||
$_EXTKEY = '';
|
||||
if ($nameParts[0] === 'tx' || $nameParts[0] === 'tt') {
|
||||
// Try to extract extension name
|
||||
$title = (string)($v['ctrl']['title'] ?? '');
|
||||
if (strpos($title, 'LLL:EXT:') === 0) {
|
||||
$_EXTKEY = substr($title, 8);
|
||||
$_EXTKEY = substr($_EXTKEY, 0, strpos($_EXTKEY, '/'));
|
||||
if ($_EXTKEY !== '') {
|
||||
// First try to get localisation of extension title
|
||||
$temp = explode(':', substr($title, 9 + strlen($_EXTKEY)));
|
||||
$langFile = $temp[0];
|
||||
$thisTitle = $lang->sL('LLL:EXT:' . $_EXTKEY . '/' . $langFile . ':extension.title');
|
||||
// If no localisation available, read title from ext_emconf.php
|
||||
$extPath = ExtensionManagementUtility::extPath($_EXTKEY);
|
||||
$extEmConfFile = $extPath . 'ext_emconf.php';
|
||||
if (!$thisTitle && is_file($extEmConfFile)) {
|
||||
$EM_CONF = [];
|
||||
include $extEmConfFile;
|
||||
$thisTitle = $EM_CONF[$_EXTKEY]['title'];
|
||||
}
|
||||
$iconFile[$_EXTKEY] = '<img src="' . PathUtility::getAbsoluteWebPath(ExtensionManagementUtility::getExtensionIcon($extPath, true)) . '" ' . 'width="16" height="16" ' . 'alt="' . $thisTitle . '" />';
|
||||
}
|
||||
}
|
||||
if (empty($thisTitle)) {
|
||||
$_EXTKEY = $nameParts[1];
|
||||
$thisTitle = $nameParts[1];
|
||||
$iconFile[$_EXTKEY] = '';
|
||||
}
|
||||
} else {
|
||||
$_EXTKEY = 'system';
|
||||
$thisTitle = $lang->getLL('system_records');
|
||||
$iconFile['system'] = $this->moduleTemplate->getIconFactory()->getIcon('apps-pagetree-root', Icon::SIZE_SMALL)->render();
|
||||
}
|
||||
|
||||
if ($groupName === '' || $groupName !== $_EXTKEY) {
|
||||
$groupName = empty($v['ctrl']['groupName']) ? $_EXTKEY : $v['ctrl']['groupName'];
|
||||
}
|
||||
$rowContent .= $newLink;
|
||||
}
|
||||
// Compile table row:
|
||||
if ($table === 'tt_content') {
|
||||
$startRows[] = '<li>' . $rowContent . '</li>';
|
||||
} else {
|
||||
$this->tRows[$groupName]['title'] = $thisTitle;
|
||||
$this->tRows[$groupName]['html'][] = $rowContent;
|
||||
$this->tRows[$groupName]['table'][] = $table;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// User sort
|
||||
if (isset($pageTS['mod.']['wizards.']['newRecord.']['order'])) {
|
||||
$this->newRecordSortList = GeneralUtility::trimExplode(',', $pageTS['mod.']['wizards.']['newRecord.']['order'], true);
|
||||
}
|
||||
uksort($this->tRows, [$this, 'sortTableRows']);
|
||||
// Compile table row:
|
||||
$finalRows = [];
|
||||
$finalRows[] = implode('', $startRows);
|
||||
foreach ($this->tRows as $key => $value) {
|
||||
$row = '<li>' . $iconFile[$key] . ' <strong>' . $value['title'] . '</strong><ul>';
|
||||
foreach ($value['html'] as $recordKey => $record) {
|
||||
$row .= '<li>' . $record . ' ' . BackendUtility::wrapInHelp($value['table'][$recordKey], '') . '</li>';
|
||||
}
|
||||
$row .= '</ul></li>';
|
||||
$finalRows[] = $row;
|
||||
}
|
||||
|
||||
$finalRows[] = '</ul>';
|
||||
// Make table:
|
||||
$this->code .= implode('', $finalRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* User array sort function used by renderNewRecordControls
|
||||
*
|
||||
* @param string $a First array element for compare
|
||||
* @param string $b First array element for compare
|
||||
* @return int -1 for lower, 0 for equal, 1 for greater
|
||||
*/
|
||||
protected function sortTableRows(string $a, string $b): int
|
||||
{
|
||||
if (!empty($this->newRecordSortList)) {
|
||||
if (in_array($a, $this->newRecordSortList) && in_array($b, $this->newRecordSortList)) {
|
||||
// Both are in the list, return relative to position in array
|
||||
$sub = array_search($a, $this->newRecordSortList) - array_search($b, $this->newRecordSortList);
|
||||
$ret = ($sub < 0 ? -1 : $sub == 0) ? 0 : 1;
|
||||
} elseif (in_array($a, $this->newRecordSortList)) {
|
||||
// First element is in array, put to top
|
||||
$ret = -1;
|
||||
} elseif (in_array($b, $this->newRecordSortList)) {
|
||||
// Second element is in array, put first to bottom
|
||||
$ret = 1;
|
||||
} else {
|
||||
// No element is in array, return alphabetic order
|
||||
$ret = strnatcasecmp($this->tRows[$a]['title'], $this->tRows[$b]['title']);
|
||||
}
|
||||
return $ret;
|
||||
}
|
||||
// Return alphabetic order
|
||||
return strnatcasecmp($this->tRows[$a]['title'], $this->tRows[$b]['title']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the string $code to a create-new form for a record in $table created on page $pid
|
||||
*
|
||||
* @param string $linkText Link text
|
||||
* @param string $table Table name (in which to create new record)
|
||||
* @param int $pid PID value for the "&edit['.$table.']['.$pid.']=new" command (positive/negative)
|
||||
* @param bool $addContentTable If $addContentTable is set, then a new tt_content record is created together with pages
|
||||
* @return string The link.
|
||||
*/
|
||||
protected function renderLink(string $linkText, string $table, int $pid, bool $addContentTable = false): string
|
||||
{
|
||||
$urlParameters = [
|
||||
'edit' => [
|
||||
$table => [
|
||||
$pid => 'new'
|
||||
]
|
||||
],
|
||||
'returnUrl' => $this->returnUrl
|
||||
];
|
||||
|
||||
if ($table === 'pages' && $addContentTable) {
|
||||
$urlParameters['tt_content']['prev'] = 'new';
|
||||
$urlParameters['returnNewPageId'] = 1;
|
||||
}
|
||||
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
|
||||
return '<a href="' . htmlspecialchars($url) . '">' . $linkText . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if the tablename $checkTable is allowed to be created on the page with record $pid_row
|
||||
*
|
||||
* @param string $table Table name to check
|
||||
* @param array $page Potential parent page
|
||||
* @return bool Returns TRUE if the tablename $table is allowed to be created on the $page
|
||||
*/
|
||||
protected function isTableAllowedOnPage(string $table, array $page): bool
|
||||
{
|
||||
if (empty($page)) {
|
||||
return $this->getBackendUserAuthentication()->isAdmin();
|
||||
}
|
||||
// be_users and be_groups may not be created anywhere but in the root.
|
||||
if ($table === 'be_users' || $table === 'be_groups') {
|
||||
return false;
|
||||
}
|
||||
// Checking doktype:
|
||||
$doktype = (int)$page['doktype'];
|
||||
if (!($allowedTableList = $GLOBALS['PAGES_TYPES'][$doktype]['allowedTables'])) {
|
||||
$allowedTableList = $GLOBALS['PAGES_TYPES']['default']['allowedTables'];
|
||||
}
|
||||
// If all tables or the table is listed as an allowed type, return TRUE
|
||||
if (strstr($allowedTableList, '*') || GeneralUtility::inList($allowedTableList, $table)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the record link should be shown for a table
|
||||
*
|
||||
* Returns TRUE if:
|
||||
* - $allowedNewTables and $deniedNewTables are empty
|
||||
* - the table is not found in $deniedNewTables and $allowedNewTables is not set or the $table tablename is found in
|
||||
* $allowedNewTables
|
||||
*
|
||||
* If $table tablename is found in $allowedNewTables and $deniedNewTables,
|
||||
* $deniedNewTables has priority over $allowedNewTables.
|
||||
*
|
||||
* @param string $table Table name to test if in allowedTables
|
||||
* @param array $allowedNewTables Array of new tables that are allowed.
|
||||
* @param array $deniedNewTables Array of new tables that are not allowed.
|
||||
* @return bool Returns TRUE if a link for creating new records should be displayed for $table
|
||||
*/
|
||||
protected function isRecordCreationAllowedForTable(string $table, array $allowedNewTables = [], array $deniedNewTables = []): bool
|
||||
{
|
||||
if (!$this->getBackendUserAuthentication()->check('tables_modify', $table)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$allowedNewTables = $allowedNewTables ?: $this->allowedNewTables;
|
||||
$deniedNewTables = $deniedNewTables ?: $this->deniedNewTables;
|
||||
// No deny/allow tables are set:
|
||||
if (empty($allowedNewTables) && empty($deniedNewTables)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !in_array($table, $deniedNewTables) && (empty($allowedNewTables) || in_array($table, $allowedNewTables));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sys_language records are present
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkIfLanguagesExist(): bool
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_language');
|
||||
$queryBuilder->getRestrictions()->removeAll();
|
||||
|
||||
$count = $queryBuilder
|
||||
->count('uid')
|
||||
->from('sys_language')
|
||||
->execute()
|
||||
->fetchColumn(0);
|
||||
return (bool)$count;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Resource\File;
|
||||
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Class OnlineMediaController handles uploading online media
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class OnlineMediaController
|
||||
{
|
||||
/**
|
||||
* AJAX endpoint for storing the URL as a sys_file record
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function createAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$url = $request->getParsedBody()['url'];
|
||||
$targetFolderIdentifier = $request->getParsedBody()['targetFolder'];
|
||||
$allowedExtensions = GeneralUtility::trimExplode(',', $request->getParsedBody()['allowed'] ?: '');
|
||||
|
||||
if (!empty($url)) {
|
||||
$data = [];
|
||||
$file = $this->addMediaFromUrl($url, $targetFolderIdentifier, $allowedExtensions);
|
||||
if ($file !== null) {
|
||||
$data['file'] = $file->getUid();
|
||||
} else {
|
||||
$data['error'] = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.invalid_url');
|
||||
}
|
||||
return new JsonResponse($data);
|
||||
}
|
||||
return new JsonResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process add media request, and redirects to the previous page
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$files = $request->getParsedBody()['data'];
|
||||
$redirect = $request->getParsedBody()['redirect'];
|
||||
$newMedia = [];
|
||||
if (isset($files['newMedia'])) {
|
||||
$newMedia = (array)$files['newMedia'];
|
||||
}
|
||||
|
||||
foreach ($newMedia as $media) {
|
||||
if (!empty($media['url']) && !empty($media['target'])) {
|
||||
$allowed = !empty($media['allowed']) ? GeneralUtility::trimExplode(',', $media['allowed']) : [];
|
||||
$file = $this->addMediaFromUrl($media['url'], $media['target'], $allowed);
|
||||
if ($file !== null) {
|
||||
$flashMessage = GeneralUtility::makeInstance(
|
||||
FlashMessage::class,
|
||||
$file->getName(),
|
||||
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.added'),
|
||||
FlashMessage::OK,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
$flashMessage = GeneralUtility::makeInstance(
|
||||
FlashMessage::class,
|
||||
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.invalid_url'),
|
||||
$this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.error.new_media.failed'),
|
||||
FlashMessage::ERROR,
|
||||
true
|
||||
);
|
||||
}
|
||||
$this->addFlashMessage($flashMessage);
|
||||
if (empty($redirect) && $media['redirect']) {
|
||||
$redirect = $media['redirect'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$redirect = GeneralUtility::sanitizeLocalUrl($redirect);
|
||||
if ($redirect) {
|
||||
return new RedirectResponse($redirect, 303);
|
||||
}
|
||||
|
||||
throw new \RuntimeException('No redirect after uploading a media found, probably a mis-use of the template not sending the proper Return URL.', 1511945040);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $targetFolderIdentifier
|
||||
* @param string[] $allowedExtensions
|
||||
* @return File|null
|
||||
*/
|
||||
protected function addMediaFromUrl($url, $targetFolderIdentifier, array $allowedExtensions = [])
|
||||
{
|
||||
$targetFolder = null;
|
||||
if ($targetFolderIdentifier) {
|
||||
try {
|
||||
$targetFolder = ResourceFactory::getInstance()->getFolderObjectFromCombinedIdentifier($targetFolderIdentifier);
|
||||
} catch (\Exception $e) {
|
||||
$targetFolder = null;
|
||||
}
|
||||
}
|
||||
if ($targetFolder === null) {
|
||||
$targetFolder = $this->getBackendUser()->getDefaultUploadFolder();
|
||||
}
|
||||
return OnlineMediaHelperRegistry::getInstance()->transformUrlToFile($url, $targetFolder, $allowedExtensions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add flash message to message queue
|
||||
*
|
||||
* @param FlashMessage $flashMessage
|
||||
*/
|
||||
protected function addFlashMessage(FlashMessage $flashMessage)
|
||||
{
|
||||
/** @var FlashMessageService $flashMessageService */
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
|
||||
/** @var \TYPO3\CMS\Core\Messaging\FlashMessageQueue $defaultFlashMessageQueue */
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$defaultFlashMessageQueue->enqueue($flashMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Backend\Controller\Page;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
|
||||
use TYPO3\CMS\Backend\Domain\Repository\Localization\LocalizationRepository;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Backend\View\BackendLayoutView;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\Response;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Versioning\VersionState;
|
||||
|
||||
/**
|
||||
* LocalizationController handles the AJAX requests for record localization
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class LocalizationController
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
const ACTION_COPY = 'copyFromLanguage';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
const ACTION_LOCALIZE = 'localize';
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* @var LocalizationRepository
|
||||
*/
|
||||
protected $localizationRepository;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$this->localizationRepository = GeneralUtility::makeInstance(LocalizationRepository::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get used languages in a page
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getUsedLanguagesInPage(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['pageId'], $params['languageId'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$pageId = (int)$params['pageId'];
|
||||
$languageId = (int)$params['languageId'];
|
||||
|
||||
/** @var TranslationConfigurationProvider $translationProvider */
|
||||
$translationProvider = GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
|
||||
$systemLanguages = $translationProvider->getSystemLanguages($pageId);
|
||||
|
||||
$availableLanguages = [];
|
||||
|
||||
// First check whether column has localized records
|
||||
$elementsInColumnCount = $this->localizationRepository->getLocalizedRecordCount($pageId, $languageId);
|
||||
|
||||
if ($elementsInColumnCount === 0) {
|
||||
$fetchedAvailableLanguages = $this->localizationRepository->fetchAvailableLanguages($pageId, $languageId);
|
||||
foreach ($fetchedAvailableLanguages as $language) {
|
||||
if (isset($systemLanguages[$language['sys_language_uid']])) {
|
||||
$availableLanguages[] = $systemLanguages[$language['sys_language_uid']];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$result = $this->localizationRepository->fetchOriginLanguage($pageId, $languageId);
|
||||
$availableLanguages[] = $systemLanguages[$result['sys_language_uid']];
|
||||
}
|
||||
|
||||
// Pre-render all flag icons
|
||||
foreach ($availableLanguages as &$language) {
|
||||
if ($language['flagIcon'] === 'empty-empty') {
|
||||
$language['flagIcon'] = '';
|
||||
} else {
|
||||
$language['flagIcon'] = $this->iconFactory->getIcon($language['flagIcon'], Icon::SIZE_SMALL)->render();
|
||||
}
|
||||
}
|
||||
|
||||
return (new JsonResponse())->setPayload($availableLanguages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a prepared summary of records being translated
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function getRecordLocalizeSummary(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['pageId'], $params['destLanguageId'], $params['languageId'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
$pageId = (int)$params['pageId'];
|
||||
$destLanguageId = (int)$params['destLanguageId'];
|
||||
$languageId = (int)$params['languageId'];
|
||||
|
||||
$records = [];
|
||||
$result = $this->localizationRepository->getRecordsToCopyDatabaseResult(
|
||||
$pageId,
|
||||
$destLanguageId,
|
||||
$languageId,
|
||||
'*'
|
||||
);
|
||||
|
||||
while ($row = $result->fetch()) {
|
||||
BackendUtility::workspaceOL('tt_content', $row, -99, true);
|
||||
if (!$row || VersionState::cast($row['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
|
||||
continue;
|
||||
}
|
||||
$colPos = $row['colPos'];
|
||||
if (!isset($records[$colPos])) {
|
||||
$records[$colPos] = [];
|
||||
}
|
||||
$records[$colPos][] = [
|
||||
'icon' => $this->iconFactory->getIconForRecord('tt_content', $row, Icon::SIZE_SMALL)->render(),
|
||||
'title' => $row[$GLOBALS['TCA']['tt_content']['ctrl']['label']],
|
||||
'uid' => $row['uid']
|
||||
];
|
||||
}
|
||||
|
||||
return (new JsonResponse())->setPayload([
|
||||
'records' => $records,
|
||||
'columns' => $this->getPageColumns($pageId),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function localizeRecords(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$params = $request->getQueryParams();
|
||||
if (!isset($params['pageId'], $params['srcLanguageId'], $params['destLanguageId'], $params['action'], $params['uidList'])) {
|
||||
return new JsonResponse(null, 400);
|
||||
}
|
||||
|
||||
if ($params['action'] !== static::ACTION_COPY && $params['action'] !== static::ACTION_LOCALIZE) {
|
||||
$response = new Response('php://temp', 400, ['Content-Type' => 'application/json; charset=utf-8']);
|
||||
$response->getBody()->write('Invalid action "' . $params['action'] . '" called.');
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Filter transmitted but invalid uids
|
||||
$params['uidList'] = $this->filterInvalidUids(
|
||||
(int)$params['pageId'],
|
||||
(int)$params['destLanguageId'],
|
||||
(int)$params['srcLanguageId'],
|
||||
$params['uidList']
|
||||
);
|
||||
|
||||
$this->process($params);
|
||||
|
||||
return (new JsonResponse())->setPayload([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all possible UIDs of a page, colPos and language that might be processed and removes invalid UIDs that might
|
||||
* be smuggled in.
|
||||
*
|
||||
* @param int $pageId
|
||||
* @param int $destLanguageId
|
||||
* @param int $srcLanguageId
|
||||
* @param array $transmittedUidList
|
||||
* @return array
|
||||
*/
|
||||
protected function filterInvalidUids(
|
||||
int $pageId,
|
||||
int $destLanguageId,
|
||||
int $srcLanguageId,
|
||||
array $transmittedUidList
|
||||
): array {
|
||||
// Get all valid uids that can be processed
|
||||
$validUidList = $this->localizationRepository->getRecordsToCopyDatabaseResult(
|
||||
$pageId,
|
||||
$destLanguageId,
|
||||
$srcLanguageId,
|
||||
'uid'
|
||||
);
|
||||
|
||||
return array_intersect(array_unique($transmittedUidList), array_column($validUidList->fetchAll(), 'uid'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the localization actions
|
||||
*
|
||||
* @param array $params
|
||||
*/
|
||||
protected function process($params): void
|
||||
{
|
||||
$destLanguageId = (int)$params['destLanguageId'];
|
||||
|
||||
// Build command map
|
||||
$cmd = [
|
||||
'tt_content' => []
|
||||
];
|
||||
|
||||
if (isset($params['uidList']) && is_array($params['uidList'])) {
|
||||
foreach ($params['uidList'] as $currentUid) {
|
||||
if ($params['action'] === static::ACTION_LOCALIZE) {
|
||||
$cmd['tt_content'][$currentUid] = [
|
||||
'localize' => $destLanguageId
|
||||
];
|
||||
} else {
|
||||
$cmd['tt_content'][$currentUid] = [
|
||||
'copyToLanguage' => $destLanguageId,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([], $cmd);
|
||||
$dataHandler->process_cmdmap();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $pageId
|
||||
* @return array
|
||||
*/
|
||||
protected function getPageColumns(int $pageId): array
|
||||
{
|
||||
$columns = [];
|
||||
$backendLayoutView = GeneralUtility::makeInstance(BackendLayoutView::class);
|
||||
$backendLayouts = $backendLayoutView->getSelectedBackendLayout($pageId);
|
||||
|
||||
foreach ($backendLayouts['__items'] as $backendLayout) {
|
||||
$columns[(int)$backendLayout[1]] = $backendLayout[0];
|
||||
}
|
||||
|
||||
return [
|
||||
'columns' => $columns,
|
||||
'columnList' => $backendLayouts['__colPosList'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Page;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* "Create multiple pages" controller
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class NewMultiplePagesController
|
||||
{
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor Method
|
||||
*
|
||||
* @var ModuleTemplate $moduleTemplate
|
||||
*/
|
||||
public function __construct(ModuleTemplate $moduleTemplate = null)
|
||||
{
|
||||
$this->moduleTemplate = $moduleTemplate ?? GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function Handling input variables and rendering main view
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface Response
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$pageUid = (int)$request->getQueryParams()['id'];
|
||||
|
||||
// Show only if there is a valid page and if this page may be viewed by the user
|
||||
$pageRecord = BackendUtility::readPageAccess($pageUid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (!is_array($pageRecord)) {
|
||||
// User has no permission on parent page, should not happen, just render an empty page
|
||||
$this->moduleTemplate->setContent('');
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
// Doc header handling
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageRecord);
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('pages_new')
|
||||
->setFieldName('pages_new');
|
||||
$viewButton = $buttonBar->makeLinkButton()
|
||||
->setOnClick(BackendUtility::viewOnClick($pageUid, '', BackendUtility::BEgetRootLine($pageUid)))
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
|
||||
->setIcon($iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
|
||||
->setHref('#');
|
||||
$shortcutButton = $buttonBar->makeShortcutButton()
|
||||
->setModuleName('pages_new')
|
||||
->setGetVariables(['id']);
|
||||
$buttonBar->addButton($cshButton)->addButton($viewButton)->addButton($shortcutButton);
|
||||
|
||||
// Main view setup
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/Page/NewPages.html'
|
||||
));
|
||||
|
||||
$calculatedPermissions = $backendUser->calcPerms($pageRecord);
|
||||
$canCreateNew = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_NEW;
|
||||
|
||||
$view->assign('canCreateNew', $canCreateNew);
|
||||
$view->assign('maxTitleLength', $backendUser->uc['titleLen'] ?? 20);
|
||||
$view->assign('pageUid', $pageUid);
|
||||
|
||||
if ($canCreateNew) {
|
||||
$newPagesData = (array)$request->getParsedBody()['pages'];
|
||||
if (!empty($newPagesData)) {
|
||||
$hasNewPagesData = true;
|
||||
$afterExisting = isset($request->getParsedBody()['createInListEnd']);
|
||||
$hidePages = isset($request->getParsedBody()['hidePages']);
|
||||
$hidePagesInMenu = isset($request->getParsedBody()['hidePagesInMenus']);
|
||||
$pagesCreated = $this->createPages($newPagesData, $pageUid, $afterExisting, $hidePages, $hidePagesInMenu);
|
||||
$view->assign('pagesCreated', $pagesCreated);
|
||||
$subPages = $this->getSubPagesOfPage($pageUid);
|
||||
$visiblePages = [];
|
||||
foreach ($subPages as $page) {
|
||||
$calculatedPermissions = $backendUser->calcPerms($page);
|
||||
if ($backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_SHOW) {
|
||||
$visiblePages[] = $page;
|
||||
}
|
||||
}
|
||||
$view->assign('visiblePages', $visiblePages);
|
||||
} else {
|
||||
$hasNewPagesData = false;
|
||||
$view->assign('pageTypes', $this->getTypeSelectData($pageUid));
|
||||
}
|
||||
$view->assign('hasNewPagesData', $hasNewPagesData);
|
||||
}
|
||||
|
||||
$this->moduleTemplate->setContent($view->render());
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist new pages in DB
|
||||
*
|
||||
* @param array $newPagesData Data array with title and page type
|
||||
* @param int $pageUid Uid of page new pages should be added in
|
||||
* @param bool $afterExisting True if new pages should be created after existing pages
|
||||
* @param bool $hidePages True if new pages should be set to hidden
|
||||
* @param bool $hidePagesInMenu True if new pages should be set to hidden in menu
|
||||
* @return bool TRUE if at least on pages has been added
|
||||
*/
|
||||
protected function createPages(array $newPagesData, int $pageUid, bool $afterExisting, bool $hidePages, bool $hidePagesInMenu): bool
|
||||
{
|
||||
$pagesCreated = false;
|
||||
|
||||
// Set first pid to "-1 * uid of last existing sub page" if pages should be created at end
|
||||
$firstPid = $pageUid;
|
||||
if ($afterExisting) {
|
||||
$subPages = $this->getSubPagesOfPage($pageUid);
|
||||
$lastPage = end($subPages);
|
||||
if (isset($lastPage['uid']) && MathUtility::canBeInterpretedAsInteger($lastPage['uid'])) {
|
||||
$firstPid = -(int)$lastPage['uid'];
|
||||
}
|
||||
}
|
||||
|
||||
$commandArray = [];
|
||||
$firstRecord = true;
|
||||
$previousIdentifier = '';
|
||||
foreach ($newPagesData as $identifier => $data) {
|
||||
if (!trim($data['title'])) {
|
||||
continue;
|
||||
}
|
||||
$commandArray['pages'][$identifier]['hidden'] = (int)$hidePages;
|
||||
$commandArray['pages'][$identifier]['nav_hide'] = (int)$hidePagesInMenu;
|
||||
$commandArray['pages'][$identifier]['title'] = $data['title'];
|
||||
$commandArray['pages'][$identifier]['doktype'] = $data['doktype'];
|
||||
if ($firstRecord) {
|
||||
$firstRecord = false;
|
||||
$commandArray['pages'][$identifier]['pid'] = $firstPid;
|
||||
} else {
|
||||
$commandArray['pages'][$identifier]['pid'] = '-' . $previousIdentifier;
|
||||
}
|
||||
$previousIdentifier = $identifier;
|
||||
}
|
||||
|
||||
if (!empty($commandArray)) {
|
||||
$pagesCreated = true;
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start($commandArray, []);
|
||||
$dataHandler->process_datamap();
|
||||
BackendUtility::setUpdateSignal('updatePageTree');
|
||||
}
|
||||
|
||||
return $pagesCreated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page selector type data
|
||||
*
|
||||
* @param int $pageUid Page Uid
|
||||
* @return array
|
||||
*/
|
||||
protected function getTypeSelectData(int $pageUid)
|
||||
{
|
||||
$tsConfig = BackendUtility::getPagesTSconfig($pageUid);
|
||||
$pagesTsConfig = $tsConfig['TCEFORM.']['pages.'] ?? [];
|
||||
|
||||
// Find all available doktypes for the current user
|
||||
$types = $GLOBALS['PAGES_TYPES'];
|
||||
unset($types['default']);
|
||||
$types = array_keys($types);
|
||||
$types[] = 1; // default
|
||||
$types[] = 3; // link
|
||||
$types[] = 4; // shortcut
|
||||
$types[] = 7; // mount point
|
||||
$types[] = 199; // spacer
|
||||
|
||||
if (!$this->getBackendUser()->isAdmin() && isset($this->getBackendUser()->groupData['pagetypes_select'])) {
|
||||
$types = GeneralUtility::trimExplode(',', $this->getBackendUser()->groupData['pagetypes_select'], true);
|
||||
}
|
||||
$removeItems = isset($pagesTsConfig['doktype.']['removeItems']) ? GeneralUtility::trimExplode(',', $pagesTsConfig['doktype.']['removeItems'], true) : [];
|
||||
$allowedDoktypes = array_diff($types, $removeItems);
|
||||
|
||||
// All doktypes in the TCA
|
||||
$availableDoktypes = $GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'];
|
||||
|
||||
// Sort by group and allowedDoktypes
|
||||
$groupedData = [];
|
||||
$groupLabel = '';
|
||||
foreach ($availableDoktypes as $doktypeData) {
|
||||
// If it is a group, save the group label for the children underneath
|
||||
if ($doktypeData[1] === '--div--') {
|
||||
$groupLabel = $doktypeData[0];
|
||||
} else {
|
||||
if (in_array($doktypeData[1], $allowedDoktypes)) {
|
||||
$groupedData[$groupLabel][] = $doktypeData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $groupedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of sub pages with some all fields from given page.
|
||||
* Fetch all data fields for full page icon display
|
||||
*
|
||||
* @param int $pageUid Get sub pages from this pages
|
||||
* @return array
|
||||
*/
|
||||
protected function getSubPagesOfPage(int $pageUid): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
return $queryBuilder->select('*')
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pageUid, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
$GLOBALS['TCA']['pages']['ctrl']['languageField'],
|
||||
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->orderBy('sorting')
|
||||
->execute()
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current BE user
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Page;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* "Sort sub pages" controller - reachable from context menu "more" on page records
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SortSubPagesController
|
||||
{
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor Method
|
||||
*
|
||||
* @var ModuleTemplate $moduleTemplate
|
||||
*/
|
||||
public function __construct(ModuleTemplate $moduleTemplate = null)
|
||||
{
|
||||
$this->moduleTemplate = $moduleTemplate ?? GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function Handling input variables and rendering main view
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface Response
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$parentPageUid = (int)$request->getQueryParams()['id'];
|
||||
|
||||
// Show only if there is a valid page and if this page may be viewed by the user
|
||||
$pageInformation = BackendUtility::readPageAccess($parentPageUid, $backendUser->getPagePermsClause(Permission::PAGE_SHOW));
|
||||
if (!is_array($pageInformation)) {
|
||||
// User has no permission on parent page, should not happen, just render an empty page
|
||||
$this->moduleTemplate->setContent('');
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
// Doc header handling
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$this->moduleTemplate->getDocHeaderComponent()->setMetaInformation($pageInformation);
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('pages_sort')
|
||||
->setFieldName('pages_sort');
|
||||
$viewButton = $buttonBar->makeLinkButton()
|
||||
->setOnClick(BackendUtility::viewOnClick($parentPageUid, '', BackendUtility::BEgetRootLine($parentPageUid)))
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.showPage'))
|
||||
->setIcon($iconFactory->getIcon('actions-view-page', Icon::SIZE_SMALL))
|
||||
->setHref('#');
|
||||
$shortcutButton = $buttonBar->makeShortcutButton()
|
||||
->setModuleName('pages_sort')
|
||||
->setGetVariables(['id']);
|
||||
$buttonBar->addButton($cshButton)->addButton($viewButton)->addButton($shortcutButton);
|
||||
|
||||
// Main view setup
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/Page/SortSubPages.html'
|
||||
));
|
||||
|
||||
$isInWorkspace = $backendUser->workspace !== 0;
|
||||
$view->assign('isInWorkspace', $isInWorkspace);
|
||||
$view->assign('maxTitleLength', $backendUser->uc['titleLen'] ?? 20);
|
||||
$view->assign('parentPageUid', $parentPageUid);
|
||||
$view->assign('dateFormat', $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']);
|
||||
$view->assign('timeFormat', $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']);
|
||||
|
||||
if (!$isInWorkspace) {
|
||||
// Apply new sorting if given
|
||||
$newSortBy = $request->getQueryParams()['newSortBy'] ?? null;
|
||||
if ($newSortBy && in_array($newSortBy, ['title', 'subtitle', 'nav_title', 'crdate', 'tstamp'], true)) {
|
||||
$this->sortSubPagesByField($parentPageUid, (string)$newSortBy);
|
||||
} elseif ($newSortBy && $newSortBy === 'reverseCurrentSorting') {
|
||||
$this->reverseSortingOfPages($parentPageUid);
|
||||
}
|
||||
|
||||
// Get sub pages, loop through them and add page/user specific permission details
|
||||
$pageRecords = $this->getSubPagesOfPage($parentPageUid);
|
||||
$hasInvisiblePage = false;
|
||||
$subPages = [];
|
||||
foreach ($pageRecords as $page) {
|
||||
$pageWithPermissions = [];
|
||||
$pageWithPermissions['record'] = $page;
|
||||
$calculatedPermissions = $backendUser->calcPerms($page);
|
||||
$pageWithPermissions['canEdit'] = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_EDIT;
|
||||
$canSeePage = $backendUser->isAdmin() || $calculatedPermissions & Permission::PAGE_SHOW;
|
||||
if ($canSeePage) {
|
||||
$subPages[] = $pageWithPermissions;
|
||||
} else {
|
||||
$hasInvisiblePage = true;
|
||||
}
|
||||
}
|
||||
$view->assign('subPages', $subPages);
|
||||
$view->assign('hasInvisiblePage', $hasInvisiblePage);
|
||||
}
|
||||
|
||||
$this->moduleTemplate->setContent($view->render());
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort sub pages of given uid by field name alphabetically
|
||||
*
|
||||
* @param int $parentPageUid Parent page uid
|
||||
* @param string $newSortBy Field name to sort by
|
||||
* @throws \RuntimeException If $newSortBy does not validate
|
||||
*/
|
||||
protected function sortSubPagesByField(int $parentPageUid, string $newSortBy)
|
||||
{
|
||||
if (!in_array($newSortBy, ['title', 'subtitle', 'nav_title', 'crdate', 'tstamp'], true)) {
|
||||
throw new \RuntimeException(
|
||||
'New sort by must be one of "title", "subtitle", "nav_title", "crdate" or tstamp',
|
||||
1498924810
|
||||
);
|
||||
}
|
||||
$subPages = $this->getSubPagesOfPage($parentPageUid, $newSortBy);
|
||||
if (!empty($subPages)) {
|
||||
$subPages = array_reverse($subPages);
|
||||
$this->persistNewSubPageOrder($parentPageUid, $subPages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse current sorting of sub pages
|
||||
*
|
||||
* @param int $parentPageUid Parent page uid
|
||||
*/
|
||||
protected function reverseSortingOfPages(int $parentPageUid)
|
||||
{
|
||||
$subPages = $this->getSubPagesOfPage($parentPageUid);
|
||||
if (!empty($subPages)) {
|
||||
$this->persistNewSubPageOrder($parentPageUid, $subPages);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store new sub page order
|
||||
*
|
||||
* @param int $parentPageUid Parent page uid
|
||||
* @param array $subPages List of sub pages in new order
|
||||
*/
|
||||
protected function persistNewSubPageOrder(int $parentPageUid, array $subPages)
|
||||
{
|
||||
$commandArray = [];
|
||||
foreach ($subPages as $subPage) {
|
||||
$commandArray['pages'][$subPage['uid']]['move'] = $parentPageUid;
|
||||
}
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$dataHandler->start([], $commandArray);
|
||||
$dataHandler->process_cmdmap();
|
||||
BackendUtility::setUpdateSignal('updatePageTree');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of sub pages with some all fields from given page.
|
||||
* Fetch all data fields for full page icon display
|
||||
*
|
||||
* @param int $parentPageUid Get sub pages from this pages
|
||||
* @param string $orderBy Order pages by this field
|
||||
* @return array
|
||||
*/
|
||||
protected function getSubPagesOfPage(int $parentPageUid, string $orderBy = 'sorting'): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()->removeAll()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
|
||||
return $queryBuilder->select('*')
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('sys_language_uid', 0),
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($parentPageUid, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->orderBy($orderBy)
|
||||
->execute()
|
||||
->fetchAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current BE user
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,657 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Page;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Configuration\BackendUserConfiguration;
|
||||
use TYPO3\CMS\Backend\Tree\Repository\PageTreeRepository;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DocumentTypeExclusionRestriction;
|
||||
use TYPO3\CMS\Core\Exception\Page\RootLineException;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Site\PseudoSiteFinder;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\RootlineUtility;
|
||||
use TYPO3\CMS\Workspaces\Service\WorkspaceService;
|
||||
|
||||
/**
|
||||
* Controller providing data to the page tree
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class TreeController
|
||||
{
|
||||
/**
|
||||
* Option to use the nav_title field for outputting in the tree items, set via userTS.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $useNavTitle = false;
|
||||
|
||||
/**
|
||||
* Option to prefix the page ID when outputting the tree items, set via userTS.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $addIdAsPrefix = false;
|
||||
|
||||
/**
|
||||
* Option to prefix the domain name of sys_domains when outputting the tree items, set via userTS.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $addDomainName = false;
|
||||
|
||||
/**
|
||||
* Option to add the rootline path above each mount point, set via userTS.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $showMountPathAboveMounts = false;
|
||||
|
||||
/**
|
||||
* An array of background colors for a branch in the tree, set via userTS.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $backgroundColors = [];
|
||||
|
||||
/**
|
||||
* A list of pages not to be shown.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hiddenRecords = [];
|
||||
|
||||
/**
|
||||
* Contains the state of all items that are expanded.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $expandedState = [];
|
||||
|
||||
/**
|
||||
* Instance of the icon factory, to be used for generating the items.
|
||||
*
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* Number of tree levels which should be returned on the first page tree load
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $levelsToFetch = 2;
|
||||
|
||||
/**
|
||||
* When set to true all nodes returend by API will be expanded
|
||||
* @var bool
|
||||
*/
|
||||
protected $expandAllNodes = false;
|
||||
|
||||
/**
|
||||
* Constructor to set up common objects needed in various places.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
}
|
||||
|
||||
protected function initializeConfiguration()
|
||||
{
|
||||
$userTsConfig = $this->getBackendUser()->getTSConfig();
|
||||
$this->hiddenRecords = GeneralUtility::intExplode(
|
||||
',',
|
||||
$userTsConfig['options.']['hideRecords.']['pages'] ?? '',
|
||||
true
|
||||
);
|
||||
$this->backgroundColors = $userTsConfig['options.']['pageTree.']['backgroundColor.'] ?? [];
|
||||
$this->addIdAsPrefix = (bool)($userTsConfig['options.']['pageTree.']['showPageIdWithTitle'] ?? false);
|
||||
$this->addDomainName = (bool)($userTsConfig['options.']['pageTree.']['showDomainNameWithTitle'] ?? false);
|
||||
$this->useNavTitle = (bool)($userTsConfig['options.']['pageTree.']['showNavTitle'] ?? false);
|
||||
$this->showMountPathAboveMounts = (bool)($userTsConfig['options.']['pageTree.']['showPathAboveMounts'] ?? false);
|
||||
$backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class);
|
||||
$this->expandedState = $backendUserConfiguration->get('BackendComponents.States.Pagetree');
|
||||
if (is_object($this->expandedState) && is_object($this->expandedState->stateHash)) {
|
||||
$this->expandedState = (array)$this->expandedState->stateHash;
|
||||
} else {
|
||||
$this->expandedState = $this->expandedState['stateHash'] ?: [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns page tree configuration in JSON
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function fetchConfigurationAction(): ResponseInterface
|
||||
{
|
||||
$configuration = [
|
||||
'allowRecursiveDelete' => !empty($this->getBackendUser()->uc['recursiveDelete']),
|
||||
'allowDragMove' => $this->isDragMoveAllowed(),
|
||||
'doktypes' => $this->getDokTypes(),
|
||||
'displayDeleteConfirmation' => $this->getBackendUser()->jsConfirmation(JsConfirmation::DELETE),
|
||||
'temporaryMountPoint' => $this->getMountPointPath((int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0)),
|
||||
];
|
||||
|
||||
return new JsonResponse($configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of doktypes to display in page tree toolbar drag area
|
||||
*
|
||||
* Note: The list can be filtered by the user TypoScript
|
||||
* option "options.pageTree.doktypesToShowInNewPageDragArea".
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDokTypes(): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$doktypeLabelMap = [];
|
||||
foreach ($GLOBALS['TCA']['pages']['columns']['doktype']['config']['items'] as $doktypeItemConfig) {
|
||||
if ($doktypeItemConfig[1] === '--div--') {
|
||||
continue;
|
||||
}
|
||||
$doktypeLabelMap[$doktypeItemConfig[1]] = $doktypeItemConfig[0];
|
||||
}
|
||||
$doktypes = GeneralUtility::intExplode(',', $backendUser->getTSConfig()['options.']['pageTree.']['doktypesToShowInNewPageDragArea'] ?? '', true);
|
||||
$output = [];
|
||||
$allowedDoktypes = GeneralUtility::intExplode(',', $backendUser->groupData['pagetypes_select'], true);
|
||||
$isAdmin = $backendUser->isAdmin();
|
||||
// Early return if backend user may not create any doktype
|
||||
if (!$isAdmin && empty($allowedDoktypes)) {
|
||||
return $output;
|
||||
}
|
||||
foreach ($doktypes as $doktype) {
|
||||
if (!$isAdmin && !in_array($doktype, $allowedDoktypes, true)) {
|
||||
continue;
|
||||
}
|
||||
$label = htmlspecialchars($this->getLanguageService()->sL($doktypeLabelMap[$doktype]));
|
||||
$output[] = [
|
||||
'nodeType' => $doktype,
|
||||
'icon' => $GLOBALS['TCA']['pages']['ctrl']['typeicon_classes'][$doktype] ?? '',
|
||||
'title' => $label,
|
||||
'tooltip' => $label
|
||||
];
|
||||
}
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns JSON representing page tree
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function fetchDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->initializeConfiguration();
|
||||
|
||||
$items = [];
|
||||
if (!empty($request->getQueryParams()['pid'])) {
|
||||
// Fetching a part of a page tree
|
||||
$entryPoints = $this->getAllEntryPointPageTrees((int)$request->getQueryParams()['pid']);
|
||||
$mountPid = (int)($request->getQueryParams()['mount'] ?? 0);
|
||||
$parentDepth = (int)($request->getQueryParams()['pidDepth'] ?? 0);
|
||||
$this->levelsToFetch = $parentDepth + $this->levelsToFetch;
|
||||
foreach ($entryPoints as $page) {
|
||||
$items = array_merge($items, $this->pagesToFlatArray($page, $mountPid, $parentDepth));
|
||||
}
|
||||
} else {
|
||||
$entryPoints = $this->getAllEntryPointPageTrees();
|
||||
foreach ($entryPoints as $page) {
|
||||
$items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns JSON representing page tree filtered by keyword
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function filterDataAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$searchQuery = $request->getQueryParams()['q'] ?? '';
|
||||
if (trim($searchQuery) === '') {
|
||||
return new JsonResponse([]);
|
||||
}
|
||||
|
||||
$this->initializeConfiguration();
|
||||
$this->expandAllNodes = true;
|
||||
|
||||
$items = [];
|
||||
$entryPoints = $this->getAllEntryPointPageTrees(0, $searchQuery);
|
||||
|
||||
foreach ($entryPoints as $page) {
|
||||
if (!empty($page)) {
|
||||
$items = array_merge($items, $this->pagesToFlatArray($page, (int)$page['uid']));
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a temporary mount point
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function setTemporaryMountPointAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if (empty($request->getParsedBody()['pid'])) {
|
||||
throw new \RuntimeException(
|
||||
'Required "pid" parameter is missing.',
|
||||
1511792197
|
||||
);
|
||||
}
|
||||
$pid = (int)$request->getParsedBody()['pid'];
|
||||
|
||||
$this->getBackendUser()->uc['pageTree_temporaryMountPoint'] = $pid;
|
||||
$this->getBackendUser()->writeUC();
|
||||
$response = [
|
||||
'mountPointPath' => $this->getMountPointPath($pid)
|
||||
];
|
||||
return new JsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts nested tree structure produced by PageTreeRepository to a flat, one level array
|
||||
* and also adds visual representation information to the data.
|
||||
*
|
||||
* @param array $page
|
||||
* @param int $entryPoint
|
||||
* @param int $depth
|
||||
* @param array $inheritedData
|
||||
* @return array
|
||||
*/
|
||||
protected function pagesToFlatArray(array $page, int $entryPoint, int $depth = 0, array $inheritedData = []): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$pageId = (int)$page['uid'];
|
||||
if (in_array($pageId, $this->hiddenRecords, true)) {
|
||||
return [];
|
||||
}
|
||||
if ($pageId === 0 && !$backendUser->isAdmin()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stopPageTree = !empty($page['php_tree_stop']) && $depth > 0;
|
||||
$identifier = $entryPoint . '_' . $pageId;
|
||||
$expanded = !empty($page['expanded'])
|
||||
|| (isset($this->expandedState[$identifier]) && $this->expandedState[$identifier])
|
||||
|| $this->expandAllNodes;
|
||||
|
||||
$backgroundColor = !empty($this->backgroundColors[$pageId]) ? $this->backgroundColors[$pageId] : ($inheritedData['backgroundColor'] ?? '');
|
||||
|
||||
$suffix = '';
|
||||
$prefix = '';
|
||||
$nameSourceField = 'title';
|
||||
$visibleText = $page['title'];
|
||||
$tooltip = BackendUtility::titleAttribForPages($page, '', false);
|
||||
if ($pageId !== 0) {
|
||||
$icon = $this->iconFactory->getIconForRecord('pages', $page, Icon::SIZE_SMALL);
|
||||
} else {
|
||||
$icon = $this->iconFactory->getIcon('apps-pagetree-root', Icon::SIZE_SMALL);
|
||||
}
|
||||
|
||||
if ($this->useNavTitle && trim($page['nav_title'] ?? '') !== '') {
|
||||
$nameSourceField = 'nav_title';
|
||||
$visibleText = $page['nav_title'];
|
||||
}
|
||||
if (trim($visibleText) === '') {
|
||||
$visibleText = htmlspecialchars('[' . $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title') . ']');
|
||||
}
|
||||
|
||||
if ($this->addDomainName && $page['is_siteroot']) {
|
||||
$domain = $this->getDomainNameForPage($pageId);
|
||||
$suffix = $domain !== '' ? ' [' . $domain . ']' : '';
|
||||
}
|
||||
|
||||
$lockInfo = BackendUtility::isRecordLocked('pages', $pageId);
|
||||
if (is_array($lockInfo)) {
|
||||
$tooltip .= ' - ' . $lockInfo['msg'];
|
||||
}
|
||||
if ($this->addIdAsPrefix) {
|
||||
$prefix = htmlspecialchars('[' . $pageId . '] ');
|
||||
}
|
||||
|
||||
$items = [];
|
||||
$item = [
|
||||
// Used to track if the tree item is collapsed or not
|
||||
'stateIdentifier' => $identifier,
|
||||
'identifier' => $pageId,
|
||||
'depth' => $depth,
|
||||
'tip' => htmlspecialchars($tooltip),
|
||||
'icon' => $icon->getIdentifier(),
|
||||
'name' => $visibleText,
|
||||
'nameSourceField' => $nameSourceField,
|
||||
'alias' => htmlspecialchars($page['alias'] ?? ''),
|
||||
'selectable' => true,
|
||||
'checked' => false,
|
||||
'mountPoint' => $entryPoint,
|
||||
'workspaceId' => !empty($page['t3ver_oid']) ? $page['t3ver_oid'] : $pageId,
|
||||
'allowDelete' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_DELETE),
|
||||
'allowEdit' => $backendUser->doesUserHaveAccess($page, Permission::PAGE_EDIT)
|
||||
&& $backendUser->check('tables_modify', 'pages')
|
||||
&& $backendUser->checkLanguageAccess(0)
|
||||
];
|
||||
|
||||
if (!empty($page['_children']) || $this->getPageTreeRepository()->hasChildren($pageId)) {
|
||||
$item['hasChildren'] = true;
|
||||
if ($depth >= $this->levelsToFetch) {
|
||||
$page = $this->getPageTreeRepository()->getTreeLevels($page, 1);
|
||||
}
|
||||
}
|
||||
if (!empty($prefix)) {
|
||||
$item['prefix'] = htmlspecialchars($prefix);
|
||||
}
|
||||
if (!empty($suffix)) {
|
||||
$item['suffix'] = htmlspecialchars($suffix);
|
||||
}
|
||||
if (is_array($lockInfo)) {
|
||||
$item['locked'] = true;
|
||||
}
|
||||
if ($icon->getOverlayIcon()) {
|
||||
$item['overlayIcon'] = $icon->getOverlayIcon()->getIdentifier();
|
||||
}
|
||||
if ($expanded && is_array($page['_children']) && !empty($page['_children'])) {
|
||||
$item['expanded'] = $expanded;
|
||||
}
|
||||
if ($backgroundColor) {
|
||||
$item['backgroundColor'] = htmlspecialchars($backgroundColor);
|
||||
}
|
||||
if ($stopPageTree) {
|
||||
$item['stopPageTree'] = $stopPageTree;
|
||||
}
|
||||
$class = $this->resolvePageCssClassNames($page);
|
||||
if (!empty($class)) {
|
||||
$item['class'] = $class;
|
||||
}
|
||||
if ($depth === 0) {
|
||||
$item['isMountPoint'] = true;
|
||||
|
||||
if ($this->showMountPathAboveMounts) {
|
||||
$item['readableRootline'] = $this->getMountPointPath($pageId);
|
||||
}
|
||||
}
|
||||
|
||||
$items[] = $item;
|
||||
if (!$stopPageTree && is_array($page['_children']) && !empty($page['_children']) && ($depth < $this->levelsToFetch || $expanded)) {
|
||||
$items[key($items)]['loaded'] = true;
|
||||
foreach ($page['_children'] as $child) {
|
||||
$items = array_merge($items, $this->pagesToFlatArray($child, $entryPoint, $depth + 1, ['backgroundColor' => $backgroundColor]));
|
||||
}
|
||||
}
|
||||
return $items;
|
||||
}
|
||||
|
||||
protected function getPageTreeRepository(): PageTreeRepository
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$userTsConfig = $backendUser->getTSConfig();
|
||||
$excludedDocumentTypes = GeneralUtility::intExplode(',', $userTsConfig['options.']['pageTree.']['excludeDoktypes'] ?? '', true);
|
||||
|
||||
$additionalQueryRestrictions = [];
|
||||
if (!empty($excludedDocumentTypes)) {
|
||||
$additionalQueryRestrictions[] = GeneralUtility::makeInstance(DocumentTypeExclusionRestriction::class, $excludedDocumentTypes);
|
||||
}
|
||||
|
||||
return GeneralUtility::makeInstance(
|
||||
PageTreeRepository::class,
|
||||
(int)$backendUser->workspace,
|
||||
[],
|
||||
$additionalQueryRestrictions
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all pages for all tree entry points the user is allowed to see
|
||||
*
|
||||
* @param int $startPid
|
||||
* @param string $query The search query can either be a string to be found in the title or the nav_title of a page or the uid of a page.
|
||||
* @return array
|
||||
*/
|
||||
protected function getAllEntryPointPageTrees(int $startPid = 0, string $query = ''): array
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
$entryPointId = $startPid > 0 ? $startPid : (int)($backendUser->uc['pageTree_temporaryMountPoint'] ?? 0);
|
||||
if ($entryPointId > 0) {
|
||||
$entryPointIds = [$entryPointId];
|
||||
} else {
|
||||
//watch out for deleted pages returned as webmount
|
||||
$entryPointIds = array_map('intval', $backendUser->returnWebmounts());
|
||||
$entryPointIds = array_unique($entryPointIds);
|
||||
if (empty($entryPointIds)) {
|
||||
// use a virtual root
|
||||
// the real mount points will be fetched in getNodes() then
|
||||
// since those will be the "sub pages" of the virtual root
|
||||
$entryPointIds = [0];
|
||||
}
|
||||
}
|
||||
if (empty($entryPointIds)) {
|
||||
return [];
|
||||
}
|
||||
$repository = $this->getPageTreeRepository();
|
||||
|
||||
$permClause = $this->getBackendUser()->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
if ($query !== '') {
|
||||
$this->levelsToFetch = 999;
|
||||
$repository->fetchFilteredTree(
|
||||
$query,
|
||||
$this->getAllowedMountPoints(),
|
||||
$permClause
|
||||
);
|
||||
}
|
||||
|
||||
$entryPointRecords = [];
|
||||
foreach ($entryPointIds as $k => $entryPointId) {
|
||||
if (in_array($entryPointId, $this->hiddenRecords, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($this->backgroundColors) && is_array($this->backgroundColors)) {
|
||||
try {
|
||||
$entryPointRootLine = GeneralUtility::makeInstance(RootlineUtility::class, $entryPointId)->get();
|
||||
} catch (RootLineException $e) {
|
||||
$entryPointRootLine = [];
|
||||
}
|
||||
foreach ($entryPointRootLine as $rootLineEntry) {
|
||||
$parentUid = $rootLineEntry['uid'];
|
||||
if (!empty($this->backgroundColors[$parentUid]) && empty($this->backgroundColors[$entryPointId])) {
|
||||
$this->backgroundColors[$entryPointId] = $this->backgroundColors[$parentUid];
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($entryPointId === 0) {
|
||||
$entryPointRecord = [
|
||||
'uid' => 0,
|
||||
'title' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'] ?: 'TYPO3'
|
||||
];
|
||||
} else {
|
||||
$entryPointRecord = BackendUtility::getRecordWSOL('pages', $entryPointId, '*', $permClause);
|
||||
|
||||
if ($entryPointRecord !== null && !$this->getBackendUser()->isInWebMount($entryPointId)) {
|
||||
$entryPointRecord = null;
|
||||
}
|
||||
}
|
||||
if ($entryPointRecord) {
|
||||
$entryPointRecord['uid'] = (int)$entryPointRecord['uid'];
|
||||
if ($query === '') {
|
||||
$entryPointRecord = $repository->getTreeLevels($entryPointRecord, $this->levelsToFetch);
|
||||
} else {
|
||||
$entryPointRecord = $repository->getTree((int)$entryPointRecord['uid'], null, $entryPointIds, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($entryPointRecord) && !empty($entryPointRecord)) {
|
||||
$entryPointRecords[$k] = $entryPointRecord;
|
||||
}
|
||||
}
|
||||
|
||||
return $entryPointRecords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first configured domain name for a page
|
||||
*
|
||||
* @param int $pageId
|
||||
* @return string
|
||||
*/
|
||||
protected function getDomainNameForPage(int $pageId): string
|
||||
{
|
||||
$domain = '';
|
||||
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
|
||||
try {
|
||||
$site = $siteFinder->getSiteByRootPageId($pageId);
|
||||
$domain = (string)$site->getBase();
|
||||
} catch (SiteNotFoundException $e) {
|
||||
// No site found, let's see if it is a legacy-pseudo-site
|
||||
$pseudoSiteFinder = GeneralUtility::makeInstance(PseudoSiteFinder::class);
|
||||
try {
|
||||
$site = $pseudoSiteFinder->getSiteByRootPageId($pageId);
|
||||
$domain = trim((string)$site->getBase(), '/');
|
||||
} catch (SiteNotFoundException $e) {
|
||||
// No pseudo-site found either
|
||||
}
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mount point path for a temporary mount or the given id
|
||||
*
|
||||
* @param int $uid
|
||||
* @return string
|
||||
*/
|
||||
protected function getMountPointPath(int $uid): string
|
||||
{
|
||||
if ($uid <= 0) {
|
||||
return '';
|
||||
}
|
||||
$rootline = array_reverse(BackendUtility::BEgetRootLine($uid));
|
||||
array_shift($rootline);
|
||||
$path = [];
|
||||
foreach ($rootline as $rootlineElement) {
|
||||
$record = BackendUtility::getRecordWSOL('pages', $rootlineElement['uid'], 'title, nav_title', '', true, true);
|
||||
$text = $record['title'];
|
||||
if ($this->useNavTitle && trim($record['nav_title'] ?? '') !== '') {
|
||||
$text = $record['nav_title'];
|
||||
}
|
||||
$path[] = htmlspecialchars($text);
|
||||
}
|
||||
return '/' . implode('/', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches possible css class names to be used when a record was modified in a workspace
|
||||
*
|
||||
* @param array $page Page record (workspace overlaid)
|
||||
* @return string CSS class names to be applied
|
||||
*/
|
||||
protected function resolvePageCssClassNames(array $page): string
|
||||
{
|
||||
$classes = [];
|
||||
|
||||
if ($page['uid'] === 0) {
|
||||
return '';
|
||||
}
|
||||
$workspaceId = (int)$this->getBackendUser()->workspace;
|
||||
if ($workspaceId > 0 && ExtensionManagementUtility::isLoaded('workspaces')) {
|
||||
if ($page['t3ver_oid'] > 0 && (int)$page['t3ver_wsid'] === $workspaceId) {
|
||||
$classes[] = 'ver-element';
|
||||
$classes[] = 'ver-versions';
|
||||
} elseif (
|
||||
$this->getWorkspaceService()->hasPageRecordVersions(
|
||||
$workspaceId,
|
||||
$page['t3ver_oid'] ?: $page['uid']
|
||||
)
|
||||
) {
|
||||
$classes[] = 'ver-versions';
|
||||
}
|
||||
}
|
||||
|
||||
return implode(' ', $classes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if drag-move in the svg tree is allowed for the user
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function isDragMoveAllowed(): bool
|
||||
{
|
||||
$backendUser = $this->getBackendUser();
|
||||
return $backendUser->isAdmin()
|
||||
|| ($backendUser->check('tables_modify', 'pages') && $backendUser->checkLanguageAccess(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allowed mountpoints. Returns temporary mountpoint when temporary mountpoint is used.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
protected function getAllowedMountPoints(): array
|
||||
{
|
||||
$mountPoints = (int)($this->getBackendUser()->uc['pageTree_temporaryMountPoint'] ?? 0);
|
||||
if (!$mountPoints) {
|
||||
$mountPoints = array_map('intval', $this->getBackendUser()->returnWebmounts());
|
||||
return array_unique($mountPoints);
|
||||
}
|
||||
return [$mountPoints];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return WorkspaceService
|
||||
*/
|
||||
protected function getWorkspaceService(): WorkspaceService
|
||||
{
|
||||
return GeneralUtility::makeInstance(WorkspaceService::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService|null
|
||||
*/
|
||||
protected function getLanguageService(): ?LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'] ?? null;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Backend\Shortcut\ShortcutRepository;
|
||||
use TYPO3\CMS\Backend\Backend\ToolbarItems\ShortcutToolbarItem;
|
||||
use TYPO3\CMS\Backend\Module\ModuleLoader;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Controller for shortcut processing
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ShortcutController
|
||||
{
|
||||
/**
|
||||
* @var ShortcutToolbarItem
|
||||
*/
|
||||
protected $shortcutToolbarItem;
|
||||
|
||||
/**
|
||||
* @var ShortcutRepository
|
||||
*/
|
||||
protected $shortcutRepository;
|
||||
|
||||
/**
|
||||
* @var ModuleLoader
|
||||
*/
|
||||
protected $moduleLoader;
|
||||
|
||||
/**
|
||||
* Set up dependencies
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->shortcutToolbarItem = GeneralUtility::makeInstance(ShortcutToolbarItem::class);
|
||||
$this->shortcutRepository = GeneralUtility::makeInstance(ShortcutRepository::class);
|
||||
// Needed to get the correct icons when reloading the menu after saving it
|
||||
$this->moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
|
||||
$this->moduleLoader->load($GLOBALS['TBE_MODULES']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the menu so that it can be returned as response to an AJAX call
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function menuAction(): ResponseInterface
|
||||
{
|
||||
return new HtmlResponse($this->shortcutToolbarItem->getDropDown());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a shortcut through an AJAX call
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function addAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$result = 'success';
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$url = rawurldecode($parsedBody['url'] ?? $queryParams['url'] ?? '');
|
||||
|
||||
if ($this->shortcutRepository->shortcutExists($url)) {
|
||||
$result = 'alreadyExists';
|
||||
} else {
|
||||
$moduleName = $parsedBody['module'] ?? '';
|
||||
$parentModuleName = $parsedBody['motherModName'] ?? '';
|
||||
$shortcutName = $parsedBody['displayName'] ?? '';
|
||||
$success = $this->shortcutRepository->addShortcut($url, $moduleName, $parentModuleName, $shortcutName);
|
||||
|
||||
if (!$success) {
|
||||
$result = 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
return new HtmlResponse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the available shortcut groups, renders a form so it can be saved later on, called via AJAX
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface the full HTML for the form
|
||||
*/
|
||||
public function showEditFormAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$selectedShortcutId = (int)($parsedBody['shortcutId'] ?? $queryParams['shortcutId']);
|
||||
$selectedShortcutGroupId = (int)($parsedBody['shortcutGroup'] ?? $queryParams['shortcutGroup']);
|
||||
$selectedShortcut = $this->shortcutRepository->getShortcutById($selectedShortcutId);
|
||||
$shortcutGroups = $this->shortcutRepository->getShortcutGroups();
|
||||
|
||||
$editFormView = $this->getFluidTemplateObject('EditForm.html');
|
||||
$editFormView->assign('selectedShortcutId', $selectedShortcutId);
|
||||
$editFormView->assign('selectedShortcutGroupId', $selectedShortcutGroupId);
|
||||
$editFormView->assign('selectedShortcut', $selectedShortcut);
|
||||
$editFormView->assign('shortcutGroups', $shortcutGroups);
|
||||
|
||||
return new HtmlResponse($editFormView->render());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets called when a shortcut is changed, checks whether the user has
|
||||
* permissions to do so and saves the changes if everything is ok
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function updateAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$shortcutId = (int)($parsedBody['shortcutId'] ?? $queryParams['shortcutId'] ?? 0);
|
||||
$shortcutTitle = strip_tags($parsedBody['shortcutTitle'] ?? $queryParams['shortcutTitle'] ?? '');
|
||||
$shortcutGroupId = (int)($parsedBody['shortcutGroup'] ?? $queryParams['shortcutGroup'] ?? 0);
|
||||
|
||||
$success = $this->shortcutRepository->updateShortcut($shortcutId, $shortcutTitle, $shortcutGroupId);
|
||||
|
||||
return new HtmlResponse($success ? $shortcutTitle : 'failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a shortcut through an AJAX call
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function removeAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
$shortcutId = (int)($parsedBody['shortcutId'] ?? $queryParams['shortcutId'] ?? 0);
|
||||
$success = $this->shortcutRepository->removeShortcut($shortcutId);
|
||||
|
||||
return new JsonResponse(['success' => $success]);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns a new standalone view, shorthand function
|
||||
*
|
||||
* @param string $templateFilename
|
||||
* @return StandaloneView
|
||||
* @throws \TYPO3\CMS\Extbase\Mvc\Exception\InvalidExtensionNameException
|
||||
* @throws \InvalidArgumentException
|
||||
* @internal param string $templateFile
|
||||
*/
|
||||
protected function getFluidTemplateObject(string $templateFilename): StandaloneView
|
||||
{
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
$view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
|
||||
$view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/ShortcutToolbarItem']);
|
||||
$view->setTemplate($templateFilename);
|
||||
$view->getRequest()->setControllerExtensionName('Backend');
|
||||
|
||||
return $view;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Clipboard\Clipboard;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Messaging\AbstractMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Script Class, creating object of \TYPO3\CMS\Core\DataHandling\DataHandler and
|
||||
* sending the posted data to the object.
|
||||
*
|
||||
* Used by many smaller forms/links in TYPO3, including the QuickEdit module.
|
||||
* Is not used by FormEngine though (main form rendering script) - that uses the same class (DataHandler) but makes its own initialization (to save the redirect request).
|
||||
* For all other cases than FormEngine it is recommended to use this script for submitting your editing forms - but the best solution in any case would probably be to link your application to FormEngine, that will give you easy form-rendering as well.
|
||||
*/
|
||||
class SimpleDataHandlerController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'flags' => 'Using $flags of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'data' => 'Using $data of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'cmd' => 'Using $cmd of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'mirror' => 'Using $mirror of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'cacheCmd' => 'Using $cacheCmd of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'redirect' => 'Using $redirect of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'CB' => 'Using $CB of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'tce' => 'Using $tce of class SimpleDataHandlerController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Array. Accepts options to be set in TCE object. Currently it supports "reverseOrder" (bool).
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $flags;
|
||||
|
||||
/**
|
||||
* Data array on the form [tablename][uid][fieldname] = value
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data;
|
||||
|
||||
/**
|
||||
* Command array on the form [tablename][uid][command] = value.
|
||||
* This array may get additional data set internally based on clipboard commands send in CB var!
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cmd;
|
||||
|
||||
/**
|
||||
* Array passed to ->setMirror.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $mirror;
|
||||
|
||||
/**
|
||||
* Cache command sent to ->clear_cacheCmd
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $cacheCmd;
|
||||
|
||||
/**
|
||||
* Redirect URL. Script will redirect to this location after performing operations (unless errors has occurred)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirect;
|
||||
|
||||
/**
|
||||
* Clipboard command array. May trigger changes in "cmd"
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $CB;
|
||||
|
||||
/**
|
||||
* TYPO3 Core Engine
|
||||
*
|
||||
* @var \TYPO3\CMS\Core\DataHandling\DataHandler
|
||||
*/
|
||||
protected $tce;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0 with removal of init()
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
$GLOBALS['SOBE'] = $this;
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the processRequest() method, it just redirects to the given URL afterwards.
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->initializeClipboard();
|
||||
$this->processRequest();
|
||||
|
||||
// Write errors to flash message queue
|
||||
$this->tce->printLogErrorMessages();
|
||||
if ($this->redirect) {
|
||||
return new RedirectResponse(GeneralUtility::locationHeaderUrl($this->redirect), 303);
|
||||
}
|
||||
return new HtmlResponse('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all AJAX calls and returns a JSON formatted string
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function processAjaxRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// do the regular / main logic
|
||||
$this->initializeClipboard();
|
||||
$this->processRequest();
|
||||
|
||||
/** @var \TYPO3\CMS\Core\Messaging\FlashMessageService $flashMessageService */
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
|
||||
$content = [
|
||||
'redirect' => $this->redirect,
|
||||
'messages' => [],
|
||||
'hasErrors' => false
|
||||
];
|
||||
|
||||
// Prints errors (= write them to the message queue)
|
||||
$this->tce->printLogErrorMessages();
|
||||
|
||||
$messages = $flashMessageService->getMessageQueueByIdentifier()->getAllMessagesAndFlush();
|
||||
if (!empty($messages)) {
|
||||
foreach ($messages as $message) {
|
||||
$content['messages'][] = [
|
||||
'title' => $message->getTitle(),
|
||||
'message' => $message->getMessage(),
|
||||
'severity' => $message->getSeverity()
|
||||
];
|
||||
if ($message->getSeverity() === AbstractMessage::ERROR) {
|
||||
$content['hasErrors'] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return new JsonResponse($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of the class
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
public function init(ServerRequestInterface $request = null): void
|
||||
{
|
||||
if ($request === null) {
|
||||
// Method signature in TYPO3 v10.0: protected function init(ServerRequestInterface $request)
|
||||
trigger_error('SimpleDataHandlerController->init() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
}
|
||||
|
||||
$beUser = $this->getBackendUser();
|
||||
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
// GPvars:
|
||||
$this->flags = $parsedBody['flags'] ?? $queryParams['flags'] ?? null;
|
||||
$this->data = $parsedBody['data'] ?? $queryParams['data'] ?? null;
|
||||
$this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? null;
|
||||
$this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? null;
|
||||
$this->cacheCmd = $parsedBody['cacheCmd'] ?? $queryParams['cacheCmd'] ?? null;
|
||||
$redirect = $parsedBody['redirect'] ?? $queryParams['redirect'] ?? '';
|
||||
$this->redirect = GeneralUtility::sanitizeLocalUrl($redirect);
|
||||
$this->CB = $parsedBody['CB'] ?? $queryParams['CB'] ?? null;
|
||||
// Creating DataHandler object
|
||||
$this->tce = GeneralUtility::makeInstance(DataHandler::class);
|
||||
// Configuring based on user prefs.
|
||||
if ($beUser->uc['recursiveDelete']) {
|
||||
// TRUE if the delete Recursive flag is set.
|
||||
$this->tce->deleteTree = 1;
|
||||
}
|
||||
if ($beUser->uc['copyLevels']) {
|
||||
// Set to number of page-levels to copy.
|
||||
$this->tce->copyTree = MathUtility::forceIntegerInRange($beUser->uc['copyLevels'], 0, 100);
|
||||
}
|
||||
if ($beUser->uc['neverHideAtCopy']) {
|
||||
$this->tce->neverHideAtCopy = 1;
|
||||
}
|
||||
// Reverse order.
|
||||
if ($this->flags['reverseOrder']) {
|
||||
$this->tce->reverseOrder = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clipboard pasting and deleting.
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function initClipboard()
|
||||
{
|
||||
trigger_error('SimpleDataHandlerController->initClipboard() will be replaced by protected method initializeClipboard() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->initializeClipboard();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executing the posted actions ...
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('SimpleDataHandlerController->main() will be replaced by protected method processRequest() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->processRequest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clipboard pasting and deleting.
|
||||
*/
|
||||
protected function initializeClipboard(): void
|
||||
{
|
||||
if (is_array($this->CB)) {
|
||||
$clipObj = GeneralUtility::makeInstance(Clipboard::class);
|
||||
$clipObj->initializeClipboard();
|
||||
if ($this->CB['paste']) {
|
||||
$clipObj->setCurrentPad($this->CB['pad']);
|
||||
$this->cmd = $clipObj->makePasteCmdArray(
|
||||
$this->CB['paste'],
|
||||
$this->cmd,
|
||||
$this->CB['update'] ?? null
|
||||
);
|
||||
}
|
||||
if ($this->CB['delete']) {
|
||||
$clipObj->setCurrentPad($this->CB['pad']);
|
||||
$this->cmd = $clipObj->makeDeleteCmdArray($this->cmd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executing the posted actions ...
|
||||
*/
|
||||
protected function processRequest(): void
|
||||
{
|
||||
// LOAD DataHandler with data and cmd arrays:
|
||||
$this->tce->start($this->data, $this->cmd);
|
||||
if (is_array($this->mirror)) {
|
||||
$this->tce->setMirror($this->mirror);
|
||||
}
|
||||
// Register uploaded files
|
||||
$this->tce->process_uploads($_FILES);
|
||||
// Execute actions:
|
||||
$this->tce->process_datamap();
|
||||
$this->tce->process_cmdmap();
|
||||
// Clearing cache:
|
||||
if (!empty($this->cacheCmd)) {
|
||||
$this->tce->clear_cacheCmd($this->cacheCmd);
|
||||
}
|
||||
// Update page tree?
|
||||
if (isset($this->data['pages']) || isset($this->cmd['pages'])) {
|
||||
BackendUtility::setUpdateSignal('updatePageTree');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,688 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration;
|
||||
use TYPO3\CMS\Backend\Exception\SiteValidationErrorException;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup;
|
||||
use TYPO3\CMS\Backend\Form\FormResultCompiler;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\SiteConfiguration;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageService;
|
||||
use TYPO3\CMS\Core\Site\SiteFinder;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
use TYPO3\CMS\Frontend\Page\PageRepository;
|
||||
use TYPO3Fluid\Fluid\View\ViewInterface;
|
||||
|
||||
/**
|
||||
* Backend controller: The "Site management" -> "Sites" module
|
||||
*
|
||||
* List all site root pages, CRUD site configuration.
|
||||
*
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SiteConfigurationController
|
||||
{
|
||||
protected const ALLOWED_ACTIONS = ['overview', 'edit', 'save', 'delete'];
|
||||
|
||||
/**
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* @var ViewInterface
|
||||
*/
|
||||
protected $view;
|
||||
|
||||
/**
|
||||
* @var SiteFinder
|
||||
*/
|
||||
protected $siteFinder;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry method: Dispatch to other actions - those method names that end with "Action".
|
||||
*
|
||||
* @param ServerRequestInterface $request the current request
|
||||
* @return ResponseInterface the response with the content
|
||||
*/
|
||||
public function handleRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// forcing uncached sites will re-initialize `SiteFinder`
|
||||
// which is used later by FormEngine (implicit behavior)
|
||||
$this->siteFinder->getAllSites(false);
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/ContextMenu');
|
||||
$this->moduleTemplate->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
|
||||
$action = $request->getQueryParams()['action'] ?? $request->getParsedBody()['action'] ?? 'overview';
|
||||
|
||||
if (!in_array($action, self::ALLOWED_ACTIONS, true)) {
|
||||
return new HtmlResponse('Action not allowed', 400);
|
||||
}
|
||||
|
||||
$this->initializeView($action);
|
||||
|
||||
$result = call_user_func_array([$this, $action . 'Action'], [$request]);
|
||||
if ($result instanceof ResponseInterface) {
|
||||
return $result;
|
||||
}
|
||||
$this->moduleTemplate->setContent($this->view->render());
|
||||
return new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* List pages that have 'is_siteroot' flag set - those that have the globe icon in page tree.
|
||||
* Link to Add / Edit / Delete for each.
|
||||
*/
|
||||
protected function overviewAction(): void
|
||||
{
|
||||
$this->configureOverViewDocHeader();
|
||||
$allSites = $this->siteFinder->getAllSites();
|
||||
$pages = $this->getAllSitePages();
|
||||
$unassignedSites = [];
|
||||
foreach ($allSites as $identifier => $site) {
|
||||
$rootPageId = $site->getRootPageId();
|
||||
if (isset($pages[$rootPageId])) {
|
||||
$pages[$rootPageId]['siteIdentifier'] = $identifier;
|
||||
$pages[$rootPageId]['siteConfiguration'] = $site;
|
||||
} else {
|
||||
$unassignedSites[] = $site;
|
||||
}
|
||||
}
|
||||
$this->view->assignMultiple([
|
||||
'pages' => $pages,
|
||||
'unassignedSites' => $unassignedSites
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a form to create a new site configuration, or edit an existing one.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function editAction(ServerRequestInterface $request): void
|
||||
{
|
||||
$this->configureEditViewDocHeader();
|
||||
|
||||
// Put site and friends TCA into global TCA
|
||||
// @todo: We might be able to get rid of that later
|
||||
$GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
|
||||
|
||||
$siteIdentifier = $request->getQueryParams()['site'] ?? null;
|
||||
$pageUid = (int)($request->getQueryParams()['pageUid'] ?? 0);
|
||||
|
||||
if (empty($siteIdentifier) && empty($pageUid)) {
|
||||
throw new \RuntimeException('Either site identifier to edit a config or page uid to add new config must be set', 1521561148);
|
||||
}
|
||||
$isNewConfig = empty($siteIdentifier);
|
||||
|
||||
$defaultValues = [];
|
||||
if ($isNewConfig) {
|
||||
$defaultValues['site']['rootPageId'] = $pageUid;
|
||||
}
|
||||
|
||||
$allSites = $this->siteFinder->getAllSites();
|
||||
if (!$isNewConfig && !isset($allSites[$siteIdentifier])) {
|
||||
throw new \RuntimeException('Existing config for site ' . $siteIdentifier . ' not found', 1521561226);
|
||||
}
|
||||
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$returnUrl = $uriBuilder->buildUriFromRoute('site_configuration');
|
||||
|
||||
$formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'tableName' => 'site',
|
||||
'vanillaUid' => $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId(),
|
||||
'command' => $isNewConfig ? 'new' : 'edit',
|
||||
'returnUrl' => (string)$returnUrl,
|
||||
'customData' => [
|
||||
'siteIdentifier' => $isNewConfig ? '' : $siteIdentifier,
|
||||
],
|
||||
'defaultValues' => $defaultValues,
|
||||
];
|
||||
$formData = $formDataCompiler->compile($formDataCompilerInput);
|
||||
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
|
||||
$formData['renderType'] = 'outerWrapContainer';
|
||||
$formResult = $nodeFactory->create($formData)->render();
|
||||
// Needed to be set for 'onChange="reload"' and reload on type change to work
|
||||
$formResult['doSaveFieldName'] = 'doSave';
|
||||
$formResultCompiler = GeneralUtility::makeInstance(FormResultCompiler::class);
|
||||
$formResultCompiler->mergeResult($formResult);
|
||||
$formResultCompiler->addCssFiles();
|
||||
// Always add rootPageId as additional field to have a reference for new records
|
||||
$this->view->assign('rootPageId', $isNewConfig ? $pageUid : $allSites[$siteIdentifier]->getRootPageId());
|
||||
$this->view->assign('returnUrl', $returnUrl);
|
||||
$this->view->assign('formEngineHtml', $formResult['html']);
|
||||
$this->view->assign('formEngineFooter', $formResultCompiler->printNeededJSFunctions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Save incoming data from editAction and redirect to overview or edit
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function saveAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Put site and friends TCA into global TCA
|
||||
// @todo: We might be able to get rid of that later
|
||||
$GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
|
||||
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$siteTca = GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca();
|
||||
|
||||
$overviewRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']);
|
||||
$parsedBody = $request->getParsedBody();
|
||||
if (isset($parsedBody['closeDoc']) && (int)$parsedBody['closeDoc'] === 1) {
|
||||
// Closing means no save, just redirect to overview
|
||||
return new RedirectResponse($overviewRoute);
|
||||
}
|
||||
$isSave = $parsedBody['_savedok'] ?? $parsedBody['doSave'] ?? false;
|
||||
$isSaveClose = $parsedBody['_saveandclosedok'] ?? false;
|
||||
if (!$isSave && !$isSaveClose) {
|
||||
throw new \RuntimeException('Either save or save and close', 1520370364);
|
||||
}
|
||||
|
||||
if (!isset($parsedBody['data']['site']) || !is_array($parsedBody['data']['site'])) {
|
||||
throw new \RuntimeException('No site data or site identifier given', 1521030950);
|
||||
}
|
||||
|
||||
$data = $parsedBody['data'];
|
||||
// This can be NEW123 for new records
|
||||
$pageId = (int)key($data['site']);
|
||||
$sysSiteRow = current($data['site']);
|
||||
$siteIdentifier = $sysSiteRow['identifier'] ?? '';
|
||||
|
||||
$isNewConfiguration = false;
|
||||
$currentIdentifier = '';
|
||||
try {
|
||||
$currentSite = $this->siteFinder->getSiteByRootPageId($pageId);
|
||||
$currentSiteConfiguration = $currentSite->getConfiguration();
|
||||
$currentIdentifier = $currentSite->getIdentifier();
|
||||
} catch (SiteNotFoundException $e) {
|
||||
$currentSiteConfiguration = [];
|
||||
$isNewConfiguration = true;
|
||||
$pageId = (int)$parsedBody['rootPageId'];
|
||||
if (!$pageId > 0) {
|
||||
// Early validation of rootPageId - it must always be given and greater than 0
|
||||
throw new \RuntimeException('No root page id found', 1521719709);
|
||||
}
|
||||
}
|
||||
|
||||
// Validate site identifier and do not store or further process it
|
||||
$siteIdentifier = $this->validateAndProcessIdentifier($isNewConfiguration, $siteIdentifier, $pageId);
|
||||
unset($sysSiteRow['identifier']);
|
||||
|
||||
try {
|
||||
$newSysSiteData = [];
|
||||
// Hard set rootPageId: This is TCA readOnly and not transmitted by FormEngine, but is also the "uid" of the site record
|
||||
$newSysSiteData['rootPageId'] = $pageId;
|
||||
foreach ($sysSiteRow as $fieldName => $fieldValue) {
|
||||
$type = $siteTca['site']['columns'][$fieldName]['config']['type'];
|
||||
switch ($type) {
|
||||
case 'input':
|
||||
case 'text':
|
||||
$fieldValue = $this->validateAndProcessValue('site', $fieldName, $fieldValue);
|
||||
$newSysSiteData[$fieldName] = $fieldValue;
|
||||
break;
|
||||
|
||||
case 'inline':
|
||||
$newSysSiteData[$fieldName] = [];
|
||||
$childRowIds = GeneralUtility::trimExplode(',', $fieldValue, true);
|
||||
if (!isset($siteTca['site']['columns'][$fieldName]['config']['foreign_table'])) {
|
||||
throw new \RuntimeException('No foreign_table found for inline type', 1521555037);
|
||||
}
|
||||
$foreignTable = $siteTca['site']['columns'][$fieldName]['config']['foreign_table'];
|
||||
foreach ($childRowIds as $childRowId) {
|
||||
$childRowData = [];
|
||||
if (!isset($data[$foreignTable][$childRowId])) {
|
||||
if (!empty($currentSiteConfiguration[$fieldName][$childRowId])) {
|
||||
// A collapsed inline record: Fetch data from existing config
|
||||
$newSysSiteData[$fieldName][] = $currentSiteConfiguration[$fieldName][$childRowId];
|
||||
continue;
|
||||
}
|
||||
throw new \RuntimeException('No data found for table ' . $foreignTable . ' with id ' . $childRowId, 1521555177);
|
||||
}
|
||||
$childRow = $data[$foreignTable][$childRowId];
|
||||
foreach ($childRow as $childFieldName => $childFieldValue) {
|
||||
if ($childFieldName === 'pid') {
|
||||
// pid is added by inline by default, but not relevant for yml storage
|
||||
continue;
|
||||
}
|
||||
$type = $siteTca[$foreignTable]['columns'][$childFieldName]['config']['type'];
|
||||
switch ($type) {
|
||||
case 'input':
|
||||
case 'select':
|
||||
case 'text':
|
||||
$childRowData[$childFieldName] = $childFieldValue;
|
||||
break;
|
||||
case 'check':
|
||||
$childRowData[$childFieldName] = (bool)$childFieldValue;
|
||||
break;
|
||||
default:
|
||||
throw new \RuntimeException('TCA type ' . $type . ' not implemented in site handling', 1521555340);
|
||||
}
|
||||
}
|
||||
$newSysSiteData[$fieldName][] = $childRowData;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
$newSysSiteData[$fieldName] = MathUtility::canBeInterpretedAsInteger($fieldValue) ? (int)$fieldValue : $fieldValue;
|
||||
break;
|
||||
|
||||
case 'check':
|
||||
$newSysSiteData[$fieldName] = (bool)$fieldValue;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new \RuntimeException('TCA type "' . $type . '" is not implemented in site handling', 1521032781);
|
||||
}
|
||||
}
|
||||
|
||||
// keep root config objects not given via GUI
|
||||
// this way extension authors are able to use their own objects on root level
|
||||
// that are not configurable via GUI
|
||||
// however: we overwrite the full subset of any GUI object to make sure we have a clean state
|
||||
$newSysSiteData = array_merge($currentSiteConfiguration, $newSysSiteData);
|
||||
$newSiteConfiguration = $this->validateFullStructure($newSysSiteData);
|
||||
|
||||
// Persist the configuration
|
||||
$siteConfigurationManager = GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites');
|
||||
if (!$isNewConfiguration && $currentIdentifier !== $siteIdentifier) {
|
||||
$siteConfigurationManager->rename($currentIdentifier, $siteIdentifier);
|
||||
}
|
||||
$siteConfigurationManager->write($siteIdentifier, $newSiteConfiguration);
|
||||
} catch (SiteValidationErrorException $e) {
|
||||
// Do not store new config if a validation error is thrown, but redirect only to show a generated flash message
|
||||
}
|
||||
|
||||
$saveRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'edit', 'site' => $siteIdentifier]);
|
||||
if ($isSaveClose) {
|
||||
return new RedirectResponse($overviewRoute);
|
||||
}
|
||||
return new RedirectResponse($saveRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation and processing of site identifier
|
||||
*
|
||||
* @param bool $isNew If true, we're dealing with a new record
|
||||
* @param string $identifier Given identifier to validate and process
|
||||
* @param int $rootPageId Page uid this identifier is bound to
|
||||
* @return mixed Verified / modified value
|
||||
*/
|
||||
protected function validateAndProcessIdentifier(bool $isNew, string $identifier, int $rootPageId)
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
// Normal "eval" processing of field first
|
||||
$identifier = $this->validateAndProcessValue('site', 'identifier', $identifier);
|
||||
if ($isNew) {
|
||||
// Verify no other site with this identifier exists. If so, find a new unique name as
|
||||
// identifier and show a flash message the identifier has been adapted
|
||||
try {
|
||||
$this->siteFinder->getSiteByIdentifier($identifier);
|
||||
// Force this identifier to be unique
|
||||
$originalIdentifier = $identifier;
|
||||
$identifier = $identifier . '-' . str_replace('.', '', uniqid((string)mt_rand(), true));
|
||||
$message = sprintf(
|
||||
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.message'),
|
||||
$originalIdentifier,
|
||||
$identifier
|
||||
);
|
||||
$messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierRenamed.title');
|
||||
$flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$defaultFlashMessageQueue->enqueue($flashMessage);
|
||||
} catch (SiteNotFoundException $e) {
|
||||
// Do nothing, this new identifier is ok
|
||||
}
|
||||
} else {
|
||||
// If this is an existing config, the site for this identifier must have the same rootPageId, otherwise
|
||||
// a user tried to rename a site identifier to a different site that already exists. If so, we do not rename
|
||||
// the site and show a flash message
|
||||
try {
|
||||
$site = $this->siteFinder->getSiteByIdentifier($identifier);
|
||||
if ($site->getRootPageId() !== $rootPageId) {
|
||||
// Find original value and keep this
|
||||
$origSite = $this->siteFinder->getSiteByRootPageId($rootPageId);
|
||||
$originalIdentifier = $identifier;
|
||||
$identifier = $origSite->getIdentifier();
|
||||
$message = sprintf(
|
||||
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.message'),
|
||||
$originalIdentifier,
|
||||
$identifier
|
||||
);
|
||||
$messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.identifierExists.title');
|
||||
$flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$defaultFlashMessageQueue->enqueue($flashMessage);
|
||||
}
|
||||
} catch (SiteNotFoundException $e) {
|
||||
// User is renaming identifier which does not exist yet. That's ok
|
||||
}
|
||||
}
|
||||
return $identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple validation and processing method for incoming form field values.
|
||||
*
|
||||
* Note this does not support all TCA "eval" options but only what we really need.
|
||||
*
|
||||
* @param string $tableName Table name
|
||||
* @param string $fieldName Field name
|
||||
* @param mixed $fieldValue Incoming value from FormEngine
|
||||
* @return mixed Verified / modified value
|
||||
* @throws SiteValidationErrorException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function validateAndProcessValue(string $tableName, string $fieldName, $fieldValue)
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
|
||||
$handledEvals = [];
|
||||
if (!empty($fieldConfig['eval'])) {
|
||||
$evalArray = GeneralUtility::trimExplode(',', $fieldConfig['eval'], true);
|
||||
// Processing
|
||||
if (in_array('alphanum_x', $evalArray, true)) {
|
||||
$handledEvals[] = 'alphanum_x';
|
||||
$fieldValue = preg_replace('/[^a-zA-Z0-9_-]/', '', $fieldValue);
|
||||
}
|
||||
if (in_array('lower', $evalArray, true)) {
|
||||
$handledEvals[] = 'lower';
|
||||
$fieldValue = mb_strtolower($fieldValue, 'utf-8');
|
||||
}
|
||||
if (in_array('trim', $evalArray, true)) {
|
||||
$handledEvals[] = 'trim';
|
||||
$fieldValue = trim($fieldValue);
|
||||
}
|
||||
if (in_array('int', $evalArray, true)) {
|
||||
$handledEvals[] = 'int';
|
||||
$fieldValue = (int)$fieldValue;
|
||||
}
|
||||
// Validation throws - these should be handled client side already,
|
||||
// eg. 'required' being set and receiving empty, shouldn't happen server side
|
||||
if (in_array('required', $evalArray, true)) {
|
||||
$handledEvals[] = 'required';
|
||||
if (empty($fieldValue)) {
|
||||
$message = sprintf(
|
||||
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.message'),
|
||||
$fieldName
|
||||
);
|
||||
$messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.required.title');
|
||||
$flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$defaultFlashMessageQueue->enqueue($flashMessage);
|
||||
throw new SiteValidationErrorException(
|
||||
'Field ' . $fieldName . ' is set to required, but received empty.',
|
||||
1521726421
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!empty(array_diff($evalArray, $handledEvals))) {
|
||||
throw new \RuntimeException('At least one not implemented \'eval\' in list ' . $fieldConfig['eval'], 1522491734);
|
||||
}
|
||||
}
|
||||
if (isset($fieldConfig['range']['lower'])) {
|
||||
$fieldValue = (int)$fieldValue < (int)$fieldConfig['range']['lower'] ? (int)$fieldConfig['range']['lower'] : (int)$fieldValue;
|
||||
}
|
||||
if (isset($fieldConfig['range']['upper'])) {
|
||||
$fieldValue = (int)$fieldValue > (int)$fieldConfig['range']['upper'] ? (int)$fieldConfig['range']['upper'] : (int)$fieldValue;
|
||||
}
|
||||
return $fieldValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Last sanitation method after all data has been gathered. Check integrity
|
||||
* of full record, manipulate if possible, or throw exception if unfixable broken.
|
||||
*
|
||||
* @param array $newSysSiteData Incoming data
|
||||
* @return array Updated data if needed
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function validateFullStructure(array $newSysSiteData): array
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
// Verify there are not two error handlers with the same error code
|
||||
if (isset($newSysSiteData['errorHandling']) && is_array($newSysSiteData['errorHandling'])) {
|
||||
$uniqueCriteria = [];
|
||||
$validChildren = [];
|
||||
foreach ($newSysSiteData['errorHandling'] as $child) {
|
||||
if (!isset($child['errorCode'])) {
|
||||
throw new \RuntimeException('No errorCode found', 1521788518);
|
||||
}
|
||||
if (!in_array((int)$child['errorCode'], $uniqueCriteria, true)) {
|
||||
$uniqueCriteria[] = (int)$child['errorCode'];
|
||||
$validChildren[] = $child;
|
||||
} else {
|
||||
$message = sprintf(
|
||||
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.message'),
|
||||
$child['errorCode']
|
||||
);
|
||||
$messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateErrorCode.title');
|
||||
$flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$defaultFlashMessageQueue->enqueue($flashMessage);
|
||||
}
|
||||
}
|
||||
$newSysSiteData['errorHandling'] = $validChildren;
|
||||
}
|
||||
|
||||
// Verify there is only one inline child per sys_language record configured.
|
||||
if (!isset($newSysSiteData['languages']) || !is_array($newSysSiteData['languages']) || count($newSysSiteData['languages']) < 1) {
|
||||
throw new \RuntimeException(
|
||||
'No default language definition found. The interface does not allow this. Aborting',
|
||||
1521789306
|
||||
);
|
||||
}
|
||||
$uniqueCriteria = [];
|
||||
$validChildren = [];
|
||||
foreach ($newSysSiteData['languages'] as $child) {
|
||||
if (!isset($child['languageId'])) {
|
||||
throw new \RuntimeException('languageId not found', 1521789455);
|
||||
}
|
||||
if (!in_array((int)$child['languageId'], $uniqueCriteria, true)) {
|
||||
$uniqueCriteria[] = (int)$child['languageId'];
|
||||
$validChildren[] = $child;
|
||||
} else {
|
||||
$message = sprintf(
|
||||
$languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title'),
|
||||
$child['languageId']
|
||||
);
|
||||
$messageTitle = $languageService->sL('LLL:EXT:backend/Resources/Private/Language/locallang_siteconfiguration.xlf:validation.duplicateLanguageId.title');
|
||||
$flashMessage = GeneralUtility::makeInstance(FlashMessage::class, $message, $messageTitle, FlashMessage::WARNING, true);
|
||||
$flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
|
||||
$defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
|
||||
$defaultFlashMessageQueue->enqueue($flashMessage);
|
||||
}
|
||||
}
|
||||
$newSysSiteData['languages'] = $validChildren;
|
||||
|
||||
return $newSysSiteData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing configuration
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function deleteAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$siteIdentifier = $request->getQueryParams()['site'] ?? '';
|
||||
if (empty($siteIdentifier)) {
|
||||
throw new \RuntimeException('Not site identifier given', 1521565182);
|
||||
}
|
||||
// Verify site does exist, method throws if not
|
||||
GeneralUtility::makeInstance(SiteConfiguration::class, Environment::getConfigPath() . '/sites')->delete($siteIdentifier);
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$overviewRoute = $uriBuilder->buildUriFromRoute('site_configuration', ['action' => 'overview']);
|
||||
return new RedirectResponse($overviewRoute);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the Fluid View.
|
||||
*
|
||||
* @param string $templateName
|
||||
*/
|
||||
protected function initializeView(string $templateName): void
|
||||
{
|
||||
$this->view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$this->view->setTemplate($templateName);
|
||||
$this->view->setTemplateRootPaths(['EXT:backend/Resources/Private/Templates/SiteConfiguration']);
|
||||
$this->view->setPartialRootPaths(['EXT:backend/Resources/Private/Partials']);
|
||||
$this->view->setLayoutRootPaths(['EXT:backend/Resources/Private/Layouts']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create document header buttons of "edit" action
|
||||
*/
|
||||
protected function configureEditViewDocHeader(): void
|
||||
{
|
||||
$iconFactory = $this->moduleTemplate->getIconFactory();
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
$lang = $this->getLanguageService();
|
||||
$closeButton = $buttonBar->makeLinkButton()
|
||||
->setHref('#')
|
||||
->setClasses('t3js-editform-close')
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
|
||||
->setShowLabelText(true)
|
||||
->setIcon($iconFactory->getIcon('actions-close', Icon::SIZE_SMALL));
|
||||
$saveButton = $buttonBar->makeInputButton()
|
||||
->setTitle($lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
|
||||
->setName('_savedok')
|
||||
->setValue('1')
|
||||
->setShowLabelText(true)
|
||||
->setForm('siteConfigurationController')
|
||||
->setIcon($iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($closeButton);
|
||||
$buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create document header buttons of "overview" action
|
||||
*/
|
||||
protected function configureOverViewDocHeader(): void
|
||||
{
|
||||
$iconFactory = $this->moduleTemplate->getIconFactory();
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
$reloadButton = $buttonBar->makeLinkButton()
|
||||
->setHref(GeneralUtility::getIndpEnv('REQUEST_URI'))
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
|
||||
->setIcon($iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL));
|
||||
$buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
|
||||
if ($this->getBackendUser()->mayMakeShortcut()) {
|
||||
$getVars = ['id', 'route'];
|
||||
$shortcutButton = $buttonBar->makeShortcutButton()
|
||||
->setModuleName('site_configuration')
|
||||
->setGetVariables($getVars);
|
||||
$buttonBar->addButton($shortcutButton, ButtonBar::BUTTON_POSITION_RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of pages that have 'is_siteroot' set
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getAllSitePages(): array
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
|
||||
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
|
||||
$queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class, 0, false));
|
||||
$statement = $queryBuilder
|
||||
->select('*')
|
||||
->from('pages')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq('sys_language_uid', 0),
|
||||
$queryBuilder->expr()->orX(
|
||||
$queryBuilder->expr()->andX(
|
||||
$queryBuilder->expr()->eq('pid', 0),
|
||||
$queryBuilder->expr()->neq('doktype', PageRepository::DOKTYPE_SYSFOLDER)
|
||||
),
|
||||
$queryBuilder->expr()->eq('is_siteroot', 1)
|
||||
)
|
||||
)
|
||||
->orderBy('pid')
|
||||
->addOrderBy('sorting')
|
||||
->execute();
|
||||
|
||||
$pages = [];
|
||||
while ($row = $statement->fetch()) {
|
||||
$row['rootline'] = BackendUtility::BEgetRootLine((int)$row['uid']);
|
||||
array_pop($row['rootline']);
|
||||
$row['rootline'] = array_reverse($row['rootline']);
|
||||
$i = 0;
|
||||
foreach ($row['rootline'] as &$record) {
|
||||
$record['margin'] = $i++ * 20;
|
||||
}
|
||||
$pages[(int)$row['uid']] = $row;
|
||||
}
|
||||
return $pages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService(): LanguageService
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Configuration\SiteTcaConfiguration;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\SiteConfigurationDataGroup;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Site configuration FormEngine controller class. Receives inline "edit" and "new"
|
||||
* commands to expand / create site configuration inline records
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SiteInlineAjaxController extends AbstractFormEngineAjaxController
|
||||
{
|
||||
/**
|
||||
* Default constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Bring site TCA into global scope.
|
||||
// @todo: We might be able to get rid of that later
|
||||
$GLOBALS['TCA'] = array_merge($GLOBALS['TCA'], GeneralUtility::makeInstance(SiteTcaConfiguration::class)->getTca());
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline "create" new child of site configuration child records
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function newInlineChildAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
|
||||
$parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
|
||||
$domObjectId = $ajaxArguments[0];
|
||||
$inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
|
||||
$childChildUid = null;
|
||||
if (isset($ajaxArguments[1]) && MathUtility::canBeInterpretedAsInteger($ajaxArguments[1])) {
|
||||
$childChildUid = (int)$ajaxArguments[1];
|
||||
}
|
||||
// Parse the DOM identifier, add the levels to the structure stack
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
|
||||
$inlineStackProcessor->injectAjaxConfiguration($parentConfig);
|
||||
$inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
|
||||
// Parent, this table embeds the child table
|
||||
$parent = $inlineStackProcessor->getStructureLevel(-1);
|
||||
// Child, a record from this table should be rendered
|
||||
$child = $inlineStackProcessor->getUnstableStructure();
|
||||
if (MathUtility::canBeInterpretedAsInteger($child['uid'])) {
|
||||
// If uid comes in, it is the id of the record neighbor record "create after"
|
||||
$childVanillaUid = -1 * abs((int)$child['uid']);
|
||||
} else {
|
||||
// Else inline first Pid is the storage pid of new inline records
|
||||
$childVanillaUid = (int)$inlineFirstPid;
|
||||
}
|
||||
$childTableName = $parentConfig['foreign_table'];
|
||||
|
||||
$defaultDatabaseRow = [];
|
||||
if ($childTableName === 'site_language') {
|
||||
// Feed new site_language row with data from sys_language record if possible
|
||||
if ($childChildUid > 0) {
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('sys_language');
|
||||
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
|
||||
$row = $queryBuilder->select('*')->from('sys_language')
|
||||
->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($childChildUid, \PDO::PARAM_INT)))
|
||||
->execute()->fetch();
|
||||
if (empty($row)) {
|
||||
throw new \RuntimeException('Referenced sys_language row not found', 1521783937);
|
||||
}
|
||||
if (!empty($row['language_isocode'])) {
|
||||
$defaultDatabaseRow['iso-639-1'] = $row['language_isocode'];
|
||||
$defaultDatabaseRow['base'] = '/' . $row['language_isocode'] . '/';
|
||||
}
|
||||
if (!empty($row['flag']) && $row['flag'] === 'multiple') {
|
||||
$defaultDatabaseRow['flag'] = 'global';
|
||||
} elseif (!empty($row)) {
|
||||
$defaultDatabaseRow['flag'] = $row['flag'];
|
||||
}
|
||||
if (!empty($row['title'])) {
|
||||
$defaultDatabaseRow['title'] = $row['title'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'command' => 'new',
|
||||
'tableName' => $childTableName,
|
||||
'vanillaUid' => $childVanillaUid,
|
||||
'databaseRow' => $defaultDatabaseRow,
|
||||
'isInlineChild' => true,
|
||||
'inlineStructure' => $inlineStackProcessor->getStructure(),
|
||||
'inlineFirstPid' => $inlineFirstPid,
|
||||
'inlineParentUid' => $parent['uid'],
|
||||
'inlineParentTableName' => $parent['table'],
|
||||
'inlineParentFieldName' => $parent['field'],
|
||||
'inlineParentConfig' => $parentConfig,
|
||||
'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
|
||||
'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
|
||||
'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
|
||||
];
|
||||
if ($childChildUid) {
|
||||
$formDataCompilerInput['inlineChildChildUid'] = $childChildUid;
|
||||
}
|
||||
$childData = $formDataCompiler->compile($formDataCompilerInput);
|
||||
|
||||
if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
|
||||
throw new \RuntimeException('useCombination not implemented in sites module', 1522493094);
|
||||
}
|
||||
|
||||
$childData['inlineParentUid'] = (int)$parent['uid'];
|
||||
$childData['renderType'] = 'inlineRecordContainer';
|
||||
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
|
||||
$childResult = $nodeFactory->create($childData)->render();
|
||||
|
||||
$jsonArray = [
|
||||
'data' => '',
|
||||
'stylesheetFiles' => [],
|
||||
'scriptCall' => [],
|
||||
];
|
||||
|
||||
// The HTML-object-id's prefix of the dynamically created record
|
||||
$objectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
|
||||
$objectPrefix = $objectName . '-' . $child['table'];
|
||||
$objectId = $objectPrefix . '-' . $childData['databaseRow']['uid'];
|
||||
$expandSingle = $parentConfig['appearance']['expandSingle'];
|
||||
if (!$child['uid']) {
|
||||
$jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'bottom\',' . GeneralUtility::quoteJSvalue($objectName . '_records') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
|
||||
$jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',null,' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
|
||||
} else {
|
||||
$jsonArray['scriptCall'][] = 'inline.domAddNewRecord(\'after\',' . GeneralUtility::quoteJSvalue($domObjectId . '_div') . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',json.data);';
|
||||
$jsonArray['scriptCall'][] = 'inline.memorizeAddRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ',' . GeneralUtility::quoteJSvalue($child['uid']) . ',' . GeneralUtility::quoteJSvalue($childChildUid) . ');';
|
||||
}
|
||||
$jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
|
||||
if ($parentConfig['appearance']['useSortable']) {
|
||||
$inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
|
||||
$jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
|
||||
}
|
||||
if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
|
||||
$jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . GeneralUtility::quoteJSvalue($childData['databaseRow']['uid']) . ');';
|
||||
}
|
||||
// Fade out and fade in the new record in the browser view to catch the user's eye
|
||||
$jsonArray['scriptCall'][] = 'inline.fadeOutFadeIn(' . GeneralUtility::quoteJSvalue($objectId . '_div') . ');';
|
||||
|
||||
return new JsonResponse($jsonArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the details of site configuration child records.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function openInlineChildAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$ajaxArguments = $request->getParsedBody()['ajax'] ?? $request->getQueryParams()['ajax'];
|
||||
|
||||
$domObjectId = $ajaxArguments[0];
|
||||
$inlineFirstPid = $this->getInlineFirstPidFromDomObjectId($domObjectId);
|
||||
$parentConfig = $this->extractSignedParentConfigFromRequest((string)$ajaxArguments['context']);
|
||||
|
||||
// Parse the DOM identifier, add the levels to the structure stack
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByParsingDomObjectIdString($domObjectId);
|
||||
$inlineStackProcessor->injectAjaxConfiguration($parentConfig);
|
||||
|
||||
// Parent, this table embeds the child table
|
||||
$parent = $inlineStackProcessor->getStructureLevel(-1);
|
||||
$parentFieldName = $parent['field'];
|
||||
|
||||
// Set flag in config so that only the fields are rendered
|
||||
// @todo: Solve differently / rename / whatever
|
||||
$parentConfig['renderFieldsOnly'] = true;
|
||||
|
||||
$parentData = [
|
||||
'processedTca' => [
|
||||
'columns' => [
|
||||
$parentFieldName => [
|
||||
'config' => $parentConfig,
|
||||
],
|
||||
],
|
||||
],
|
||||
'tableName' => $parent['table'],
|
||||
'inlineFirstPid' => $inlineFirstPid,
|
||||
// Hand over given original return url to compile stack. Needed if inline children compile links to
|
||||
// another view (eg. edit metadata in a nested inline situation like news with inline content element image),
|
||||
// so the back link is still the link from the original request. See issue #82525. This is additionally
|
||||
// given down in TcaInline data provider to compiled children data.
|
||||
'returnUrl' => $parentConfig['originalReturnUrl'],
|
||||
];
|
||||
|
||||
// Child, a record from this table should be rendered
|
||||
$child = $inlineStackProcessor->getUnstableStructure();
|
||||
|
||||
$childData = $this->compileChild($parentData, $parentFieldName, (int)$child['uid'], $inlineStackProcessor->getStructure());
|
||||
|
||||
$childData['inlineParentUid'] = (int)$parent['uid'];
|
||||
$childData['renderType'] = 'inlineRecordContainer';
|
||||
$nodeFactory = GeneralUtility::makeInstance(NodeFactory::class);
|
||||
$childResult = $nodeFactory->create($childData)->render();
|
||||
|
||||
$jsonArray = [
|
||||
'data' => '',
|
||||
'stylesheetFiles' => [],
|
||||
'scriptCall' => [],
|
||||
];
|
||||
|
||||
// The HTML-object-id's prefix of the dynamically created record
|
||||
$objectPrefix = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid) . '-' . $child['table'];
|
||||
$objectId = $objectPrefix . '-' . (int)$child['uid'];
|
||||
$expandSingle = $parentConfig['appearance']['expandSingle'];
|
||||
$jsonArray['scriptCall'][] = 'inline.domAddRecordDetails(' . GeneralUtility::quoteJSvalue($domObjectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',' . ($expandSingle ? '1' : '0') . ',json.data);';
|
||||
if ($parentConfig['foreign_unique']) {
|
||||
$jsonArray['scriptCall'][] = 'inline.removeUsed(' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
|
||||
}
|
||||
$jsonArray = $this->mergeChildResultIntoJsonResult($jsonArray, $childResult);
|
||||
if ($parentConfig['appearance']['useSortable']) {
|
||||
$inlineObjectName = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($inlineFirstPid);
|
||||
$jsonArray['scriptCall'][] = 'inline.createDragAndDropSorting(' . GeneralUtility::quoteJSvalue($inlineObjectName . '_records') . ');';
|
||||
}
|
||||
if (!$parentConfig['appearance']['collapseAll'] && $expandSingle) {
|
||||
$jsonArray['scriptCall'][] = 'inline.collapseAllRecords(' . GeneralUtility::quoteJSvalue($objectId) . ',' . GeneralUtility::quoteJSvalue($objectPrefix) . ',\'' . (int)$child['uid'] . '\');';
|
||||
}
|
||||
|
||||
return new JsonResponse($jsonArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile a full child record
|
||||
*
|
||||
* @param array $parentData Result array of parent
|
||||
* @param string $parentFieldName Name of parent field
|
||||
* @param int $childUid Uid of child to compile
|
||||
* @param array $inlineStructure Current inline structure
|
||||
* @return array Full result array
|
||||
* @throws \RuntimeException
|
||||
*
|
||||
* @todo: This clones methods compileChild from TcaInline Provider. Find a better abstraction
|
||||
* @todo: to also encapsulate the more complex scenarios with combination child and friends.
|
||||
*/
|
||||
protected function compileChild(array $parentData, string $parentFieldName, int $childUid, array $inlineStructure): array
|
||||
{
|
||||
$parentConfig = $parentData['processedTca']['columns'][$parentFieldName]['config'];
|
||||
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByGivenStructure($inlineStructure);
|
||||
$inlineTopMostParent = $inlineStackProcessor->getStructureLevel(0);
|
||||
|
||||
// @todo: do not use stack processor here ...
|
||||
$child = $inlineStackProcessor->getUnstableStructure();
|
||||
$childTableName = $child['table'];
|
||||
|
||||
$formDataGroup = GeneralUtility::makeInstance(SiteConfigurationDataGroup::class);
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$formDataCompilerInput = [
|
||||
'command' => 'edit',
|
||||
'tableName' => $childTableName,
|
||||
'vanillaUid' => (int)$childUid,
|
||||
'returnUrl' => $parentData['returnUrl'],
|
||||
'isInlineChild' => true,
|
||||
'inlineStructure' => $inlineStructure,
|
||||
'inlineFirstPid' => $parentData['inlineFirstPid'],
|
||||
'inlineParentConfig' => $parentConfig,
|
||||
'isInlineAjaxOpeningContext' => true,
|
||||
|
||||
// values of the current parent element
|
||||
// it is always a string either an id or new...
|
||||
'inlineParentUid' => $parentData['databaseRow']['uid'],
|
||||
'inlineParentTableName' => $parentData['tableName'],
|
||||
'inlineParentFieldName' => $parentFieldName,
|
||||
|
||||
// values of the top most parent element set on first level and not overridden on following levels
|
||||
'inlineTopMostParentUid' => $inlineTopMostParent['uid'],
|
||||
'inlineTopMostParentTableName' => $inlineTopMostParent['table'],
|
||||
'inlineTopMostParentFieldName' => $inlineTopMostParent['field'],
|
||||
];
|
||||
if ($parentConfig['foreign_selector'] && $parentConfig['appearance']['useCombination']) {
|
||||
throw new \RuntimeException('useCombination not implemented in sites module', 1522493095);
|
||||
}
|
||||
return $formDataCompiler->compile($formDataCompilerInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge stuff from child array into json array.
|
||||
* This method is needed since ajax handling methods currently need to put scriptCalls before and after child code.
|
||||
*
|
||||
* @param array $jsonResult Given json result
|
||||
* @param array $childResult Given child result
|
||||
* @return array Merged json array
|
||||
*/
|
||||
protected function mergeChildResultIntoJsonResult(array $jsonResult, array $childResult): array
|
||||
{
|
||||
$jsonResult['data'] .= $childResult['html'];
|
||||
$jsonResult['stylesheetFiles'] = [];
|
||||
foreach ($childResult['stylesheetFiles'] as $stylesheetFile) {
|
||||
$jsonResult['stylesheetFiles'][] = $this->getRelativePathToStylesheetFile($stylesheetFile);
|
||||
}
|
||||
if (!empty($childResult['inlineData'])) {
|
||||
$jsonResult['scriptCall'][] = 'inline.addToDataArray(' . json_encode($childResult['inlineData']) . ');';
|
||||
}
|
||||
if (!empty($childResult['additionalJavaScriptSubmit'])) {
|
||||
$additionalJavaScriptSubmit = implode('', $childResult['additionalJavaScriptSubmit']);
|
||||
$additionalJavaScriptSubmit = str_replace([CR, LF], '', $additionalJavaScriptSubmit);
|
||||
$jsonResult['scriptCall'][] = 'TBE_EDITOR.addActionChecks("submit", "' . addslashes($additionalJavaScriptSubmit) . '");';
|
||||
}
|
||||
foreach ($childResult['additionalJavaScriptPost'] as $singleAdditionalJavaScriptPost) {
|
||||
$jsonResult['scriptCall'][] = $singleAdditionalJavaScriptPost;
|
||||
}
|
||||
if (!empty($childResult['additionalInlineLanguageLabelFiles'])) {
|
||||
$labels = [];
|
||||
foreach ($childResult['additionalInlineLanguageLabelFiles'] as $additionalInlineLanguageLabelFile) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule(
|
||||
$labels,
|
||||
$this->getLabelsFromLocalizationFile($additionalInlineLanguageLabelFile)
|
||||
);
|
||||
}
|
||||
$javaScriptCode = [];
|
||||
$javaScriptCode[] = 'if (typeof TYPO3 === \'undefined\' || typeof TYPO3.lang === \'undefined\') {';
|
||||
$javaScriptCode[] = ' TYPO3.lang = {}';
|
||||
$javaScriptCode[] = '}';
|
||||
$javaScriptCode[] = 'var additionalInlineLanguageLabels = ' . json_encode($labels) . ';';
|
||||
$javaScriptCode[] = 'for (var attributeName in additionalInlineLanguageLabels) {';
|
||||
$javaScriptCode[] = ' if (typeof TYPO3.lang[attributeName] === \'undefined\') {';
|
||||
$javaScriptCode[] = ' TYPO3.lang[attributeName] = additionalInlineLanguageLabels[attributeName]';
|
||||
$javaScriptCode[] = ' }';
|
||||
$javaScriptCode[] = '}';
|
||||
|
||||
$jsonResult['scriptCall'][] = implode(LF, $javaScriptCode);
|
||||
}
|
||||
$requireJsModule = $this->createExecutableStringRepresentationOfRegisteredRequireJsModules($childResult);
|
||||
$jsonResult['scriptCall'] = array_merge($requireJsModule, $jsonResult['scriptCall']);
|
||||
|
||||
return $jsonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline ajax helper method.
|
||||
*
|
||||
* Validates the config that is transferred over the wire to provide the
|
||||
* correct TCA config for the parent table
|
||||
*
|
||||
* @param string $contextString
|
||||
* @throws \RuntimeException
|
||||
* @return array
|
||||
*/
|
||||
protected function extractSignedParentConfigFromRequest(string $contextString): array
|
||||
{
|
||||
if ($contextString === '') {
|
||||
throw new \RuntimeException('Empty context string given', 1522771624);
|
||||
}
|
||||
$context = json_decode($contextString, true);
|
||||
if (empty($context['config'])) {
|
||||
throw new \RuntimeException('Empty context config section given', 1522771632);
|
||||
}
|
||||
if (!hash_equals(GeneralUtility::hmac((string)$context['config'], 'InlineContext'), (string)$context['hmac'])) {
|
||||
throw new \RuntimeException('Hash does not validate', 1522771640);
|
||||
}
|
||||
return json_decode($context['config'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get inlineFirstPid from a given objectId string
|
||||
*
|
||||
* @param string $domObjectId The id attribute of an element
|
||||
* @return int|null Pid or null
|
||||
*/
|
||||
protected function getInlineFirstPidFromDomObjectId(string $domObjectId): ?int
|
||||
{
|
||||
// Substitute FlexForm addition and make parsing a bit easier
|
||||
$domObjectId = str_replace('---', ':', $domObjectId);
|
||||
// The starting pattern of an object identifier (e.g. "data-<firstPidValue>-<anything>)
|
||||
$pattern = '/^data' . '-' . '(.+?)' . '-' . '(.+)$/';
|
||||
if (preg_match($pattern, $domObjectId, $match)) {
|
||||
return (int)$match[1];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Backend\Backend\ToolbarItems\SystemInformationToolbarItem;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Controller for system information processing
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SystemInformationController
|
||||
{
|
||||
/**
|
||||
* @var SystemInformationToolbarItem
|
||||
*/
|
||||
protected $toolbarItem;
|
||||
|
||||
/**
|
||||
* Set up dependencies
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->toolbarItem = GeneralUtility::makeInstance(SystemInformationToolbarItem::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the menu for AJAX calls
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function renderMenuAction(): ResponseInterface
|
||||
{
|
||||
return new HtmlResponse($this->toolbarItem->getDropDown());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Configuration\BackendUserConfiguration;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A wrapper class to call BE_USER->uc
|
||||
* used for AJAX and TYPO3.Storage JS object
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class UserSettingsController
|
||||
{
|
||||
/**
|
||||
* @var BackendUserConfiguration
|
||||
*/
|
||||
protected $backendUserConfiguration;
|
||||
|
||||
/**
|
||||
* Initializes the backendUserConfiguration
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->backendUserConfiguration = GeneralUtility::makeInstance(BackendUserConfiguration::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes all AJAX calls and returns a JSON for the data
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function processAjaxRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// do the regular / main logic, depending on the action parameter
|
||||
$action = $request->getParsedBody()['action'] ?? $request->getQueryParams()['action'] ?? '';
|
||||
$key = $request->getParsedBody()['key'] ?? $request->getQueryParams()['key'] ?? '';
|
||||
$value = $request->getParsedBody()['value'] ?? $request->getQueryParams()['value'] ?? '';
|
||||
$data = $this->processRequest($action, $key, $value);
|
||||
|
||||
return (new JsonResponse())->setPayload($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $key
|
||||
* @param string $value
|
||||
* @return mixed
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function process($action, $key = '', $value = '')
|
||||
{
|
||||
trigger_error('UserSettingsController->process() will be replaced by protected method processRequest() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
return $this->processRequest($action, $key, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process data
|
||||
*
|
||||
* @param string $action
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
protected function processRequest(string $action, string $key = '', $value = '')
|
||||
{
|
||||
switch ($action) {
|
||||
case 'get':
|
||||
$content = $this->backendUserConfiguration->get($key);
|
||||
break;
|
||||
case 'getAll':
|
||||
$content = $this->backendUserConfiguration->getAll();
|
||||
break;
|
||||
case 'set':
|
||||
$this->backendUserConfiguration->set($key, $value);
|
||||
$content = $this->backendUserConfiguration->getAll();
|
||||
break;
|
||||
case 'addToList':
|
||||
$this->backendUserConfiguration->addToList($key, $value);
|
||||
$content = $this->backendUserConfiguration->getAll();
|
||||
break;
|
||||
case 'removeFromList':
|
||||
$this->backendUserConfiguration->removeFromList($key, $value);
|
||||
$content = $this->backendUserConfiguration->getAll();
|
||||
break;
|
||||
case 'unset':
|
||||
$this->backendUserConfiguration->unsetOption($key);
|
||||
$content = $this->backendUserConfiguration->getAll();
|
||||
break;
|
||||
case 'clear':
|
||||
$this->backendUserConfiguration->clear();
|
||||
$content = ['result' => true];
|
||||
break;
|
||||
default:
|
||||
$content = ['result' => false];
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Template\DocumentTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
|
||||
/**
|
||||
* Class AbstractWizardController
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class AbstractWizardController
|
||||
{
|
||||
/**
|
||||
* Checks access for element
|
||||
*
|
||||
* @param string $table Table name
|
||||
* @param int $uid Record uid
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkEditAccess($table, $uid)
|
||||
{
|
||||
$record = BackendUtility::getRecord($table, $uid);
|
||||
BackendUtility::fixVersioningPid($table, $record);
|
||||
if (is_array($record)) {
|
||||
// If pages:
|
||||
if ($table === 'pages') {
|
||||
$calculatedPermissions = $this->getBackendUserAuthentication()->calcPerms($record);
|
||||
$hasAccess = $calculatedPermissions & Permission::PAGE_EDIT;
|
||||
} else {
|
||||
// Fetching pid-record first.
|
||||
$calculatedPermissions = $this->getBackendUserAuthentication()->calcPerms(
|
||||
BackendUtility::getRecord('pages', $record['pid'])
|
||||
);
|
||||
$hasAccess = $calculatedPermissions & Permission::CONTENT_EDIT;
|
||||
}
|
||||
// Check internals regarding access:
|
||||
if ($hasAccess) {
|
||||
$hasAccess = $this->getBackendUserAuthentication()->recordEditAccessInternals($table, $record);
|
||||
}
|
||||
} else {
|
||||
$hasAccess = false;
|
||||
}
|
||||
return (bool)$hasAccess;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of BackendUserAuthentication
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of LanguageService
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an instance of DocumentTemplate
|
||||
*
|
||||
* @return DocumentTemplate
|
||||
*/
|
||||
protected function getDocumentTemplate()
|
||||
{
|
||||
return $GLOBALS['TBE_TEMPLATE'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Form\FormDataCompiler;
|
||||
use TYPO3\CMS\Backend\Form\FormDataGroup\TcaDatabaseRecord;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Script Class for adding new items to a group/select field. Performs proper redirection as needed.
|
||||
* Script is typically called after new child record was added and then adds the new child to select value of parent.
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class AddController extends AbstractWizardController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'content' => 'Using $content of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'processDataFlag' => 'Using $processDataFlag of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'pid' => 'Using $pid of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'table' => 'Using $table of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'id' => 'Using $id of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'P' => 'Using $P of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'returnEditConf' => 'Using $returnEditConf of class AddController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
/**
|
||||
* Content accumulation for the module.
|
||||
*
|
||||
* @var string
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, unused
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* If set, the DataHandler class is loaded and used to add the returning ID to the parent record.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $processDataFlag = 0;
|
||||
|
||||
/**
|
||||
* Create new record -pid (pos/neg). If blank, return immediately
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $pid;
|
||||
|
||||
/**
|
||||
* The parent table we are working on.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* Loaded with the created id of a record FormEngine returns ...
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Wizard parameters, coming from TCEforms linking to the wizard.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $P;
|
||||
|
||||
/**
|
||||
* Information coming back from the FormEngine script, telling what the table/id was of the newly created record.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $returnEditConf;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
return $this->processRequest($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('AddController->main() will be replaced by protected method processRequest() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
|
||||
$response = $this->processRequest($GLOBALS['TYPO3_REQUEST']);
|
||||
HttpUtility::redirect($response->getHeaders()['location'][0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of the class.
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
// Init GPvars:
|
||||
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? [];
|
||||
$this->returnEditConf = $parsedBody['returnEditConf'] ?? $queryParams['returnEditConf'] ?? null;
|
||||
// Get this record
|
||||
$record = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
|
||||
// Set table:
|
||||
$this->table = $this->P['params']['table'];
|
||||
// Get TSconfig for it.
|
||||
$TSconfig = BackendUtility::getTCEFORM_TSconfig(
|
||||
$this->P['table'],
|
||||
is_array($record) ? $record : ['pid' => $this->P['pid']]
|
||||
);
|
||||
// Set [params][pid]
|
||||
if (strpos($this->P['params']['pid'], '###') === 0 && substr($this->P['params']['pid'], -3) === '###') {
|
||||
$keyword = substr($this->P['params']['pid'], 3, -3);
|
||||
if (strpos($keyword, 'PAGE_TSCONFIG_') === 0) {
|
||||
$this->pid = (int)$TSconfig[$this->P['field']][$keyword];
|
||||
} else {
|
||||
$this->pid = (int)$TSconfig['_' . $keyword];
|
||||
}
|
||||
} else {
|
||||
$this->pid = (int)$this->P['params']['pid'];
|
||||
}
|
||||
// Return if new record as parent (not possibly/allowed)
|
||||
if ($this->pid === '') {
|
||||
// HTTP Redirect is performed by processRequest()
|
||||
return;
|
||||
}
|
||||
// Else proceed:
|
||||
// If a new id has returned from a newly created record...
|
||||
if ($this->returnEditConf) {
|
||||
$editConfiguration = json_decode($this->returnEditConf, true);
|
||||
if (is_array($editConfiguration[$this->table]) && MathUtility::canBeInterpretedAsInteger($this->P['uid'])) {
|
||||
// Getting id and cmd from returning editConf array.
|
||||
reset($editConfiguration[$this->table]);
|
||||
$this->id = (int)key($editConfiguration[$this->table]);
|
||||
$cmd = current($editConfiguration[$this->table]);
|
||||
// ... and if everything seems OK we will register some classes for inclusion and instruct the object
|
||||
// to perform processing later.
|
||||
if ($this->P['params']['setValue']
|
||||
&& $cmd === 'edit'
|
||||
&& $this->id
|
||||
&& $this->P['table']
|
||||
&& $this->P['field'] && $this->P['uid']
|
||||
) {
|
||||
$liveRecord = BackendUtility::getLiveVersionOfRecord($this->table, $this->id, 'uid');
|
||||
if ($liveRecord) {
|
||||
$this->id = $liveRecord['uid'];
|
||||
}
|
||||
$this->processDataFlag = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function processRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
// Return if new record as parent (not possibly/allowed)
|
||||
if ($this->pid === '') {
|
||||
return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
|
||||
}
|
||||
|
||||
if ($this->returnEditConf) {
|
||||
if ($this->processDataFlag) {
|
||||
// Because OnTheFly can't handle MM relations with intermediate tables we use TcaDatabaseRecord here
|
||||
// Otherwise already stored relations are overwritten with the new entry
|
||||
/** @var TcaDatabaseRecord $formDataGroup */
|
||||
$formDataGroup = GeneralUtility::makeInstance(TcaDatabaseRecord::class);
|
||||
/** @var FormDataCompiler $formDataCompiler */
|
||||
$formDataCompiler = GeneralUtility::makeInstance(FormDataCompiler::class, $formDataGroup);
|
||||
$input = [
|
||||
'tableName' => $this->P['table'],
|
||||
'vanillaUid' => (int)$this->P['uid'],
|
||||
'command' => 'edit',
|
||||
];
|
||||
$result = $formDataCompiler->compile($input);
|
||||
$currentParentRow = $result['databaseRow'];
|
||||
|
||||
// If that record was found (should absolutely be...), then init DataHandler and set, prepend or append
|
||||
// the record
|
||||
if (is_array($currentParentRow)) {
|
||||
/** @var DataHandler $dataHandler */
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
$data = [];
|
||||
$recordId = $this->table . '_' . $this->id;
|
||||
// Setting the new field data:
|
||||
// If the field is a flexForm field, work with the XML structure instead:
|
||||
if ($this->P['flexFormPath']) {
|
||||
// Current value of flexForm path:
|
||||
$currentFlexFormData = $currentParentRow[$this->P['field']];
|
||||
/** @var FlexFormTools $flexFormTools */
|
||||
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
|
||||
$currentFlexFormValueByPath = $flexFormTools->getArrayValueByPath(
|
||||
$this->P['flexFormPath'],
|
||||
$currentFlexFormData
|
||||
);
|
||||
|
||||
// Compile currentFlexFormData to functional string
|
||||
$currentFlexFormValues = [];
|
||||
foreach ($currentFlexFormValueByPath as $value) {
|
||||
if (is_array($value)) {
|
||||
// group fields are always resolved to array
|
||||
$currentFlexFormValues[] = $value['table'] . '_' . $value['uid'];
|
||||
} else {
|
||||
// but select fields may be uids only
|
||||
$currentFlexFormValues[] = $value;
|
||||
}
|
||||
}
|
||||
$currentFlexFormValue = implode(',', $currentFlexFormValues);
|
||||
|
||||
$insertValue = '';
|
||||
switch ((string)$this->P['params']['setValue']) {
|
||||
case 'set':
|
||||
$insertValue = $recordId;
|
||||
break;
|
||||
case 'append':
|
||||
$insertValue = $currentFlexFormValue . ',' . $recordId;
|
||||
break;
|
||||
case 'prepend':
|
||||
$insertValue = $recordId . ',' . $currentFlexFormValue;
|
||||
break;
|
||||
}
|
||||
$insertValue = implode(',', GeneralUtility::trimExplode(',', $insertValue, true));
|
||||
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = [];
|
||||
$flexFormTools->setArrayValueByPath(
|
||||
$this->P['flexFormPath'],
|
||||
$data[$this->P['table']][$this->P['uid']][$this->P['field']],
|
||||
$insertValue
|
||||
);
|
||||
} else {
|
||||
$currentValue = $currentParentRow[$this->P['field']];
|
||||
|
||||
// Normalize CSV values
|
||||
if (!is_array($currentValue)) {
|
||||
$currentValue = GeneralUtility::trimExplode(',', $currentValue, true);
|
||||
}
|
||||
|
||||
// Normalize all items to "<table>_<uid>" format
|
||||
$currentValue = array_map(function ($item) {
|
||||
// Handle per-item table for "group" elements
|
||||
if (is_array($item)) {
|
||||
$item = $item['table'] . '_' . $item['uid'];
|
||||
} else {
|
||||
$item = $this->table . '_' . $item;
|
||||
}
|
||||
|
||||
return $item;
|
||||
}, $currentValue);
|
||||
|
||||
switch ((string)$this->P['params']['setValue']) {
|
||||
case 'set':
|
||||
$currentValue = [$recordId];
|
||||
break;
|
||||
case 'append':
|
||||
$currentValue[] = $recordId;
|
||||
break;
|
||||
case 'prepend':
|
||||
array_unshift($currentValue, $recordId);
|
||||
break;
|
||||
}
|
||||
|
||||
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = implode(',', $currentValue);
|
||||
}
|
||||
// Submit the data:
|
||||
$dataHandler->start($data, []);
|
||||
$dataHandler->process_datamap();
|
||||
}
|
||||
}
|
||||
// Return to the parent FormEngine record editing session:
|
||||
return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
|
||||
}
|
||||
|
||||
// Redirecting to FormEngine with instructions to create a new record
|
||||
// AND when closing to return back with information about that records ID etc.
|
||||
/** @var \TYPO3\CMS\Core\Http\NormalizedParams */
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
/** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$redirectUrl = (string)$uriBuilder->buildUriFromRoute('record_edit', [
|
||||
'returnEditConf' => 1,
|
||||
'edit[' . $this->P['params']['table'] . '][' . $this->pid . ']' => 'new',
|
||||
'returnUrl' => $normalizedParams->getRequestUri(),
|
||||
]);
|
||||
|
||||
return new RedirectResponse($redirectUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\Database\RelationHandler;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Script Class for redirecting a backend user to the editing form when an "Edit wizard" link was clicked in FormEngine somewhere
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class EditController extends AbstractWizardController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'P' => 'Using $P of class EditController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'doClose' => 'Using $doClose of class EditController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Wizard parameters, coming from FormEngine linking to the wizard.
|
||||
*
|
||||
* Contains the following parts:
|
||||
* - table
|
||||
* - field
|
||||
* - formName
|
||||
* - hmac
|
||||
* - fieldChangeFunc
|
||||
* - fieldChangeFuncHash
|
||||
* - currentValue
|
||||
* - currentSelectedValues
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $P;
|
||||
|
||||
/**
|
||||
* Boolean; if set, the window will be closed by JavaScript
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $doClose;
|
||||
|
||||
/**
|
||||
* A little JavaScript to close the open window.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $closeWindow = '<script language="javascript" type="text/javascript">close();</script>';
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
|
||||
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of the script
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request)
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? [];
|
||||
|
||||
// Used for the return URL to FormEngine so that we can close the window.
|
||||
$this->doClose = $parsedBody['doClose'] ?? $queryParams['doClose'] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$content = $this->processRequest($request);
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
* Makes a header-location redirect to an edit form IF POSSIBLE from the passed data - otherwise the window will
|
||||
* just close.
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
* @return string
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('EditController->main() will be set to protected in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
|
||||
$response = $this->processRequest($request);
|
||||
|
||||
if ($response instanceof RedirectResponse) {
|
||||
HttpUtility::redirect($response->getHeaders()['location'][0]);
|
||||
} else {
|
||||
return $response->getBody()->getContents();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process request function
|
||||
* Makes a header-location redirect to an edit form IF POSSIBLE from the passed data - otherwise the window will
|
||||
* just close.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
protected function processRequest(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($this->doClose) {
|
||||
return new HtmlResponse($this->closeWindow);
|
||||
}
|
||||
// Initialize:
|
||||
$table = $this->P['table'];
|
||||
$field = $this->P['field'];
|
||||
|
||||
if (empty($this->P['flexFormDataStructureIdentifier'])) {
|
||||
// If there is not flex data structure identifier, field config is found in globals
|
||||
$config = $GLOBALS['TCA'][$table]['columns'][$field]['config'];
|
||||
} else {
|
||||
// If there is a flex data structure identifier, parse that data structure and
|
||||
// fetch config defined by given flex path
|
||||
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
|
||||
$dataStructure = $flexFormTools->parseDataStructureByIdentifier($this->P['flexFormDataStructureIdentifier']);
|
||||
$config = $flexFormTools->getArrayValueByPath($this->P['flexFormDataStructurePath'], $dataStructure);
|
||||
if (!is_array($config)) {
|
||||
throw new \RuntimeException(
|
||||
'Something went wrong finding flex path ' . $this->P['flexFormDataStructurePath']
|
||||
. ' in data structure identified by ' . $this->P['flexFormDataStructureIdentifier'],
|
||||
1537356346
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
$urlParameters = [
|
||||
'returnUrl' => (string)$uriBuilder->buildUriFromRoute('wizard_edit', ['doClose' => 1])
|
||||
];
|
||||
|
||||
// Detecting the various allowed field type setups and acting accordingly.
|
||||
if (is_array($config)
|
||||
&& $config['type'] === 'select'
|
||||
&& !$config['MM']
|
||||
&& $config['maxitems'] <= 1
|
||||
&& MathUtility::canBeInterpretedAsInteger($this->P['currentValue'])
|
||||
&& $this->P['currentValue']
|
||||
&& $config['foreign_table']
|
||||
) {
|
||||
// SINGLE value
|
||||
$urlParameters['edit[' . $config['foreign_table'] . '][' . $this->P['currentValue'] . ']'] = 'edit';
|
||||
// Redirect to FormEngine
|
||||
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
|
||||
if (is_array($config)
|
||||
&& $this->P['currentSelectedValues']
|
||||
&& (
|
||||
$config['type'] === 'select'
|
||||
&& $config['foreign_table']
|
||||
|| $config['type'] === 'group'
|
||||
&& $config['internal_type'] === 'db'
|
||||
)
|
||||
) {
|
||||
// MULTIPLE VALUES:
|
||||
// Init settings:
|
||||
$allowedTables = $config['type'] === 'group' ? $config['allowed'] : $config['foreign_table'];
|
||||
$prependName = 1;
|
||||
// Selecting selected values into an array:
|
||||
$relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
|
||||
$relationHandler->start($this->P['currentSelectedValues'], $allowedTables);
|
||||
$value = $relationHandler->getValueArray($prependName);
|
||||
// Traverse that array and make parameters for FormEngine
|
||||
foreach ($value as $rec) {
|
||||
$recTableUidParts = GeneralUtility::revExplode('_', $rec, 2);
|
||||
$urlParameters['edit[' . $recTableUidParts[0] . '][' . $recTableUidParts[1] . ']'] = 'edit';
|
||||
}
|
||||
// Redirect to FormEngine
|
||||
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', $urlParameters);
|
||||
|
||||
return new RedirectResponse($url);
|
||||
}
|
||||
return new HtmlResponse($this->closeWindow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Wizard for rendering image manipulation view
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ImageManipulationController
|
||||
{
|
||||
/**
|
||||
* @var StandaloneView
|
||||
*/
|
||||
private $templateView;
|
||||
|
||||
/**
|
||||
* @param StandaloneView $templateView
|
||||
*/
|
||||
public function __construct(StandaloneView $templateView = null)
|
||||
{
|
||||
if (!$templateView) {
|
||||
$templateView = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$templateView->setLayoutRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Layouts/')]);
|
||||
$templateView->setPartialRootPaths([GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Partials/ImageManipulation/')]);
|
||||
$templateView->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ImageManipulation/ImageManipulationWizard.html'));
|
||||
}
|
||||
$this->templateView = $templateView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML for the wizard inside the modal
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface $response
|
||||
*/
|
||||
public function getWizardContent(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
if ($this->isSignatureValid($request)) {
|
||||
$parsedBody = json_decode($request->getParsedBody()['arguments'], true);
|
||||
$fileUid = $parsedBody['image'];
|
||||
$image = null;
|
||||
if (MathUtility::canBeInterpretedAsInteger($fileUid)) {
|
||||
try {
|
||||
$image = ResourceFactory::getInstance()->getFileObject($fileUid);
|
||||
} catch (FileDoesNotExistException $e) {
|
||||
}
|
||||
}
|
||||
$viewData = [
|
||||
'image' => $image,
|
||||
'cropVariants' => $parsedBody['cropVariants']
|
||||
];
|
||||
$content = $this->templateView->renderSection('Main', $viewData);
|
||||
return new HtmlResponse($content);
|
||||
}
|
||||
return new HtmlResponse('', 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if hmac signature is correct
|
||||
*
|
||||
* @param ServerRequestInterface $request the request with the POST parameters
|
||||
* @return bool
|
||||
*/
|
||||
protected function isSignatureValid(ServerRequestInterface $request): bool
|
||||
{
|
||||
$token = GeneralUtility::hmac($request->getParsedBody()['arguments'], 'ajax_wizard_image_manipulation');
|
||||
return hash_equals($token, $request->getParsedBody()['signature']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
|
||||
/**
|
||||
* Script Class for redirecting the user to the Web > List module if a wizard-link has been clicked in FormEngine
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class ListController extends AbstractWizardController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'pid' => 'Using $pid of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'P' => 'Using $P of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'table' => 'Using $table of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'id' => 'Using $id of class ListController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $pid;
|
||||
|
||||
/**
|
||||
* Wizard parameters, coming from FormEngine linking to the wizard.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $P;
|
||||
|
||||
/**
|
||||
* Table to show, if none, then all tables are listed in list module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* Page id to list.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Initialization of the class, setting GPvars.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
|
||||
|
||||
// @deprecated since TYPO3 v9, will be obsolete in TYPO3 v10.0
|
||||
$request = $GLOBALS['TYPO3_REQUEST'];
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
|
||||
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? null;
|
||||
$this->table = $parsedBody['table'] ?? $queryParams['table'] ?? null;
|
||||
$this->id = $parsedBody['id'] ?? $queryParams['id'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$this->processRequest($request);
|
||||
return new HtmlResponse('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function
|
||||
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main(): void
|
||||
{
|
||||
trigger_error('ListController->main() will be replaced by protected method processRequest() in TYPO3 v10.0. Do not call from other extension.', E_USER_DEPRECATED);
|
||||
$this->processRequest($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
/**
|
||||
* Main function
|
||||
* Will issue a location-header, redirecting either BACK or to a new FormEngine instance...
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function processRequest(ServerRequestInterface $request): void
|
||||
{
|
||||
// Get this record
|
||||
$origRow = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
|
||||
// Get TSconfig for it.
|
||||
$TSconfig = BackendUtility::getTCEFORM_TSconfig(
|
||||
$this->table,
|
||||
is_array($origRow) ? $origRow : ['pid' => $this->P['pid']]
|
||||
);
|
||||
// Set [params][pid]
|
||||
if (strpos($this->P['params']['pid'], '###') === 0 && substr($this->P['params']['pid'], -3) === '###') {
|
||||
$keyword = substr($this->P['params']['pid'], 3, -3);
|
||||
if (strpos($keyword, 'PAGE_TSCONFIG_') === 0) {
|
||||
$this->pid = (int)$TSconfig[$this->P['field']][$keyword];
|
||||
} else {
|
||||
$this->pid = (int)$TSconfig['_' . $keyword];
|
||||
}
|
||||
} else {
|
||||
$this->pid = (int)$this->P['params']['pid'];
|
||||
}
|
||||
// Make redirect:
|
||||
// If pid is blank OR if id is set, then return...
|
||||
if ((string)$this->id !== '') {
|
||||
$redirectUrl = GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']);
|
||||
} else {
|
||||
/** @var UriBuilder $uriBuilder */
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$requestUri = $normalizedParams->getRequestUri();
|
||||
// Otherwise, show the list:
|
||||
$urlParameters = [];
|
||||
$urlParameters['id'] = $this->pid;
|
||||
$urlParameters['table'] = $this->P['params']['table'];
|
||||
$urlParameters['returnUrl'] = !empty($this->P['returnUrl'])
|
||||
? GeneralUtility::sanitizeLocalUrl($this->P['returnUrl'])
|
||||
: $requestUri;
|
||||
$redirectUrl = (string)$uriBuilder->buildUriFromRoute('web_list', $urlParameters);
|
||||
}
|
||||
HttpUtility::redirect($redirectUrl);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Form\Wizard\SuggestWizardDefaultReceiver;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Receives ajax request from FormEngine suggest wizard and creates suggest answer as json result
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class SuggestWizardController
|
||||
{
|
||||
/**
|
||||
* Ajax handler for the "suggest" feature in FormEngine.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @throws \RuntimeException for incomplete or invalid arguments
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function searchAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
|
||||
$search = $parsedBody['value'] ?? null;
|
||||
$tableName = $parsedBody['tableName'] ?? null;
|
||||
$fieldName = $parsedBody['fieldName'] ?? null;
|
||||
$uid = $parsedBody['uid'] ?? null;
|
||||
$pid = isset($parsedBody['pid']) ? (int)$parsedBody['pid'] : 0;
|
||||
$dataStructureIdentifier = '' ?? null;
|
||||
if (!empty($parsedBody['dataStructureIdentifier'])) {
|
||||
$dataStructureIdentifier = $parsedBody['dataStructureIdentifier'];
|
||||
}
|
||||
$flexFormSheetName = $parsedBody['flexFormSheetName'] ?? null;
|
||||
$flexFormFieldName = $parsedBody['flexFormFieldName'] ?? null;
|
||||
$flexFormContainerName = $parsedBody['flexFormContainerName'] ?? null;
|
||||
$flexFormContainerFieldName = $parsedBody['flexFormContainerFieldName'] ?? null;
|
||||
|
||||
// Determine TCA config of field
|
||||
if (empty($dataStructureIdentifier)) {
|
||||
// Normal columns field
|
||||
$fieldConfig = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
|
||||
$fieldNameInPageTsConfig = $fieldName;
|
||||
} else {
|
||||
// A flex flex form field
|
||||
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
|
||||
$dataStructure = $flexFormTools->parseDataStructureByIdentifier($dataStructureIdentifier);
|
||||
if (empty($flexFormContainerFieldName)) {
|
||||
// @todo: See if a path in pageTsConfig like "TCEForm.tableName.theContainerFieldName =" is useful and works with other pageTs, too.
|
||||
$fieldNameInPageTsConfig = $flexFormFieldName;
|
||||
if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]['TCEforms']['config'])
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
'Specified path ' . $flexFormFieldName . ' not found in flex form data structure',
|
||||
1480609491
|
||||
);
|
||||
}
|
||||
$fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]['TCEforms']['config'];
|
||||
} else {
|
||||
$fieldNameInPageTsConfig = $flexFormContainerFieldName;
|
||||
if (!isset($dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerName]
|
||||
['el'][$flexFormContainerFieldName]['TCEforms']['config'])
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
'Specified path ' . $flexFormContainerName . ' not found in flex form section container data structure',
|
||||
1480611208
|
||||
);
|
||||
}
|
||||
$fieldConfig = $dataStructure['sheets'][$flexFormSheetName]['ROOT']
|
||||
['el'][$flexFormFieldName]
|
||||
['el'][$flexFormContainerName]
|
||||
['el'][$flexFormContainerFieldName]['TCEforms']['config'];
|
||||
}
|
||||
}
|
||||
|
||||
$pageTsConfig = BackendUtility::getPagesTSconfig($pid);
|
||||
|
||||
$wizardConfig = $fieldConfig['suggestOptions'] ?? [];
|
||||
|
||||
$queryTables = $this->getTablesToQueryFromFieldConfiguration($fieldConfig);
|
||||
$whereClause = $this->getWhereClause($fieldConfig);
|
||||
|
||||
$resultRows = [];
|
||||
|
||||
// fetch the records for each query table. A query table is a table from which records are allowed to
|
||||
// be added to the TCEForm selector, originally fetched from the "allowed" config option in the TCA
|
||||
foreach ($queryTables as $queryTable) {
|
||||
// if the table does not exist, skip it
|
||||
if (!is_array($GLOBALS['TCA'][$queryTable]) || empty($GLOBALS['TCA'][$queryTable])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$config = $this->getConfigurationForTable($queryTable, $wizardConfig, $pageTsConfig, $tableName, $fieldNameInPageTsConfig);
|
||||
|
||||
// process addWhere
|
||||
if (!isset($config['addWhere']) && $whereClause) {
|
||||
$config['addWhere'] = $whereClause;
|
||||
}
|
||||
if (isset($config['addWhere'])) {
|
||||
$replacement = [
|
||||
'###THIS_UID###' => (int)$uid,
|
||||
'###CURRENT_PID###' => (int)$pid
|
||||
];
|
||||
if (isset($pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'])) {
|
||||
$fieldTSconfig = $pageTsConfig['TCEFORM.'][$tableName . '.'][$fieldNameInPageTsConfig . '.'];
|
||||
if (isset($fieldTSconfig['PAGE_TSCONFIG_ID'])) {
|
||||
$replacement['###PAGE_TSCONFIG_ID###'] = (int)$fieldTSconfig['PAGE_TSCONFIG_ID'];
|
||||
}
|
||||
if (isset($fieldTSconfig['PAGE_TSCONFIG_IDLIST'])) {
|
||||
$replacement['###PAGE_TSCONFIG_IDLIST###'] = implode(',', GeneralUtility::intExplode(',', $fieldTSconfig['PAGE_TSCONFIG_IDLIST']));
|
||||
}
|
||||
if (isset($fieldTSconfig['PAGE_TSCONFIG_STR'])) {
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($fieldConfig['foreign_table']);
|
||||
// nasty hack, but it's currently not possible to just quote anything "inside" the value but not escaping
|
||||
// the whole field as it is not known where it is used in the WHERE clause
|
||||
$replacement['###PAGE_TSCONFIG_STR###'] = trim($connection->quote($fieldTSconfig['PAGE_TSCONFIG_STR']), '\'');
|
||||
}
|
||||
}
|
||||
$config['addWhere'] = strtr(' ' . $config['addWhere'], $replacement);
|
||||
}
|
||||
|
||||
// instantiate the class that should fetch the records for this $queryTable
|
||||
$receiverClassName = $config['receiverClass'];
|
||||
if (!class_exists($receiverClassName)) {
|
||||
$receiverClassName = SuggestWizardDefaultReceiver::class;
|
||||
}
|
||||
$receiverObj = GeneralUtility::makeInstance($receiverClassName, $queryTable, $config);
|
||||
$params = [
|
||||
'value' => $search,
|
||||
'uid' => $uid,
|
||||
];
|
||||
$rows = $receiverObj->queryTable($params);
|
||||
if (empty($rows)) {
|
||||
continue;
|
||||
}
|
||||
$resultRows = $rows + $resultRows;
|
||||
unset($rows);
|
||||
}
|
||||
|
||||
// Limit the number of items in the result list
|
||||
$maxItems = $config['maxItemsInResultList'] ?? 10;
|
||||
$maxItems = min(count($resultRows), $maxItems);
|
||||
|
||||
array_splice($resultRows, $maxItems);
|
||||
return (new JsonResponse())->setPayload(array_values($resultRows));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if a table has been marked as hidden in the configuration
|
||||
*
|
||||
* @param array $tableConfig
|
||||
* @return bool
|
||||
*/
|
||||
protected function isTableHidden(array $tableConfig)
|
||||
{
|
||||
return (bool)$tableConfig['ctrl']['hideTable'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current backend user is allowed to access the given table, based on the ctrl-section of the
|
||||
* table's configuration array (TCA) entry.
|
||||
*
|
||||
* @param array $tableConfig
|
||||
* @return bool
|
||||
*/
|
||||
protected function currentBackendUserMayAccessTable(array $tableConfig)
|
||||
{
|
||||
if ($this->getBackendUser()->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the user is no admin, they may not access admin-only tables
|
||||
if ($tableConfig['ctrl']['adminOnly']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// allow access to root level pages if security restrictions should be bypassed
|
||||
return !$tableConfig['ctrl']['rootLevel'] || $tableConfig['ctrl']['security']['ignoreRootLevelRestriction'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the configuration for the suggest wizard for the given table. This does multiple overlays from the
|
||||
* TSconfig.
|
||||
*
|
||||
* @param string $queryTable The table to query
|
||||
* @param array $wizardConfig The configuration for the wizard as configured in the data structure
|
||||
* @param array $TSconfig The TSconfig array of the current page
|
||||
* @param string $table The table where the wizard is used
|
||||
* @param string $field The field where the wizard is used
|
||||
* @return array
|
||||
*/
|
||||
protected function getConfigurationForTable($queryTable, array $wizardConfig, array $TSconfig, $table, $field)
|
||||
{
|
||||
$config = (array)$wizardConfig['default'];
|
||||
|
||||
if (is_array($wizardConfig[$queryTable])) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $wizardConfig[$queryTable]);
|
||||
}
|
||||
$globalSuggestTsConfig = $TSconfig['TCEFORM.']['suggest.'];
|
||||
$currentFieldSuggestTsConfig = $TSconfig['TCEFORM.'][$table . '.'][$field . '.']['suggest.'];
|
||||
|
||||
// merge the configurations of different "levels" to get the working configuration for this table and
|
||||
// field (i.e., go from the most general to the most special configuration)
|
||||
if (is_array($globalSuggestTsConfig['default.'])) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig['default.']);
|
||||
}
|
||||
|
||||
if (is_array($globalSuggestTsConfig[$queryTable . '.'])) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $globalSuggestTsConfig[$queryTable . '.']);
|
||||
}
|
||||
|
||||
// use $table instead of $queryTable here because we overlay a config
|
||||
// for the input-field here, not for the queried table
|
||||
if (is_array($currentFieldSuggestTsConfig['default.'])) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig['default.']);
|
||||
}
|
||||
|
||||
if (is_array($currentFieldSuggestTsConfig[$queryTable . '.'])) {
|
||||
ArrayUtility::mergeRecursiveWithOverrule($config, $currentFieldSuggestTsConfig[$queryTable . '.']);
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the given field configuration for the tables that should be used for querying and returns them as an
|
||||
* array.
|
||||
*
|
||||
* @param array $fieldConfig
|
||||
* @return array
|
||||
*/
|
||||
protected function getTablesToQueryFromFieldConfiguration(array $fieldConfig)
|
||||
{
|
||||
$queryTables = [];
|
||||
|
||||
if (isset($fieldConfig['allowed'])) {
|
||||
if ($fieldConfig['allowed'] !== '*') {
|
||||
// list of allowed tables
|
||||
$queryTables = GeneralUtility::trimExplode(',', $fieldConfig['allowed']);
|
||||
} else {
|
||||
// all tables are allowed, if the user can access them
|
||||
foreach ($GLOBALS['TCA'] as $tableName => $tableConfig) {
|
||||
if (!$this->isTableHidden($tableConfig) && $this->currentBackendUserMayAccessTable($tableConfig)) {
|
||||
$queryTables[] = $tableName;
|
||||
}
|
||||
}
|
||||
unset($tableName, $tableConfig);
|
||||
}
|
||||
} elseif (isset($fieldConfig['foreign_table'])) {
|
||||
// use the foreign table
|
||||
$queryTables = [$fieldConfig['foreign_table']];
|
||||
}
|
||||
|
||||
return $queryTables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SQL WHERE clause to use for querying records. This is currently only relevant if a foreign_table
|
||||
* is configured and should be used; it could e.g. be used to limit to a certain subset of records from the
|
||||
* foreign table
|
||||
*
|
||||
* @param array $fieldConfig
|
||||
* @return string
|
||||
*/
|
||||
protected function getWhereClause(array $fieldConfig)
|
||||
{
|
||||
if (!isset($fieldConfig['foreign_table'])) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// strip ORDER BY clause
|
||||
return trim(preg_replace('/ORDER[[:space:]]+BY.*/i', '', $fieldConfig['foreign_table_where']));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,792 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Controller\Wizard;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
|
||||
use TYPO3\CMS\Backend\Template\ModuleTemplate;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
|
||||
use TYPO3\CMS\Core\Configuration\FlexForm\FlexFormTools;
|
||||
use TYPO3\CMS\Core\DataHandling\DataHandler;
|
||||
use TYPO3\CMS\Core\Http\HtmlResponse;
|
||||
use TYPO3\CMS\Core\Http\RedirectResponse;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\HttpUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Script Class for rendering the Table Wizard
|
||||
* @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class TableController extends AbstractWizardController
|
||||
{
|
||||
use PublicPropertyDeprecationTrait;
|
||||
|
||||
/**
|
||||
* Properties which have been moved to protected status from public
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $deprecatedPublicProperties = [
|
||||
'content' => 'Using $content of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'inputStyle' => 'Using $inputStyle of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'xmlStorage' => 'Using $xmlStorage of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'numNewRows' => 'Using $numNewRows of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'colsFieldName' => 'Using $colsFieldName of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'P' => 'Using $P of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'TABLECFG' => 'Using $TABLECFG of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'tableParsing_quote' => 'Using $tableParsing_quote of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
'tableParsing_delimiter' => 'Using $tableParsing_delimiter of class TableController from the outside is discouraged, as this variable is only used for internal storage.',
|
||||
];
|
||||
|
||||
/**
|
||||
* Content accumulation for the module.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* If TRUE, <input> fields are shown instead of textareas.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $inputStyle = false;
|
||||
|
||||
/**
|
||||
* If set, the string version of the content is interpreted/written as XML
|
||||
* instead of the original line-based kind. This variable still needs binding
|
||||
* to the wizard parameters - but support is ready!
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $xmlStorage = 0;
|
||||
|
||||
/**
|
||||
* Number of new rows to add in bottom of wizard
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $numNewRows = 1;
|
||||
|
||||
/**
|
||||
* Name of field in parent record which MAY contain the number of columns for the table
|
||||
* here hardcoded to the value of tt_content. Should be set by FormEngine parameters (from P)
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $colsFieldName = 'cols';
|
||||
|
||||
/**
|
||||
* Wizard parameters, coming from FormEngine linking to the wizard.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $P;
|
||||
|
||||
/**
|
||||
* The array which is constantly submitted by the multidimensional form of this wizard.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $TABLECFG;
|
||||
|
||||
/**
|
||||
* Table parsing
|
||||
* quoting of table cells
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableParsing_quote;
|
||||
|
||||
/**
|
||||
* delimiter between table cells
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableParsing_delimiter;
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* ModuleTemplate object
|
||||
*
|
||||
* @var ModuleTemplate
|
||||
*/
|
||||
protected $moduleTemplate;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
|
||||
$this->getLanguageService()->includeLLFile('EXT:core/Resources/Private/Language/locallang_wizards.xlf');
|
||||
|
||||
// @deprecated since TYPO3 v9, will be moved out of __construct() in TYPO3 v10.0
|
||||
$this->init($GLOBALS['TYPO3_REQUEST']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects the request object for the current request or subrequest
|
||||
* As this controller goes only through the main() method, it is rather simple for now
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function mainAction(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$response = $this->renderContent($request);
|
||||
|
||||
if (empty($response)) {
|
||||
$response = new HtmlResponse($this->moduleTemplate->renderContent());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the table wizard
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function main()
|
||||
{
|
||||
trigger_error('TableController->main() will be replaced by protected method renderContent() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
|
||||
$response = $this->renderContent($GLOBALS['TYPO3_REQUEST']);
|
||||
|
||||
if ($response instanceof RedirectResponse) {
|
||||
HttpUtility::redirect($response->getHeaders()['location'][0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the table wizard content
|
||||
*
|
||||
* @return string HTML content for the form.
|
||||
* @throws \RuntimeException
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function tableWizard()
|
||||
{
|
||||
trigger_error('TableController->tableWizard() will be replaced by protected method renderTableWizard() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
|
||||
$result = $this->renderTableWizard($GLOBALS['TYPO3_REQUEST']);
|
||||
|
||||
if ($result instanceof RedirectResponse) {
|
||||
HttpUtility::redirect($result->getHeaders()['location'][0]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will get and return the configuration code string
|
||||
* Will also save (and possibly redirect/exit) the content if a save button has been pressed
|
||||
*
|
||||
* @param array $row Current parent record row
|
||||
* @return array Table config code in an array
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function getConfigCode($row)
|
||||
{
|
||||
trigger_error('TableController->getConfigCode() will be replaced by protected method getConfiguration() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
|
||||
$result = $this->getConfiguration($row, $GLOBALS['TYPO3_REQUEST']);
|
||||
|
||||
if ($result instanceof RedirectResponse) {
|
||||
HttpUtility::redirect($result->getHeaders()['location'][0]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the HTML for the Table Wizard:
|
||||
*
|
||||
* @param array $configuration Table config array
|
||||
* @return string HTML for the table wizard
|
||||
* @internal
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function getTableHTML($configuration)
|
||||
{
|
||||
trigger_error('TableController->getTableHTML() will be replaced by protected method getTableWizard() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
return $this->getTableWizard($configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a control button (up/down/around/delete) has been pressed for an item and accordingly it will
|
||||
* manipulate the internal TABLECFG array
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function changeFunc()
|
||||
{
|
||||
trigger_error('TableController->changeFunc() will be replaced by protected method manipulateTable() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
$this->manipulateTable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input array to a configuration code string
|
||||
*
|
||||
* @param array $cfgArr Array of table configuration (follows the input structure from the table wizard POST form)
|
||||
* @return string The array converted into a string with line-based configuration.
|
||||
* @see cfgString2CfgArray()
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function cfgArray2CfgString($cfgArr)
|
||||
{
|
||||
trigger_error('TableController->cfgArray2CfgString() will be replaced by protected method configurationArrayToString() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
return $this->configurationArrayToString($cfgArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input configuration code string into an array
|
||||
*
|
||||
* @param string $configurationCode Configuration code
|
||||
* @param int $columns Default number of columns
|
||||
* @return array Configuration array
|
||||
* @see cfgArray2CfgString()
|
||||
*
|
||||
* @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0
|
||||
*/
|
||||
public function cfgString2CfgArray($configurationCode, $columns)
|
||||
{
|
||||
trigger_error('TableController->cfgString2CfgArray() will be replaced by protected method configurationStringToArray() in TYPO3 v10.0. Do not call from other extensions.', E_USER_DEPRECATED);
|
||||
return $this->configurationStringToArray($configurationCode, $columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialization of the class
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
*/
|
||||
protected function init(ServerRequestInterface $request): void
|
||||
{
|
||||
$parsedBody = $request->getParsedBody();
|
||||
$queryParams = $request->getQueryParams();
|
||||
// GPvars:
|
||||
$this->P = $parsedBody['P'] ?? $queryParams['P'] ?? null;
|
||||
$this->TABLECFG = $parsedBody['TABLE'] ?? $queryParams['TABLE'] ?? null;
|
||||
// Setting options:
|
||||
$this->xmlStorage = $this->P['params']['xmlOutput'];
|
||||
$this->numNewRows = MathUtility::forceIntegerInRange($this->P['params']['numNewRows'], 1, 50, 5);
|
||||
// Textareas or input fields:
|
||||
$this->inputStyle = isset($this->TABLECFG['textFields']) ? (bool)$this->TABLECFG['textFields'] : true;
|
||||
$this->tableParsing_delimiter = '|';
|
||||
$this->tableParsing_quote = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function, rendering the table wizard
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return ResponseInterface|null
|
||||
*/
|
||||
protected function renderContent(ServerRequestInterface $request): ?ResponseInterface
|
||||
{
|
||||
$normalizedParams = $request->getAttribute('normalizedParams');
|
||||
$requestUri = $normalizedParams->getRequestUri();
|
||||
list($rUri) = explode('#', $requestUri);
|
||||
$this->content .= '<form action="' . htmlspecialchars($rUri) . '" method="post" id="TableController" name="wizardForm">';
|
||||
if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
|
||||
$tableWizard = $this->renderTableWizard($request);
|
||||
|
||||
if ($tableWizard instanceof RedirectResponse) {
|
||||
return $tableWizard;
|
||||
}
|
||||
|
||||
$this->content .= '<h2>' . htmlspecialchars($this->getLanguageService()->getLL('table_title')) . '</h2>'
|
||||
. '<div>' . $tableWizard . '</div>';
|
||||
} else {
|
||||
$this->content .= '<h2>' . htmlspecialchars($this->getLanguageService()->getLL('table_title')) . '</h2>'
|
||||
. '<div><span class="text-danger">' . htmlspecialchars($this->getLanguageService()->getLL('table_noData')) . '</span></div>';
|
||||
}
|
||||
$this->content .= '</form>';
|
||||
// Setting up the buttons and markers for docHeader
|
||||
$this->getButtons();
|
||||
// Build the <body> for the module
|
||||
$this->moduleTemplate->setContent($this->content);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the panel of buttons for submitting the form or otherwise perform operations.
|
||||
*/
|
||||
protected function getButtons(): void
|
||||
{
|
||||
$buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
|
||||
if ($this->P['table'] && $this->P['field'] && $this->P['uid']) {
|
||||
// CSH
|
||||
$cshButton = $buttonBar->makeHelpButton()
|
||||
->setModuleName('xMOD_csh_corebe')
|
||||
->setFieldName('wizard_table_wiz');
|
||||
$buttonBar->addButton($cshButton);
|
||||
// Close
|
||||
$closeButton = $buttonBar->makeLinkButton()
|
||||
->setHref($this->P['returnUrl'])
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL))
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.closeDoc'))
|
||||
->setShowLabelText(true);
|
||||
$buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 1);
|
||||
// Save
|
||||
$saveButton = $buttonBar->makeInputButton()
|
||||
->setName('_savedok')
|
||||
->setValue('1')
|
||||
->setForm('TableController')
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
|
||||
->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:rm.saveDoc'))
|
||||
->setShowLabelText(true);
|
||||
$buttonBar->addButton($saveButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
|
||||
// Reload
|
||||
$reloadButton = $buttonBar->makeInputButton()
|
||||
->setName('_refresh')
|
||||
->setValue('1')
|
||||
->setForm('TableController')
|
||||
->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL))
|
||||
->setTitle($this->getLanguageService()->getLL('forms_refresh'));
|
||||
$buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the table wizard content
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @return string|ResponseInterface HTML content for the form.
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function renderTableWizard(ServerRequestInterface $request)
|
||||
{
|
||||
if (!$this->checkEditAccess($this->P['table'], $this->P['uid'])) {
|
||||
throw new \RuntimeException('Wizard Error: No access', 1349692692);
|
||||
}
|
||||
// First, check the references by selecting the record:
|
||||
$row = BackendUtility::getRecord($this->P['table'], $this->P['uid']);
|
||||
if (!is_array($row)) {
|
||||
throw new \RuntimeException('Wizard Error: No reference to record', 1294587125);
|
||||
}
|
||||
// This will get the content of the form configuration code field to us - possibly cleaned up,
|
||||
// saved to database etc. if the form has been submitted in the meantime.
|
||||
$tableCfgArray = $this->getConfiguration($row, $request);
|
||||
|
||||
if ($tableCfgArray instanceof ResponseInterface) {
|
||||
return $tableCfgArray;
|
||||
}
|
||||
|
||||
// Generation of the Table Wizards HTML code:
|
||||
$content = $this->getTableWizard($tableCfgArray);
|
||||
// Return content:
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will get and return the configuration code string
|
||||
* Will also save (and possibly redirect/exit) the content if a save button has been pressed
|
||||
*
|
||||
* @param array $row Current parent record row
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array|ResponseInterface Table config code in an array
|
||||
*/
|
||||
protected function getConfiguration(array $row, ServerRequestInterface $request)
|
||||
{
|
||||
// Get delimiter settings
|
||||
$this->tableParsing_quote = $row['table_enclosure'] ? chr((int)$row['table_enclosure']) : '';
|
||||
$this->tableParsing_delimiter = $row['table_delimiter'] ? chr((int)$row['table_delimiter']) : '|';
|
||||
// If some data has been submitted, then construct
|
||||
if (isset($this->TABLECFG['c'])) {
|
||||
// Process incoming:
|
||||
$this->manipulateTable();
|
||||
// Convert to string (either line based or XML):
|
||||
if ($this->xmlStorage) {
|
||||
// Convert the input array to XML:
|
||||
$bodyText = '<?xml version="1.0" encoding="utf-8" standalone="yes" ?>' . LF . GeneralUtility::array2xml($this->TABLECFG['c'], '', 0, 'T3TableWizard');
|
||||
// Setting cfgArr directly from the input:
|
||||
$configuration = $this->TABLECFG['c'];
|
||||
} else {
|
||||
// Convert the input array to a string of configuration code:
|
||||
$bodyText = $this->configurationArrayToString($this->TABLECFG['c']);
|
||||
// Create cfgArr from the string based configuration - that way it is cleaned up
|
||||
// and any incompatibilities will be removed!
|
||||
$configuration = $this->configurationStringToArray($bodyText, (int)$row[$this->colsFieldName]);
|
||||
}
|
||||
// If a save button has been pressed, then save the new field content:
|
||||
if ($_POST['_savedok'] || $_POST['_saveandclosedok']) {
|
||||
// Get DataHandler object:
|
||||
/** @var DataHandler $dataHandler */
|
||||
$dataHandler = GeneralUtility::makeInstance(DataHandler::class);
|
||||
// Put content into the data array:
|
||||
$data = [];
|
||||
if ($this->P['flexFormPath']) {
|
||||
// Current value of flexForm path:
|
||||
$currentFlexFormData = GeneralUtility::xml2array($row[$this->P['field']]);
|
||||
/** @var FlexFormTools $flexFormTools */
|
||||
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
|
||||
$flexFormTools->setArrayValueByPath($this->P['flexFormPath'], $currentFlexFormData, $bodyText);
|
||||
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = $currentFlexFormData;
|
||||
} else {
|
||||
$data[$this->P['table']][$this->P['uid']][$this->P['field']] = $bodyText;
|
||||
}
|
||||
// Perform the update:
|
||||
$dataHandler->start($data, []);
|
||||
$dataHandler->process_datamap();
|
||||
// If the save/close button was pressed, then redirect the screen:
|
||||
if ($_POST['_saveandclosedok']) {
|
||||
return new RedirectResponse(GeneralUtility::sanitizeLocalUrl($this->P['returnUrl']));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If nothing has been submitted, load the $bodyText variable from the selected database row:
|
||||
if ($this->xmlStorage) {
|
||||
$configuration = GeneralUtility::xml2array($row[$this->P['field']]);
|
||||
} else {
|
||||
if ($this->P['flexFormPath']) {
|
||||
// Current value of flexForm path:
|
||||
$currentFlexFormData = GeneralUtility::xml2array($row[$this->P['field']]);
|
||||
/** @var FlexFormTools $flexFormTools */
|
||||
$flexFormTools = GeneralUtility::makeInstance(FlexFormTools::class);
|
||||
$configuration = $flexFormTools->getArrayValueByPath(
|
||||
$this->P['flexFormPath'],
|
||||
$currentFlexFormData
|
||||
);
|
||||
$configuration = $this->configurationStringToArray($configuration, 0);
|
||||
} else {
|
||||
// Regular line based table configuration:
|
||||
$columns = $row[$this->colsFieldName] ?? 0;
|
||||
$configuration = $this->configurationStringToArray($row[$this->P['field']] ?? '', (int)$columns);
|
||||
}
|
||||
}
|
||||
$configuration = is_array($configuration) ? $configuration : [];
|
||||
}
|
||||
return $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the HTML for the Table Wizard:
|
||||
*
|
||||
* @param array $configuration Table config array
|
||||
* @return string HTML for the table wizard
|
||||
*/
|
||||
protected function getTableWizard(array $configuration): string
|
||||
{
|
||||
// Traverse the rows:
|
||||
$tRows = [];
|
||||
$k = 0;
|
||||
$countLines = count($configuration);
|
||||
foreach ($configuration as $cellArr) {
|
||||
if (is_array($cellArr)) {
|
||||
// Initialize:
|
||||
$cells = [];
|
||||
$a = 0;
|
||||
// Traverse the columns:
|
||||
foreach ($cellArr as $cellContent) {
|
||||
if ($this->inputStyle) {
|
||||
$cells[] = '<input class="form-control" type="text" name="TABLE[c][' . ($k + 1) * 2 . '][' . ($a + 1) * 2 . ']" value="' . htmlspecialchars($cellContent) . '" />';
|
||||
} else {
|
||||
$cellContent = preg_replace('/<br[ ]?[\\/]?>/i', LF, $cellContent);
|
||||
$cells[] = '<textarea class="form-control" rows="6" name="TABLE[c][' . ($k + 1) * 2 . '][' . ($a + 1) * 2 . ']">' . htmlspecialchars($cellContent) . '</textarea>';
|
||||
}
|
||||
// Increment counter:
|
||||
$a++;
|
||||
}
|
||||
// CTRL panel for a table row (move up/down/around):
|
||||
$onClick = 'document.wizardForm.action+=' . GeneralUtility::quoteJSvalue('#ANC_' . (($k + 1) * 2 - 2)) . ';';
|
||||
$onClick = ' onclick="' . htmlspecialchars($onClick) . '"';
|
||||
$ctrl = '';
|
||||
if ($k !== 0) {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[row_up][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_up')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-up"></span></button>';
|
||||
} else {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[row_bottom][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_bottom')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-double-down"></span></button>';
|
||||
}
|
||||
if ($k + 1 !== $countLines) {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[row_down][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_down')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-down"></span></button>';
|
||||
} else {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[row_top][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_top')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-angle-double-up"></span></button>';
|
||||
}
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[row_remove][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_removeRow')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-trash"></span></button>';
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[row_add][' . ($k + 1) * 2 . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_addRow')) . '"' . $onClick . '><span class="t3-icon fa fa-fw fa-plus"></span></button>';
|
||||
$tRows[] = '
|
||||
<tr>
|
||||
<td>
|
||||
<a name="ANC_' . ($k + 1) * 2 . '"></a>
|
||||
<span class="btn-group' . ($this->inputStyle ? '' : '-vertical') . '">' . $ctrl . '</span>
|
||||
</td>
|
||||
<td>' . implode('</td>
|
||||
<td>', $cells) . '</td>
|
||||
</tr>';
|
||||
// Increment counter:
|
||||
$k++;
|
||||
}
|
||||
}
|
||||
// CTRL panel for a table column (move left/right/around/delete)
|
||||
$cells = [];
|
||||
$cells[] = '';
|
||||
// Finding first row:
|
||||
$firstRow = reset($configuration);
|
||||
if (is_array($firstRow)) {
|
||||
$cols = count($firstRow);
|
||||
for ($a = 1; $a <= $cols; $a++) {
|
||||
$b = $a * 2;
|
||||
$ctrl = '';
|
||||
if ($a !== 1) {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[col_left][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_left')) . '"><span class="t3-icon fa fa-fw fa-angle-left"></span></button>';
|
||||
} else {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[col_end][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_end')) . '"><span class="t3-icon fa fa-fw fa-angle-double-right"></span></button>';
|
||||
}
|
||||
if ($a != $cols) {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[col_right][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_right')) . '"><span class="t3-icon fa fa-fw fa-angle-right"></span></button>';
|
||||
} else {
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[col_start][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_start')) . '"><span class="t3-icon fa fa-fw fa-angle-double-left"></span></button>';
|
||||
}
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[col_remove][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_removeColumn')) . '"><span class="t3-icon fa fa-fw fa-trash"></span></button>';
|
||||
$ctrl .= '<button class="btn btn-default" name="TABLE[col_add][' . $b . ']" title="' . htmlspecialchars($this->getLanguageService()->getLL('table_addColumn')) . '"><span class="t3-icon fa fa-fw fa-plus"></span></button>';
|
||||
$cells[] = '<span class="btn-group">' . $ctrl . '</span>';
|
||||
}
|
||||
$tRows[] = '
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>' . implode('</td>
|
||||
<td>', $cells) . '</td>
|
||||
</tr>
|
||||
</tfoot>';
|
||||
}
|
||||
$content = '';
|
||||
$addSubmitOnClick = 'onclick="document.getElementById(\'TableController\').submit();"';
|
||||
// Implode all table rows into a string, wrapped in table tags.
|
||||
$content .= '
|
||||
|
||||
<!-- Table wizard -->
|
||||
<div class="table-fit table-fit-inline-block">
|
||||
<table id="typo3-tablewizard" class="table table-center">
|
||||
' . implode('', $tRows) . '
|
||||
</table>
|
||||
</div>';
|
||||
// Input type checkbox:
|
||||
$content .= '
|
||||
|
||||
<!-- Input mode check box: -->
|
||||
<div class="checkbox">
|
||||
<input type="hidden" name="TABLE[textFields]" value="0" />
|
||||
<label for="textFields">
|
||||
<input type="checkbox" ' . $addSubmitOnClick . ' name="TABLE[textFields]" id="textFields" value="1"' . ($this->inputStyle ? ' checked="checked"' : '') . ' />
|
||||
' . $this->getLanguageService()->getLL('table_smallFields') . '
|
||||
</label>
|
||||
</div>';
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects if a control button (up/down/around/delete) has been pressed for an item and accordingly it will
|
||||
* manipulate the internal TABLECFG array
|
||||
*/
|
||||
protected function manipulateTable(): void
|
||||
{
|
||||
if ($this->TABLECFG['col_remove']) {
|
||||
$kk = key($this->TABLECFG['col_remove']);
|
||||
$cmd = 'col_remove';
|
||||
} elseif ($this->TABLECFG['col_add']) {
|
||||
$kk = key($this->TABLECFG['col_add']);
|
||||
$cmd = 'col_add';
|
||||
} elseif ($this->TABLECFG['col_start']) {
|
||||
$kk = key($this->TABLECFG['col_start']);
|
||||
$cmd = 'col_start';
|
||||
} elseif ($this->TABLECFG['col_end']) {
|
||||
$kk = key($this->TABLECFG['col_end']);
|
||||
$cmd = 'col_end';
|
||||
} elseif ($this->TABLECFG['col_left']) {
|
||||
$kk = key($this->TABLECFG['col_left']);
|
||||
$cmd = 'col_left';
|
||||
} elseif ($this->TABLECFG['col_right']) {
|
||||
$kk = key($this->TABLECFG['col_right']);
|
||||
$cmd = 'col_right';
|
||||
} elseif ($this->TABLECFG['row_remove']) {
|
||||
$kk = key($this->TABLECFG['row_remove']);
|
||||
$cmd = 'row_remove';
|
||||
} elseif ($this->TABLECFG['row_add']) {
|
||||
$kk = key($this->TABLECFG['row_add']);
|
||||
$cmd = 'row_add';
|
||||
} elseif ($this->TABLECFG['row_top']) {
|
||||
$kk = key($this->TABLECFG['row_top']);
|
||||
$cmd = 'row_top';
|
||||
} elseif ($this->TABLECFG['row_bottom']) {
|
||||
$kk = key($this->TABLECFG['row_bottom']);
|
||||
$cmd = 'row_bottom';
|
||||
} elseif ($this->TABLECFG['row_up']) {
|
||||
$kk = key($this->TABLECFG['row_up']);
|
||||
$cmd = 'row_up';
|
||||
} elseif ($this->TABLECFG['row_down']) {
|
||||
$kk = key($this->TABLECFG['row_down']);
|
||||
$cmd = 'row_down';
|
||||
} else {
|
||||
$kk = '';
|
||||
$cmd = '';
|
||||
}
|
||||
if ($cmd && MathUtility::canBeInterpretedAsInteger($kk)) {
|
||||
if (strpos($cmd, 'row_') === 0) {
|
||||
switch ($cmd) {
|
||||
case 'row_remove':
|
||||
unset($this->TABLECFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_add':
|
||||
for ($a = 1; $a <= $this->numNewRows; $a++) {
|
||||
// Checking if set: The point is that any new row between existing rows
|
||||
// will be TRUE after one row is added while if rows are added in the bottom
|
||||
// of the table there will be no existing rows to stop the addition of new rows
|
||||
// which means it will add up to $this->numNewRows rows then.
|
||||
if (!isset($this->TABLECFG['c'][$kk + $a])) {
|
||||
$this->TABLECFG['c'][$kk + $a] = [];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'row_top':
|
||||
$this->TABLECFG['c'][1] = $this->TABLECFG['c'][$kk];
|
||||
unset($this->TABLECFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_bottom':
|
||||
$this->TABLECFG['c'][10000000] = $this->TABLECFG['c'][$kk];
|
||||
unset($this->TABLECFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_up':
|
||||
$this->TABLECFG['c'][$kk - 3] = $this->TABLECFG['c'][$kk];
|
||||
unset($this->TABLECFG['c'][$kk]);
|
||||
break;
|
||||
case 'row_down':
|
||||
$this->TABLECFG['c'][$kk + 3] = $this->TABLECFG['c'][$kk];
|
||||
unset($this->TABLECFG['c'][$kk]);
|
||||
break;
|
||||
}
|
||||
ksort($this->TABLECFG['c']);
|
||||
}
|
||||
if (strpos($cmd, 'col_') === 0) {
|
||||
foreach ($this->TABLECFG['c'] as $cAK => $value) {
|
||||
switch ($cmd) {
|
||||
case 'col_remove':
|
||||
unset($this->TABLECFG['c'][$cAK][$kk]);
|
||||
break;
|
||||
case 'col_add':
|
||||
$this->TABLECFG['c'][$cAK][$kk + 1] = '';
|
||||
break;
|
||||
case 'col_start':
|
||||
$this->TABLECFG['c'][$cAK][1] = $this->TABLECFG['c'][$cAK][$kk];
|
||||
unset($this->TABLECFG['c'][$cAK][$kk]);
|
||||
break;
|
||||
case 'col_end':
|
||||
$this->TABLECFG['c'][$cAK][1000000] = $this->TABLECFG['c'][$cAK][$kk];
|
||||
unset($this->TABLECFG['c'][$cAK][$kk]);
|
||||
break;
|
||||
case 'col_left':
|
||||
$this->TABLECFG['c'][$cAK][$kk - 3] = $this->TABLECFG['c'][$cAK][$kk];
|
||||
unset($this->TABLECFG['c'][$cAK][$kk]);
|
||||
break;
|
||||
case 'col_right':
|
||||
$this->TABLECFG['c'][$cAK][$kk + 3] = $this->TABLECFG['c'][$cAK][$kk];
|
||||
unset($this->TABLECFG['c'][$cAK][$kk]);
|
||||
break;
|
||||
}
|
||||
ksort($this->TABLECFG['c'][$cAK]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert line breaks to <br /> tags:
|
||||
foreach ($this->TABLECFG['c'] as $a => $value) {
|
||||
foreach ($this->TABLECFG['c'][$a] as $b => $value2) {
|
||||
$this->TABLECFG['c'][$a][$b] = str_replace(
|
||||
[CR, LF],
|
||||
['', '<br />'],
|
||||
$this->TABLECFG['c'][$a][$b]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input array to a configuration code string
|
||||
*
|
||||
* @param array $cfgArr Array of table configuration (follows the input structure from the table wizard POST form)
|
||||
* @return string The array converted into a string with line-based configuration.
|
||||
* @see configurationStringToArray()
|
||||
*/
|
||||
protected function configurationArrayToString(array $cfgArr): string
|
||||
{
|
||||
$inLines = [];
|
||||
// Traverse the elements of the table wizard and transform the settings into configuration code.
|
||||
foreach ($cfgArr as $valueA) {
|
||||
$thisLine = [];
|
||||
foreach ($valueA as $valueB) {
|
||||
$thisLine[] = $this->tableParsing_quote
|
||||
. str_replace($this->tableParsing_delimiter, '', $valueB) . $this->tableParsing_quote;
|
||||
}
|
||||
$inLines[] = implode($this->tableParsing_delimiter, $thisLine);
|
||||
}
|
||||
// Finally, implode the lines into a string:
|
||||
return implode(LF, $inLines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the input configuration code string into an array
|
||||
*
|
||||
* @param string $configurationCode Configuration code
|
||||
* @param int $columns Default number of columns
|
||||
* @return array Configuration array
|
||||
* @see configurationArrayToString()
|
||||
*/
|
||||
protected function configurationStringToArray(string $configurationCode, int $columns): array
|
||||
{
|
||||
// Explode lines in the configuration code - each line is a table row.
|
||||
$tableLines = explode(LF, $configurationCode);
|
||||
// Setting number of columns
|
||||
// auto...
|
||||
if (!$columns && trim($tableLines[0])) {
|
||||
$columns = count(explode($this->tableParsing_delimiter, $tableLines[0]));
|
||||
}
|
||||
$columns = $columns ?: 4;
|
||||
// Traverse the number of table elements:
|
||||
$configurationArray = [];
|
||||
foreach ($tableLines as $key => $value) {
|
||||
// Initialize:
|
||||
$valueParts = explode($this->tableParsing_delimiter, $value);
|
||||
// Traverse columns:
|
||||
for ($a = 0; $a < $columns; $a++) {
|
||||
if ($this->tableParsing_quote
|
||||
&& $valueParts[$a][0] === $this->tableParsing_quote
|
||||
&& substr($valueParts[$a], -1, 1) === $this->tableParsing_quote
|
||||
) {
|
||||
$valueParts[$a] = substr(trim($valueParts[$a]), 1, -1);
|
||||
}
|
||||
$configurationArray[$key][$a] = (string)$valueParts[$a];
|
||||
}
|
||||
}
|
||||
return $configurationArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Domain\Model\Module;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Model for menu entries
|
||||
*/
|
||||
class BackendModule
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $title = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $name = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $icon = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $link = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $onClick = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $navigationComponentId = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $navigationFrameScript = '';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $navigationFrameScriptParameters = '';
|
||||
|
||||
/**
|
||||
* @var \SplObjectStorage
|
||||
*/
|
||||
protected $children;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $collapsed = false;
|
||||
|
||||
/**
|
||||
* construct
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->children = new \SplObjectStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set children
|
||||
*
|
||||
* @param \SplObjectStorage $children
|
||||
*/
|
||||
public function setChildren($children)
|
||||
{
|
||||
$this->children = $children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get children
|
||||
*
|
||||
* @return \SplObjectStorage
|
||||
*/
|
||||
public function getChildren()
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Child
|
||||
*
|
||||
* @param \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule $child
|
||||
*/
|
||||
public function addChild(\TYPO3\CMS\Backend\Domain\Model\Module\BackendModule $child)
|
||||
{
|
||||
$this->children->attach($child);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set icon
|
||||
*
|
||||
* @param string $icon
|
||||
*/
|
||||
public function setIcon($icon)
|
||||
{
|
||||
$this->icon = $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get icon
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getIcon()
|
||||
{
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name
|
||||
*
|
||||
* @param string $name
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set title
|
||||
*
|
||||
* @param string $title
|
||||
*/
|
||||
public function setTitle($title)
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Title
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTitle()
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Link
|
||||
*
|
||||
* @param string $link
|
||||
*/
|
||||
public function setLink($link)
|
||||
{
|
||||
$this->link = $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Link
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLink()
|
||||
{
|
||||
return $this->link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Description
|
||||
*
|
||||
* @param string $description
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->description = $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Description
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Navigation Component Id
|
||||
*
|
||||
* @param string $navigationComponentId
|
||||
*/
|
||||
public function setNavigationComponentId($navigationComponentId)
|
||||
{
|
||||
$this->navigationComponentId = $navigationComponentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Navigation Component Id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNavigationComponentId()
|
||||
{
|
||||
return $this->navigationComponentId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $navigationFrameScript
|
||||
*/
|
||||
public function setNavigationFrameScript($navigationFrameScript)
|
||||
{
|
||||
$this->navigationFrameScript = $navigationFrameScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNavigationFrameScript()
|
||||
{
|
||||
return $this->navigationFrameScript;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $navigationFrameScriptParameters
|
||||
*/
|
||||
public function setNavigationFrameScriptParameters($navigationFrameScriptParameters)
|
||||
{
|
||||
$this->navigationFrameScriptParameters = $navigationFrameScriptParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getNavigationFrameScriptParameters()
|
||||
{
|
||||
return $this->navigationFrameScriptParameters;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set onClick
|
||||
*
|
||||
* @param string $onClick
|
||||
*/
|
||||
public function setOnClick($onClick)
|
||||
{
|
||||
$this->onClick = $onClick;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get onClick
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOnClick()
|
||||
{
|
||||
return $this->onClick;
|
||||
}
|
||||
|
||||
public function setCollapsed(bool $collapsed): void
|
||||
{
|
||||
$this->collapsed = $collapsed;
|
||||
}
|
||||
|
||||
public function getCollapsed(): bool
|
||||
{
|
||||
return $this->collapsed;
|
||||
}
|
||||
}
|
||||
+264
@@ -0,0 +1,264 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Backend\Domain\Repository\Localization;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Doctrine\DBAL\Driver\Statement;
|
||||
use TYPO3\CMS\Backend\Configuration\TranslationConfigurationProvider;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\Connection;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
|
||||
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Repository for record localizations
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class LocalizationRepository
|
||||
{
|
||||
/**
|
||||
* @var TranslationConfigurationProvider
|
||||
*/
|
||||
protected $translationConfigurationProvider;
|
||||
|
||||
public function __construct(TranslationConfigurationProvider $translationConfigurationProvider = null)
|
||||
{
|
||||
$this->translationConfigurationProvider = $translationConfigurationProvider ?? GeneralUtility::makeInstance(TranslationConfigurationProvider::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the language from which the records in a certain language were initially localized
|
||||
*
|
||||
* @param int $pageId
|
||||
* @param int $localizedLanguage
|
||||
* @return array
|
||||
*/
|
||||
public function fetchOriginLanguage(int $pageId, int $localizedLanguage): array
|
||||
{
|
||||
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
|
||||
|
||||
$queryBuilder->select('tt_content_orig.sys_language_uid')
|
||||
->from('tt_content')
|
||||
->join(
|
||||
'tt_content',
|
||||
'tt_content',
|
||||
'tt_content_orig',
|
||||
$queryBuilder->expr()->eq(
|
||||
'tt_content.l10n_source',
|
||||
$queryBuilder->quoteIdentifier('tt_content_orig.uid')
|
||||
)
|
||||
)
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'tt_content.pid',
|
||||
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'tt_content.sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($localizedLanguage, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->neq(
|
||||
'tt_content_orig.sys_language_uid',
|
||||
$queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->groupBy('tt_content_orig.sys_language_uid');
|
||||
$this->getAllowedLanguageConstraintsForBackendUser($pageId, $queryBuilder, $this->getBackendUser(), 'tt_content_orig');
|
||||
|
||||
return $queryBuilder->execute()->fetch() ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns number of localized records in given page and language
|
||||
* Records which were added to the language directly (not through translation) are not counted.
|
||||
*
|
||||
* @param int $pageId
|
||||
* @param int $languageId
|
||||
* @return int
|
||||
*/
|
||||
public function getLocalizedRecordCount(int $pageId, int $languageId): int
|
||||
{
|
||||
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
|
||||
|
||||
$rowCount = $queryBuilder->count('uid')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->neq(
|
||||
'l10n_source',
|
||||
$queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute()
|
||||
->fetchColumn(0);
|
||||
|
||||
return (int)$rowCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all available languages
|
||||
*
|
||||
* @param int $pageId
|
||||
* @param int $languageId
|
||||
* @return array
|
||||
*/
|
||||
public function fetchAvailableLanguages(int $pageId, int $languageId): array
|
||||
{
|
||||
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
|
||||
$queryBuilder->select('sys_language_uid')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->neq(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->groupBy('sys_language_uid');
|
||||
|
||||
$this->getAllowedLanguageConstraintsForBackendUser($pageId, $queryBuilder, $this->getBackendUser());
|
||||
$languages = $queryBuilder->execute()->fetchAll();
|
||||
return $languages ?: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds additional query constraints to exclude hidden languages and
|
||||
* limit a backend user to its allowed languages (unless the user is an admin)
|
||||
*
|
||||
* @param int $pageId
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param BackendUserAuthentication $backendUser
|
||||
* @param string $alias
|
||||
*/
|
||||
protected function getAllowedLanguageConstraintsForBackendUser(int $pageId, QueryBuilder $queryBuilder, BackendUserAuthentication $backendUser, string $alias = ''): void
|
||||
{
|
||||
if ($backendUser->isAdmin()) {
|
||||
return;
|
||||
}
|
||||
// This always includes default language
|
||||
$allowedLanguages = $this->translationConfigurationProvider->getSystemLanguages($pageId);
|
||||
$queryBuilder->andWhere(
|
||||
$queryBuilder->expr()->in(
|
||||
($alias === '' ? '' : ($alias . '.')) . 'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter(array_keys($allowedLanguages), Connection::PARAM_INT_ARRAY)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get records for copy process
|
||||
*
|
||||
* @param int $pageId
|
||||
* @param int $destLanguageId
|
||||
* @param int $languageId
|
||||
* @param string $fields
|
||||
* @return Statement
|
||||
*/
|
||||
public function getRecordsToCopyDatabaseResult(int $pageId, int $destLanguageId, int $languageId, string $fields = '*'): Statement
|
||||
{
|
||||
$originalUids = [];
|
||||
|
||||
// Get original uid of existing elements triggered language
|
||||
$queryBuilder = $this->getQueryBuilderWithWorkspaceRestriction('tt_content');
|
||||
|
||||
$originalUidsStatement = $queryBuilder
|
||||
->select('l10n_source')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($destLanguageId, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->execute();
|
||||
|
||||
while ($origUid = $originalUidsStatement->fetchColumn(0)) {
|
||||
$originalUids[] = (int)$origUid;
|
||||
}
|
||||
|
||||
$queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true))
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'pid',
|
||||
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->orderBy('sorting');
|
||||
|
||||
if (!empty($originalUids)) {
|
||||
$queryBuilder
|
||||
->andWhere(
|
||||
$queryBuilder->expr()->notIn(
|
||||
'uid',
|
||||
$queryBuilder->createNamedParameter($originalUids, Connection::PARAM_INT_ARRAY)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $queryBuilder->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a QueryBuilder for the given table with preconfigured restrictions
|
||||
* to not retrieve workspace placeholders or deleted records.
|
||||
*
|
||||
* @param string $tableName
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
protected function getQueryBuilderWithWorkspaceRestriction(string $tableName): QueryBuilder
|
||||
{
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($tableName);
|
||||
$queryBuilder->getRestrictions()
|
||||
->removeAll()
|
||||
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
|
||||
->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
|
||||
|
||||
return $queryBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Domain\Repository\Module;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Module\ModuleLoader;
|
||||
use TYPO3\CMS\Backend\Routing\UriBuilder;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Imaging\IconRegistry;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Repository for backend module menu
|
||||
* compiles all data from $GLOBALS[TBE_MODULES]
|
||||
*/
|
||||
class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface
|
||||
{
|
||||
/**
|
||||
* @var \TYPO3\CMS\Backend\Module\ModuleStorage
|
||||
*/
|
||||
protected $moduleStorage;
|
||||
|
||||
/**
|
||||
* Constructs the module menu and gets the Singleton instance of the menu
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->moduleStorage = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Module\ModuleStorage::class);
|
||||
|
||||
$rawData = $this->getRawModuleMenuData();
|
||||
|
||||
$this->convertRawModuleDataToModuleMenuObject($rawData);
|
||||
$this->createMenuEntriesForTbeModulesExt();
|
||||
}
|
||||
|
||||
/**
|
||||
* loads all module information in the module storage
|
||||
*
|
||||
* @param array $excludeGroupNames
|
||||
* @return \SplObjectStorage
|
||||
*/
|
||||
public function loadAllowedModules(array $excludeGroupNames = [])
|
||||
{
|
||||
if (empty($excludeGroupNames)) {
|
||||
return $this->moduleStorage->getEntries();
|
||||
}
|
||||
|
||||
$modules = new \SplObjectStorage();
|
||||
foreach ($this->moduleStorage->getEntries() as $moduleGroup) {
|
||||
if (!in_array($moduleGroup->getName(), $excludeGroupNames, true)) {
|
||||
if ($moduleGroup->getChildren()->count() > 0) {
|
||||
$modules->attach($moduleGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $groupName
|
||||
* @return \SplObjectStorage|false
|
||||
**/
|
||||
public function findByGroupName($groupName = '')
|
||||
{
|
||||
foreach ($this->moduleStorage->getEntries() as $moduleGroup) {
|
||||
if ($moduleGroup->getName() === $groupName) {
|
||||
return $moduleGroup;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module menu entry by name
|
||||
*
|
||||
* @param string $name
|
||||
* @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule|bool
|
||||
*/
|
||||
public function findByModuleName($name)
|
||||
{
|
||||
$entries = $this->moduleStorage->getEntries();
|
||||
$entry = $this->findByModuleNameInGivenEntries($name, $entries);
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a module menu entry by name in a given storage
|
||||
*
|
||||
* @param string $name
|
||||
* @param \SplObjectStorage $entries
|
||||
* @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule|bool
|
||||
*/
|
||||
public function findByModuleNameInGivenEntries($name, \SplObjectStorage $entries)
|
||||
{
|
||||
foreach ($entries as $entry) {
|
||||
if ($entry->getName() === $name) {
|
||||
return $entry;
|
||||
}
|
||||
$children = $entry->getChildren();
|
||||
if (!empty($children)) {
|
||||
$childRecord = $this->findByModuleNameInGivenEntries($name, $children);
|
||||
if ($childRecord !== false) {
|
||||
return $childRecord;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the module menu object structure from the raw data array
|
||||
*
|
||||
* @param array $rawModuleData
|
||||
*/
|
||||
protected function convertRawModuleDataToModuleMenuObject(array $rawModuleData)
|
||||
{
|
||||
foreach ($rawModuleData as $module) {
|
||||
$entry = $this->createEntryFromRawData($module);
|
||||
if (isset($module['subitems']) && !empty($module['subitems'])) {
|
||||
foreach ($module['subitems'] as $subitem) {
|
||||
$subEntry = $this->createEntryFromRawData($subitem);
|
||||
$entry->addChild($subEntry);
|
||||
}
|
||||
}
|
||||
$this->moduleStorage->attachEntry($entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a menu entry object from an array
|
||||
*
|
||||
* @param array $module
|
||||
* @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule
|
||||
*/
|
||||
protected function createEntryFromRawData(array $module)
|
||||
{
|
||||
/** @var \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule $entry */
|
||||
$entry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Domain\Model\Module\BackendModule::class);
|
||||
if (!empty($module['name']) && is_string($module['name'])) {
|
||||
$entry->setName($module['name']);
|
||||
}
|
||||
if (!empty($module['title']) && is_string($module['title'])) {
|
||||
$entry->setTitle($this->getLanguageService()->sL($module['title']));
|
||||
}
|
||||
if (!empty($module['onclick']) && is_string($module['onclick'])) {
|
||||
$entry->setOnClick($module['onclick']);
|
||||
}
|
||||
if (!empty($module['link']) && is_string($module['link'])) {
|
||||
$entry->setLink($module['link']);
|
||||
} elseif (empty($module['link']) && !empty($module['path']) && is_string($module['path'])) {
|
||||
$entry->setLink($module['path']);
|
||||
}
|
||||
if (!empty($module['description']) && is_string($module['description'])) {
|
||||
$entry->setDescription($this->getLanguageService()->sL($module['description']));
|
||||
}
|
||||
if (!empty($module['icon'])) {
|
||||
$entry->setIcon($module['icon']);
|
||||
}
|
||||
if (!empty($module['navigationComponentId']) && is_string($module['navigationComponentId'])) {
|
||||
$entry->setNavigationComponentId($module['navigationComponentId']);
|
||||
}
|
||||
if (!empty($module['navigationFrameScript']) && is_string($module['navigationFrameScript'])) {
|
||||
$entry->setNavigationFrameScript($module['navigationFrameScript']);
|
||||
} elseif (!empty($module['parentNavigationFrameScript']) && is_string($module['parentNavigationFrameScript'])) {
|
||||
$entry->setNavigationFrameScript($module['parentNavigationFrameScript']);
|
||||
}
|
||||
if (!empty($module['navigationFrameScriptParam']) && is_string($module['navigationFrameScriptParam'])) {
|
||||
$entry->setNavigationFrameScriptParameters($module['navigationFrameScriptParam']);
|
||||
}
|
||||
$moduleMenuState = json_decode($this->getBackendUser()->uc['modulemenu'] ?? '{}', true);
|
||||
$entry->setCollapsed(isset($moduleMenuState[$module['name']]));
|
||||
return $entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the "third level" menu entries (submodules for the info module for
|
||||
* example) from the TBE_MODULES_EXT array
|
||||
*/
|
||||
protected function createMenuEntriesForTbeModulesExt()
|
||||
{
|
||||
foreach ($GLOBALS['TBE_MODULES_EXT'] ?? [] as $mainModule => $tbeModuleExt) {
|
||||
list($main) = explode('_', $mainModule);
|
||||
$mainEntry = $this->findByModuleName($main);
|
||||
if ($mainEntry === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$subEntries = $mainEntry->getChildren();
|
||||
if (empty($subEntries)) {
|
||||
continue;
|
||||
}
|
||||
$matchingSubEntry = $this->findByModuleName($mainModule);
|
||||
if ($matchingSubEntry !== false) {
|
||||
if (isset($tbeModuleExt['MOD_MENU']) && isset($tbeModuleExt['MOD_MENU']['function'])) {
|
||||
foreach ($tbeModuleExt['MOD_MENU']['function'] as $subModule) {
|
||||
$entry = $this->createEntryFromRawData($subModule);
|
||||
$matchingSubEntry->addChild($entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* loads the module menu from the moduleloader based on $GLOBALS['TBE_MODULES']
|
||||
* and compiles an array with all the data needed for menu etc.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRawModuleMenuData()
|
||||
{
|
||||
// Loads the backend modules available for the logged in user.
|
||||
/** @var ModuleLoader $moduleLoader */
|
||||
$moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
|
||||
$moduleLoader->observeWorkspaces = true;
|
||||
$moduleLoader->load($GLOBALS['TBE_MODULES']);
|
||||
$loadedModules = $moduleLoader->modules;
|
||||
|
||||
$modules = [];
|
||||
|
||||
// Unset modules that are meant to be hidden from the menu.
|
||||
$loadedModules = $this->removeHiddenModules($loadedModules);
|
||||
$uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
|
||||
$dummyScript = (string)$uriBuilder->buildUriFromRoute('dummy');
|
||||
foreach ($loadedModules as $moduleName => $moduleData) {
|
||||
$moduleLink = '';
|
||||
if (!is_array($moduleData['sub'])) {
|
||||
$moduleLink = $moduleData['script'];
|
||||
}
|
||||
$moduleLink = GeneralUtility::resolveBackPath($moduleLink);
|
||||
$moduleLabels = $moduleLoader->getLabelsForModule($moduleName);
|
||||
$moduleKey = 'modmenu_' . $moduleName;
|
||||
$modules[$moduleKey] = [
|
||||
'name' => $moduleName,
|
||||
'title' => $moduleLabels['title'],
|
||||
'onclick' => 'top.goToModule(' . GeneralUtility::quoteJSvalue($moduleName) . ');',
|
||||
'icon' => $this->getModuleIcon($moduleKey, $moduleData),
|
||||
'link' => $moduleLink,
|
||||
'description' => $moduleLabels['shortdescription']
|
||||
];
|
||||
if (!is_array($moduleData['sub']) && $moduleData['script'] !== $dummyScript) {
|
||||
// Work around for modules with own main entry, but being self the only submodule
|
||||
$modules[$moduleKey]['subitems'][$moduleKey] = [
|
||||
'name' => $moduleName,
|
||||
'title' => $moduleLabels['title'],
|
||||
'onclick' => 'top.goToModule(' . GeneralUtility::quoteJSvalue($moduleName) . ');',
|
||||
'icon' => $this->getModuleIcon($moduleKey, $moduleData),
|
||||
'link' => $moduleLink,
|
||||
'originalLink' => $moduleLink,
|
||||
'description' => $moduleLabels['shortdescription'],
|
||||
'navigationFrameScript' => null,
|
||||
'navigationFrameScriptParam' => null,
|
||||
'navigationComponentId' => null
|
||||
];
|
||||
} elseif (is_array($moduleData['sub'])) {
|
||||
foreach ($moduleData['sub'] as $submoduleName => $submoduleData) {
|
||||
if (isset($submoduleData['script'])) {
|
||||
$submoduleLink = GeneralUtility::resolveBackPath($submoduleData['script']);
|
||||
} else {
|
||||
$submoduleLink = (string)$uriBuilder->buildUriFromRoute($submoduleData['name']);
|
||||
}
|
||||
$submoduleKey = $moduleName . '_' . $submoduleName;
|
||||
$submoduleLabels = $moduleLoader->getLabelsForModule($submoduleKey);
|
||||
$submoduleDescription = $submoduleLabels['shortdescription'];
|
||||
$originalLink = $submoduleLink;
|
||||
$navigationFrameScript = $submoduleData['navFrameScript'];
|
||||
$modules[$moduleKey]['subitems'][$submoduleKey] = [
|
||||
'name' => $moduleName . '_' . $submoduleName,
|
||||
'title' => $submoduleLabels['title'],
|
||||
'onclick' => 'top.goToModule(' . GeneralUtility::quoteJSvalue($moduleName . '_' . $submoduleName) . ');',
|
||||
'icon' => $this->getModuleIcon($moduleKey, $submoduleData),
|
||||
'link' => $submoduleLink,
|
||||
'originalLink' => $originalLink,
|
||||
'description' => $submoduleDescription,
|
||||
'navigationFrameScript' => $navigationFrameScript,
|
||||
'navigationFrameScriptParam' => $submoduleData['navFrameScriptParam'],
|
||||
'navigationComponentId' => $submoduleData['navigationComponentId']
|
||||
];
|
||||
// if the main module has a navframe script, inherit to the submodule,
|
||||
// but only if it is not disabled explicitly (option is set to FALSE)
|
||||
if ($moduleData['navFrameScript'] && $submoduleData['inheritNavigationComponentFromMainModule'] !== false) {
|
||||
$modules[$moduleKey]['subitems'][$submoduleKey]['parentNavigationFrameScript'] = $moduleData['navFrameScript'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $modules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads User configuration from options.hideModules and removes
|
||||
* modules accordingly.
|
||||
*
|
||||
* @param array $loadedModules
|
||||
* @return array
|
||||
*/
|
||||
protected function removeHiddenModules($loadedModules)
|
||||
{
|
||||
$userTsConfig = $this->getBackendUser()->getTSConfig();
|
||||
|
||||
// Hide modules if set in userTS.
|
||||
$hiddenMainModules = GeneralUtility::trimExplode(',', $userTsConfig['options.']['hideModules'] ?? '', true);
|
||||
foreach ($hiddenMainModules as $hiddenMainModule) {
|
||||
unset($loadedModules[$hiddenMainModule]);
|
||||
}
|
||||
|
||||
// Hide sub-modules if set in userTS.
|
||||
$hiddenModules = $userTsConfig['options.']['hideModules.'] ?? [];
|
||||
if (is_array($hiddenModules)) {
|
||||
foreach ($hiddenModules as $mainModuleName => $subModules) {
|
||||
$hiddenSubModules = GeneralUtility::trimExplode(',', $subModules, true);
|
||||
foreach ($hiddenSubModules as $hiddenSubModule) {
|
||||
unset($loadedModules[$mainModuleName]['sub'][$hiddenSubModule]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $loadedModules;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the module icon
|
||||
*
|
||||
* @param string $moduleKey Module key
|
||||
* @param array $moduleData the compiled data associated with it
|
||||
* @return string Icon data, either sprite or <img> tag
|
||||
*/
|
||||
protected function getModuleIcon($moduleKey, $moduleData)
|
||||
{
|
||||
$iconIdentifier = !empty($moduleData['iconIdentifier'])
|
||||
? $moduleData['iconIdentifier']
|
||||
: 'module-icon-' . $moduleKey;
|
||||
$iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
|
||||
if ($iconRegistry->isRegistered($iconIdentifier)) {
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
return $iconFactory->getIcon($iconIdentifier)->render();
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return language service instance
|
||||
*
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,434 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Domain\Repository;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Controller\HelpController;
|
||||
use TYPO3\CMS\Backend\Module\ModuleLoader;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Type\File\ImageInfo;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
|
||||
/**
|
||||
* Table manual repository for csh manual handling
|
||||
* @internal This class is a specific Backend repository implementation and is not considered part of the Public TYPO3 API.
|
||||
*/
|
||||
class TableManualRepository
|
||||
{
|
||||
/**
|
||||
* Get the manual of the given table
|
||||
*
|
||||
* @param string $table
|
||||
* @return array the manual for a TCA table, see getItem() for details
|
||||
*/
|
||||
public function getTableManual($table)
|
||||
{
|
||||
$parts = [];
|
||||
|
||||
// Load descriptions for table $table
|
||||
$this->getLanguageService()->loadSingleTableDescription($table);
|
||||
if (is_array($GLOBALS['TCA_DESCR'][$table]['columns']) && $this->checkAccess('tables_select', $table)) {
|
||||
// Reserved for header of table
|
||||
$parts[0] = '';
|
||||
// Traverse table columns as listed in TCA_DESCR
|
||||
foreach ($GLOBALS['TCA_DESCR'][$table]['columns'] as $field => $_) {
|
||||
if (!$this->isExcludableField($table, $field) || $this->checkAccess('non_exclude_fields', $table . ':' . $field)) {
|
||||
if (!$field) {
|
||||
// Header
|
||||
$parts[0] = $this->getItem($table, '', true);
|
||||
} else {
|
||||
// Field
|
||||
$parts[] = $this->getItem($table, $field, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$parts[0]) {
|
||||
unset($parts[0]);
|
||||
}
|
||||
}
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single manual
|
||||
*
|
||||
* @param string $table table name
|
||||
* @param string $field field name
|
||||
* @return array
|
||||
*/
|
||||
public function getSingleManual($table, $field)
|
||||
{
|
||||
$this->getLanguageService()->loadSingleTableDescription($table);
|
||||
return $this->getItem($table, $field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get TOC sections
|
||||
*
|
||||
* @param int $mode e.g. HelpController::TOC_ONLY
|
||||
* @return array
|
||||
*/
|
||||
public function getSections($mode)
|
||||
{
|
||||
// Initialize
|
||||
$cshKeys = array_flip(array_keys($GLOBALS['TCA_DESCR']));
|
||||
$tcaKeys = array_keys($GLOBALS['TCA']);
|
||||
$outputSections = [];
|
||||
$tocArray = [];
|
||||
// TYPO3 Core Features
|
||||
$lang = $this->getLanguageService();
|
||||
$lang->loadSingleTableDescription('xMOD_csh_corebe');
|
||||
$this->renderTableOfContentItem($mode, 'xMOD_csh_corebe', 'core', $outputSections, $tocArray, $cshKeys);
|
||||
// Backend Modules
|
||||
$loadModules = GeneralUtility::makeInstance(ModuleLoader::class);
|
||||
$loadModules->load($GLOBALS['TBE_MODULES']);
|
||||
foreach ($loadModules->modules as $mainMod => $info) {
|
||||
$cshKey = '_MOD_' . $mainMod;
|
||||
if ($cshKeys[$cshKey]) {
|
||||
$lang->loadSingleTableDescription($cshKey);
|
||||
$this->renderTableOfContentItem($mode, $cshKey, 'modules', $outputSections, $tocArray, $cshKeys);
|
||||
}
|
||||
if (is_array($info['sub'])) {
|
||||
foreach ($info['sub'] as $subMod => $subInfo) {
|
||||
$cshKey = '_MOD_' . $mainMod . '_' . $subMod;
|
||||
if ($cshKeys[$cshKey]) {
|
||||
$lang->loadSingleTableDescription($cshKey);
|
||||
$this->renderTableOfContentItem($mode, $cshKey, 'modules', $outputSections, $tocArray, $cshKeys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Database Tables
|
||||
foreach ($tcaKeys as $table) {
|
||||
// Load descriptions for table $table
|
||||
$lang->loadSingleTableDescription($table);
|
||||
if (is_array($GLOBALS['TCA_DESCR'][$table]['columns']) && $this->checkAccess('tables_select', $table)) {
|
||||
$this->renderTableOfContentItem($mode, $table, 'tables', $outputSections, $tocArray, $cshKeys);
|
||||
}
|
||||
}
|
||||
foreach ($cshKeys as $cshKey => $value) {
|
||||
// Extensions
|
||||
if (GeneralUtility::isFirstPartOfStr($cshKey, 'xEXT_') && !isset($GLOBALS['TCA'][$cshKey])) {
|
||||
$lang->loadSingleTableDescription($cshKey);
|
||||
$this->renderTableOfContentItem($mode, $cshKey, 'extensions', $outputSections, $tocArray, $cshKeys);
|
||||
}
|
||||
// Other
|
||||
if (!GeneralUtility::isFirstPartOfStr($cshKey, '_MOD_') && !isset($GLOBALS['TCA'][$cshKey])) {
|
||||
$lang->loadSingleTableDescription($cshKey);
|
||||
$this->renderTableOfContentItem($mode, $cshKey, 'other', $outputSections, $tocArray, $cshKeys);
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode === HelpController::TOC_ONLY) {
|
||||
return $tocArray;
|
||||
}
|
||||
|
||||
return [
|
||||
'toc' => $tocArray,
|
||||
'content' => $outputSections
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TOC list element and renders corresponding HELP content if "renderALL" mode is set.
|
||||
*
|
||||
* @param int $mode Mode
|
||||
* @param string $table CSH key / Table name
|
||||
* @param string $tocCat TOC category keyword: "core", "modules", "tables", "other
|
||||
* @param array $outputSections Array for accumulation of rendered HELP Content (in "renderALL" mode). Passed by reference!
|
||||
* @param array $tocArray TOC array; Here TOC index elements are created. Passed by reference!
|
||||
* @param array $CSHkeys CSH keys array. Every item rendered will be unset in this array so finally we can see what CSH keys are not processed yet. Passed by reference!
|
||||
*/
|
||||
protected function renderTableOfContentItem($mode, $table, $tocCat, &$outputSections, &$tocArray, &$CSHkeys)
|
||||
{
|
||||
$tocArray[$tocCat][$table] = $this->getTableFieldLabel($table);
|
||||
if (!$mode) {
|
||||
// Render full manual right here!
|
||||
$outputSections[$table]['content'] = $this->getTableManual($table);
|
||||
if (!$outputSections[$table]) {
|
||||
unset($outputSections[$table]);
|
||||
}
|
||||
}
|
||||
|
||||
// Unset CSH key
|
||||
unset($CSHkeys[$table]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns composite label for table/field
|
||||
*
|
||||
* @param string $key CSH key / table name
|
||||
* @param string $field Sub key / field name
|
||||
* @param string $mergeToken Token to merge the two strings with
|
||||
* @return string Labels joined with merge token
|
||||
* @see getTableFieldNames()
|
||||
*/
|
||||
protected function getTableFieldLabel($key, $field = '', $mergeToken = ': ')
|
||||
{
|
||||
// Get table / field parts
|
||||
list($tableName, $fieldName) = $this->getTableFieldNames($key, $field);
|
||||
// Create label
|
||||
return $this->getLanguageService()->sL($tableName) . ($field ? $mergeToken . rtrim(trim($this->getLanguageService()->sL($fieldName)), ':') : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns labels for a given field in a given structure
|
||||
*
|
||||
* @param string $key CSH key / table name
|
||||
* @param string $field Sub key / field name
|
||||
* @return array Table and field labels in a numeric array
|
||||
*/
|
||||
protected function getTableFieldNames($key, $field)
|
||||
{
|
||||
$this->getLanguageService()->loadSingleTableDescription($key);
|
||||
// Define the label for the key
|
||||
if (!empty($GLOBALS['TCA_DESCR'][$key]['columns']['']['alttitle'])) {
|
||||
// If there's an alternative title, use it
|
||||
$keyName = $GLOBALS['TCA_DESCR'][$key]['columns']['']['alttitle'];
|
||||
} elseif (isset($GLOBALS['TCA'][$key])) {
|
||||
// Otherwise, if it's a table, use its title
|
||||
$keyName = $GLOBALS['TCA'][$key]['ctrl']['title'];
|
||||
} else {
|
||||
// If no title was found, make sure to remove any "_MOD_"
|
||||
$keyName = preg_replace('/^_MOD_/', '', $key);
|
||||
}
|
||||
// Define the label for the field
|
||||
$fieldName = $field;
|
||||
if (!empty($GLOBALS['TCA_DESCR'][$key]['columns'][$field]['alttitle'])) {
|
||||
// If there's an alternative title, use it
|
||||
$fieldName = $GLOBALS['TCA_DESCR'][$key]['columns'][$field]['alttitle'];
|
||||
} elseif (!empty($GLOBALS['TCA'][$key]['columns'][$field])) {
|
||||
// Otherwise, if it's a table, use its title
|
||||
$fieldName = $GLOBALS['TCA'][$key]['columns'][$field]['label'];
|
||||
}
|
||||
return [$keyName, $fieldName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a single $table/$field information piece
|
||||
* If $anchors is set, then seeAlso references to the same table will be page-anchors, not links.
|
||||
*
|
||||
* @param string $table CSH key / table name
|
||||
* @param string $field Sub key / field name
|
||||
* @param bool $anchors If anchors is to be shown.
|
||||
* @return array with the information
|
||||
*/
|
||||
protected function getItem($table, $field, $anchors = false)
|
||||
{
|
||||
if (!empty($table)) {
|
||||
$field = !empty($field) ? $field : '';
|
||||
$setup = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
|
||||
return [
|
||||
'table' => $table,
|
||||
'field' => $field,
|
||||
'configuration' => $setup,
|
||||
'headerLine' => $this->getTableFieldLabel($table, $field),
|
||||
'content' => !empty($setup['description']) ? $setup['description'] : '',
|
||||
'images' => !empty($setup['image']) ? $this->getImages($setup['image'], $setup['image_descr']) : [],
|
||||
'seeAlso' => !empty($setup['seeAlso']) ? $this->getSeeAlsoLinks($setup['seeAlso'], $anchors ? $table : '') : '',
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get see-also links
|
||||
*
|
||||
* @param string $value See-also input codes
|
||||
* @param string $anchorTable If $anchorTable is set to a tablename, then references to this table will be made as anchors, not URLs.
|
||||
* @return array See-also links
|
||||
*/
|
||||
protected function getSeeAlsoLinks($value, $anchorTable = '')
|
||||
{
|
||||
// Split references by comma or linebreak
|
||||
$items = preg_split('/[,' . LF . ']/', $value);
|
||||
$lines = [];
|
||||
foreach ($items as $itemValue) {
|
||||
$itemValue = trim($itemValue);
|
||||
if ($itemValue) {
|
||||
$reference = GeneralUtility::trimExplode(':', $itemValue);
|
||||
$referenceUrl = GeneralUtility::trimExplode('|', $itemValue);
|
||||
if (strpos($referenceUrl[1], 'http') === 0) {
|
||||
// URL reference
|
||||
$lines[] = [
|
||||
'url' => $referenceUrl[1],
|
||||
'title' => $referenceUrl[0],
|
||||
'target' => '_blank'
|
||||
];
|
||||
} elseif (strpos($referenceUrl[1], 'FILE:') === 0) {
|
||||
// File reference
|
||||
$fileName = GeneralUtility::getFileAbsFileName(substr($referenceUrl[1], 5));
|
||||
if ($fileName && @is_file($fileName)) {
|
||||
$fileName = '../' . PathUtility::stripPathSitePrefix($fileName);
|
||||
$lines[] = [
|
||||
'url' => $fileName,
|
||||
'title' => $referenceUrl[0],
|
||||
'target' => '_blank'
|
||||
];
|
||||
}
|
||||
} else {
|
||||
// Table reference
|
||||
$table = !empty($reference[0]) ? $reference[0] : '';
|
||||
$field = !empty($reference[1]) ? $reference[1] : '';
|
||||
$accessAllowed = true;
|
||||
// Check if table exists and current user can access it
|
||||
if (!empty($table)) {
|
||||
$accessAllowed = !$this->getTableSetup($table) || $this->checkAccess('tables_select', $table);
|
||||
}
|
||||
// Check if field exists and is excludable or user can access it
|
||||
if ($accessAllowed && !empty($field)) {
|
||||
$accessAllowed = !$this->isExcludableField($table, $field) || $this->checkAccess('non_exclude_fields', $table . ':' . $field);
|
||||
}
|
||||
// Check read access
|
||||
if ($accessAllowed && isset($GLOBALS['TCA_DESCR'][$table])) {
|
||||
// Make see-also link
|
||||
$label = $this->getTableFieldLabel($table, $field, ' / ');
|
||||
if ($anchorTable && $table === $anchorTable) {
|
||||
$lines[] = [
|
||||
'url' => '#' . rawurlencode(implode('.', $reference)),
|
||||
'title' => $label,
|
||||
];
|
||||
} else {
|
||||
$lines[] = [
|
||||
'internal' => true,
|
||||
'arguments' => [
|
||||
'table' => $table,
|
||||
'field' => $field,
|
||||
'action' => 'detail',
|
||||
],
|
||||
'title' => $label
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if given table / field is excludable
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $field The field
|
||||
* @return bool TRUE if given field is excludable
|
||||
*/
|
||||
protected function isExcludableField($table, $field)
|
||||
{
|
||||
$fieldSetup = $this->getFieldSetup($table, $field);
|
||||
if (!empty($fieldSetup)) {
|
||||
return !empty($fieldSetup['exclude']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of images with description
|
||||
*
|
||||
* @param string $images Image file reference (list of)
|
||||
* @param string $descriptions Description string (divided for each image by line break)
|
||||
* @return array
|
||||
*/
|
||||
protected function getImages($images, $descriptions)
|
||||
{
|
||||
$imageData = [];
|
||||
// Splitting
|
||||
$imgArray = GeneralUtility::trimExplode(',', $images, true);
|
||||
if (!empty($imgArray)) {
|
||||
$descrArray = explode(LF, $descriptions, count($imgArray));
|
||||
foreach ($imgArray as $k => $image) {
|
||||
$descriptions = $descrArray[$k];
|
||||
$absImagePath = GeneralUtility::getFileAbsFileName($image);
|
||||
if ($absImagePath && @is_file($absImagePath)) {
|
||||
$imgFile = PathUtility::stripPathSitePrefix($absImagePath);
|
||||
$imageInfo = GeneralUtility::makeInstance(ImageInfo::class, $absImagePath);
|
||||
if ($imageInfo->getWidth()) {
|
||||
$imageData[] = [
|
||||
'image' => $imgFile,
|
||||
'description' => $descriptions
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $imageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the setup for given table
|
||||
*
|
||||
* @param string $table The table
|
||||
* @return array The table setup
|
||||
*/
|
||||
protected function getTableSetup($table)
|
||||
{
|
||||
if (!empty($table) && !empty($GLOBALS['TCA'][$table])) {
|
||||
return $GLOBALS['TCA'][$table];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the setup for given table / field
|
||||
*
|
||||
* @param string $table The table
|
||||
* @param string $field The field
|
||||
* @param bool $allowEmptyField Allow empty field
|
||||
* @return array The field setup
|
||||
*/
|
||||
protected function getFieldSetup($table, $field, $allowEmptyField = false)
|
||||
{
|
||||
$tableSetup = $this->getTableSetup($table);
|
||||
if (!empty($tableSetup) && (!empty($field) || $allowEmptyField) && !empty($tableSetup['columns'][$field])) {
|
||||
return $tableSetup['columns'][$field];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current backend user has access to given identifier
|
||||
*
|
||||
* @param string $type The type
|
||||
* @param string $identifier The search string in access list
|
||||
* @return bool TRUE if the user has access
|
||||
*/
|
||||
protected function checkAccess($type, $identifier)
|
||||
{
|
||||
if (!empty($type) && !empty($identifier)) {
|
||||
return $this->getBackendUser()->check($type, $identifier);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser(): BackendUserAuthentication
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns LanguageService
|
||||
*
|
||||
* @return \TYPO3\CMS\Core\Localization\LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Exception as CoreException;
|
||||
|
||||
/**
|
||||
* Generic Backend exception
|
||||
*/
|
||||
class Exception extends CoreException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Exception;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Exception;
|
||||
|
||||
class BackendLockedException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Exception;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown if site configuration for a page is not found
|
||||
*/
|
||||
class SiteValidationErrorException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Backend\Form;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Base class for container and single elements - their abstracts extend from here.
|
||||
*/
|
||||
abstract class AbstractNode implements NodeInterface, LoggerAwareInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* Instance of the node factory to create sub elements, container and single element expansions.
|
||||
*
|
||||
* @var NodeFactory
|
||||
*/
|
||||
protected $nodeFactory;
|
||||
|
||||
/**
|
||||
* Main data array to work on, given from parent to child elements
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* A list of default field information added to the element / container.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldInformation = [];
|
||||
|
||||
/**
|
||||
* A list of default field controls added to the element / container.
|
||||
* This property is often reset by single elements.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldControl = [];
|
||||
|
||||
/**
|
||||
* A list of default field wizards added to the element / container.
|
||||
* This property is often reset by single elements.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldWizard = [];
|
||||
|
||||
/**
|
||||
* Set data to data array and register node factory to render sub elements
|
||||
*
|
||||
* @param NodeFactory $nodeFactory
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(NodeFactory $nodeFactory, array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->nodeFactory = $nodeFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for single nodes
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
abstract public function render();
|
||||
|
||||
/**
|
||||
* Initialize the array that is returned to parent after calling. This structure
|
||||
* is identical for *all* nodes. Parent will merge the return of a child with its
|
||||
* own stuff and in itself return an array of the same structure.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function initializeResultArray(): array
|
||||
{
|
||||
return [
|
||||
'additionalJavaScriptPost' => [],
|
||||
'additionalJavaScriptSubmit' => [],
|
||||
'additionalHiddenFields' => [],
|
||||
'additionalInlineLanguageLabelFiles' => [],
|
||||
'stylesheetFiles' => [],
|
||||
// can hold strings or arrays,
|
||||
// string = requireJS module,
|
||||
// array = requireJS module + callback e.g. array('TYPO3/Foo/Bar', 'function() {}')
|
||||
'requireJsModules' => [],
|
||||
'inlineData' => [],
|
||||
'html' => '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge existing data with a child return array.
|
||||
* The incoming $childReturn array should be initialized
|
||||
* using initializeResultArray() beforehand.
|
||||
*
|
||||
* @param array $existing Currently merged array
|
||||
* @param array $childReturn Array returned by child
|
||||
* @param bool $mergeHtml If false, the ['html'] section of $childReturn will NOT be added to $existing
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function mergeChildReturnIntoExistingResult(array $existing, array $childReturn, bool $mergeHtml = true): array
|
||||
{
|
||||
if ($mergeHtml && !empty($childReturn['html'])) {
|
||||
$existing['html'] .= LF . $childReturn['html'];
|
||||
}
|
||||
foreach ($childReturn['additionalJavaScriptPost'] ?? [] as $value) {
|
||||
$existing['additionalJavaScriptPost'][] = $value;
|
||||
}
|
||||
foreach ($childReturn['additionalJavaScriptSubmit'] ?? [] as $value) {
|
||||
$existing['additionalJavaScriptSubmit'][] = $value;
|
||||
}
|
||||
foreach ($childReturn['additionalHiddenFields'] ?? [] as $value) {
|
||||
$existing['additionalHiddenFields'][] = $value;
|
||||
}
|
||||
foreach ($childReturn['stylesheetFiles'] ?? [] as $value) {
|
||||
$existing['stylesheetFiles'][] = $value;
|
||||
}
|
||||
foreach ($childReturn['requireJsModules'] ?? [] as $module) {
|
||||
$existing['requireJsModules'][] = $module;
|
||||
}
|
||||
foreach ($childReturn['additionalInlineLanguageLabelFiles'] ?? [] as $inlineLanguageLabelFile) {
|
||||
$existing['additionalInlineLanguageLabelFiles'][] = $inlineLanguageLabelFile;
|
||||
}
|
||||
if (!empty($childReturn['inlineData'])) {
|
||||
$existingInlineData = $existing['inlineData'];
|
||||
$childInlineData = $childReturn['inlineData'];
|
||||
ArrayUtility::mergeRecursiveWithOverrule($existingInlineData, $childInlineData);
|
||||
$existing['inlineData'] = $existingInlineData;
|
||||
}
|
||||
return $existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build JSON string for validations rules.
|
||||
*
|
||||
* @param array $config
|
||||
* @return string
|
||||
*/
|
||||
protected function getValidationDataAsJsonString(array $config): string
|
||||
{
|
||||
$validationRules = [];
|
||||
if (!empty($config['eval'])) {
|
||||
$evalList = GeneralUtility::trimExplode(',', $config['eval'], true);
|
||||
foreach ($evalList as $evalType) {
|
||||
$validationRules[] = [
|
||||
'type' => $evalType,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (!empty($config['range'])) {
|
||||
$newValidationRule = [
|
||||
'type' => 'range',
|
||||
];
|
||||
if (!empty($config['range']['lower'])) {
|
||||
$newValidationRule['lower'] = $config['range']['lower'];
|
||||
}
|
||||
if (!empty($config['range']['upper'])) {
|
||||
$newValidationRule['upper'] = $config['range']['upper'];
|
||||
}
|
||||
$validationRules[] = $newValidationRule;
|
||||
}
|
||||
if (!empty($config['maxitems']) || !empty($config['minitems'])) {
|
||||
$minItems = isset($config['minitems']) ? (int)$config['minitems'] : 0;
|
||||
$maxItems = isset($config['maxitems']) ? (int)$config['maxitems'] : 99999;
|
||||
$type = $config['type'] ?: 'range';
|
||||
$validationRules[] = [
|
||||
'type' => $type,
|
||||
'minItems' => $minItems,
|
||||
'maxItems' => $maxItems
|
||||
];
|
||||
}
|
||||
if (!empty($config['required'])) {
|
||||
$validationRules[] = ['type' => 'required'];
|
||||
}
|
||||
return json_encode($validationRules);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Form\AbstractNode;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Abstract container has various methods used by the container classes
|
||||
*/
|
||||
abstract class AbstractContainer extends AbstractNode
|
||||
{
|
||||
/**
|
||||
* Merge field information configuration with default and render them.
|
||||
*
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderFieldInformation(): array
|
||||
{
|
||||
$options = $this->data;
|
||||
$fieldInformation = $this->defaultFieldInformation;
|
||||
$currentRenderType = $this->data['renderType'];
|
||||
$fieldInformationFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldInformation'] ?? [];
|
||||
ArrayUtility::mergeRecursiveWithOverrule($fieldInformation, $fieldInformationFromTca);
|
||||
$options['renderType'] = 'fieldInformation';
|
||||
$options['renderData']['fieldInformation'] = $fieldInformation;
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge field control configuration with default controls and render them.
|
||||
*
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderFieldControl(): array
|
||||
{
|
||||
$options = $this->data;
|
||||
$fieldControl = $this->defaultFieldControl;
|
||||
$currentRenderType = $this->data['renderType'];
|
||||
$fieldControlFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldControl'] ?? [];
|
||||
ArrayUtility::mergeRecursiveWithOverrule($fieldControl, $fieldControlFromTca);
|
||||
$options['renderType'] = 'fieldControl';
|
||||
$options['renderData']['fieldControl'] = $fieldControl;
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge field wizard configuration with default wizards and render them.
|
||||
*
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderFieldWizard(): array
|
||||
{
|
||||
$options = $this->data;
|
||||
$fieldWizard = $this->defaultFieldWizard;
|
||||
$currentRenderType = $this->data['renderType'];
|
||||
$fieldWizardFromTca = $options['processedTca']['ctrl']['container'][$currentRenderType]['fieldWizard'] ?? [];
|
||||
ArrayUtility::mergeRecursiveWithOverrule($fieldWizard, $fieldWizardFromTca);
|
||||
$options['renderType'] = 'fieldWizard';
|
||||
$options['renderData']['fieldWizard'] = $fieldWizard;
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* A single field of TCA 'types' 'showitem' can have three semicolon separated configuration options:
|
||||
* fieldName: Name of the field to be found in TCA 'columns' section
|
||||
* fieldLabel: An alternative field label
|
||||
* paletteName: Name of a palette to be found in TCA 'palettes' section that is rendered after this field
|
||||
*
|
||||
* @param string $field Semicolon separated field configuration
|
||||
* @throws \RuntimeException
|
||||
* @return array
|
||||
*/
|
||||
protected function explodeSingleFieldShowItemConfiguration($field)
|
||||
{
|
||||
$fieldArray = GeneralUtility::trimExplode(';', $field);
|
||||
if (empty($fieldArray[0])) {
|
||||
throw new \RuntimeException('Field must not be empty', 1426448465);
|
||||
}
|
||||
return [
|
||||
'fieldName' => $fieldArray[0],
|
||||
'fieldLabel' => !empty($fieldArray[1]) ? $fieldArray[1] : null,
|
||||
'paletteName' => !empty($fieldArray[2]) ? $fieldArray[2] : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Render tabs with label and content. Used by TabsContainer and FlexFormTabsContainer.
|
||||
* Re-uses the template Tabs.html which is also used by ModuleTemplate.php.
|
||||
*
|
||||
* @param array $menuItems Tab elements, each element is an array with "label" and "content"
|
||||
* @param string $domId DOM id attribute, will be appended with an iteration number per tab.
|
||||
* @param int $defaultTabIndex
|
||||
* @return string
|
||||
*/
|
||||
protected function renderTabMenu(array $menuItems, $domId, $defaultTabIndex = 1)
|
||||
{
|
||||
$templatePath = ExtensionManagementUtility::extPath('backend')
|
||||
. 'Resources/Private/Templates/DocumentTemplate/';
|
||||
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplatePathAndFilename($templatePath . 'Tabs.html');
|
||||
$view->setPartialRootPaths([$templatePath . 'Partials']);
|
||||
$view->assignMultiple([
|
||||
'id' => $domId,
|
||||
'items' => $menuItems,
|
||||
'defaultTabIndex' => $defaultTabIndex,
|
||||
'wrapContent' => false,
|
||||
'storeLastActiveTab' => true,
|
||||
]);
|
||||
return $view->render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Flex form container implementation
|
||||
* This one is called by FlexFormSectionContainer and renders HTML for a single container.
|
||||
* For processing of single elements FlexFormElementContainer is called
|
||||
*/
|
||||
class FlexFormContainerContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$flexFormFormPrefix = $this->data['flexFormFormPrefix'];
|
||||
$flexFormContainerElementCollapsed = $this->data['flexFormContainerElementCollapsed'];
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$toggleIcons = '<span class="t3js-flex-control-toggle-icon-open" style="' . ($flexFormContainerElementCollapsed ? 'display: none;' : '') . '">'
|
||||
. $iconFactory->getIcon('actions-view-list-collapse', Icon::SIZE_SMALL)->render()
|
||||
. '</span>';
|
||||
$toggleIcons .= '<span class="t3js-flex-control-toggle-icon-close" style="' . ($flexFormContainerElementCollapsed ? '' : 'display: none;') . '">'
|
||||
. $iconFactory->getIcon('actions-view-list-expand', Icon::SIZE_SMALL)->render()
|
||||
. '</span>';
|
||||
|
||||
$flexFormContainerIdentifier = $this->data['flexFormContainerIdentifier'];
|
||||
$actionFieldName = '_ACTION_FLEX_FORM'
|
||||
. $parameterArray['itemFormElName']
|
||||
. $this->data['flexFormFormPrefix']
|
||||
. '[_ACTION]'
|
||||
. '[' . $flexFormContainerIdentifier . ']';
|
||||
$toggleFieldName = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']'
|
||||
. $flexFormFormPrefix
|
||||
. '[' . $flexFormContainerIdentifier . ']'
|
||||
. '[_TOGGLE]';
|
||||
|
||||
$moveAndDeleteContent = [];
|
||||
$userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
|
||||
if ($userHasAccessToDefaultLanguage) {
|
||||
$moveAndDeleteContent[] = '<span class="btn btn-default t3js-sortable-handle"><span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:sortable.dragmove')) . '">' . $iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '</span></span>';
|
||||
$moveAndDeleteContent[] = '<span class="btn btn-default t3js-delete"><span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete')) . '">' . $iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</span></span>';
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
// Append container specific stuff to field prefix
|
||||
$options['flexFormFormPrefix'] = $flexFormFormPrefix . '[' . $flexFormContainerIdentifier . '][' . $this->data['flexFormContainerName'] . '][el]';
|
||||
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray['el'];
|
||||
$options['renderType'] = 'flexFormElementContainer';
|
||||
$containerContentResult = $this->nodeFactory->create($options)->render();
|
||||
|
||||
$containerTitle = '';
|
||||
if (!empty(trim($flexFormDataStructureArray['title']))) {
|
||||
$containerTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
|
||||
}
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$html = [];
|
||||
$html[] = '<div class="t3-form-field-container-flexsections t3-flex-section t3js-flex-section">';
|
||||
$html[] = '<input class="t3-flex-control t3js-flex-control-action" type="hidden" name="' . htmlspecialchars($actionFieldName) . '" value="" />';
|
||||
$html[] = '<div class="panel panel-default panel-condensed">';
|
||||
$html[] = '<div class="panel-heading t3js-flex-section-header" data-toggle="formengine-flex">';
|
||||
$html[] = '<div class="form-irre-header">';
|
||||
$html[] = '<div class="form-irre-header-cell form-irre-header-icon">';
|
||||
$html[] = $toggleIcons;
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div class="form-irre-header-cell form-irre-header-body">';
|
||||
$html[] = '<span class="t3js-record-title">' . htmlspecialchars($containerTitle) . '</span>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div class="form-irre-header-cell form-irre-header-control">';
|
||||
$html[] = '<div class="btn-group btn-group-sm">';
|
||||
$html[] = implode(LF, $moveAndDeleteContent);
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div class="panel-collapse t3js-flex-section-content"' . ($flexFormContainerElementCollapsed ? ' style="display:none;"' : '') . '>';
|
||||
$html[] = $containerContentResult['html'];
|
||||
$html[] = '</div>';
|
||||
$html[] = '<input';
|
||||
$html[] = 'class="t3-flex-control t3js-flex-control-toggle"';
|
||||
$html[] = 'type="hidden"';
|
||||
$html[] = 'name="' . htmlspecialchars($toggleFieldName) . '"';
|
||||
$html[] = 'value="' . ($flexFormContainerElementCollapsed ? '1' : '0') . '"';
|
||||
$html[] = '/>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
|
||||
$resultArray['html'] = implode(LF, $html);
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $containerContentResult, false);
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
|
||||
|
||||
/**
|
||||
* The container handles single elements.
|
||||
*
|
||||
* This one is called by FlexFormTabsContainer, FlexFormNoTabsContainer or FlexFormContainerContainer.
|
||||
* For single fields, the code is similar to SingleFieldContainer, processing will end up in single
|
||||
* element classes depending on specific renderType of an element. Additionally, it determines if a
|
||||
* section is handled and hands over to FlexFormSectionContainer in this case.
|
||||
*/
|
||||
class FlexFormElementContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
$flexFormFormPrefix = $this->data['flexFormFormPrefix'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
$languageService = $this->getLanguageService();
|
||||
$resultArray = $this->initializeResultArray();
|
||||
foreach ($flexFormDataStructureArray as $flexFormFieldName => $flexFormFieldArray) {
|
||||
if (
|
||||
// No item array found at all
|
||||
!is_array($flexFormFieldArray)
|
||||
// Not a section or container and not a list of single items
|
||||
|| (!isset($flexFormFieldArray['type']) && !is_array($flexFormFieldArray['config']))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($flexFormFieldArray['type'] === 'array') {
|
||||
// Section
|
||||
if (empty($flexFormFieldArray['section'])) {
|
||||
$resultArray['html'] = LF . 'Section expected at ' . $flexFormFieldName . ' but not found';
|
||||
continue;
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureArray'] = $flexFormFieldArray;
|
||||
$options['flexFormRowData'] = $flexFormRowData[$flexFormFieldName]['el'] ?? [];
|
||||
$options['flexFormFieldName'] = $flexFormFieldName;
|
||||
$options['renderType'] = 'flexFormSectionContainer';
|
||||
$sectionContainerResult = $this->nodeFactory->create($options)->render();
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $sectionContainerResult);
|
||||
} else {
|
||||
// Set up options for single element
|
||||
$fakeParameterArray = [
|
||||
'fieldConf' => [
|
||||
'label' => $languageService->sL(trim($flexFormFieldArray['label'])),
|
||||
'config' => $flexFormFieldArray['config'],
|
||||
'children' => $flexFormFieldArray['children'],
|
||||
'onChange' => $flexFormFieldArray['onChange'],
|
||||
],
|
||||
'fieldChangeFunc' => $parameterArray['fieldChangeFunc'],
|
||||
'label' => $parameterArray['label'],
|
||||
];
|
||||
|
||||
if (isset($flexFormFieldArray['description']) && !empty($flexFormFieldArray['description'])) {
|
||||
$fakeParameterArray['fieldConf']['description'] = $flexFormFieldArray['description'];
|
||||
}
|
||||
|
||||
$alertMsgOnChange = '';
|
||||
if (isset($fakeParameterArray['fieldConf']['onChange']) && $fakeParameterArray['fieldConf']['onChange'] === 'reload') {
|
||||
if ($this->getBackendUserAuthentication()->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
|
||||
$alertMsgOnChange = 'top.TYPO3.Modal.confirm('
|
||||
. 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
|
||||
. ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
|
||||
. ')'
|
||||
. '.on('
|
||||
. '"button.clicked",'
|
||||
. ' function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); }'
|
||||
. ');';
|
||||
} else {
|
||||
$alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm();}';
|
||||
}
|
||||
}
|
||||
if ($alertMsgOnChange) {
|
||||
$fakeParameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
|
||||
}
|
||||
|
||||
$originalFieldName = $parameterArray['itemFormElName'];
|
||||
$fakeParameterArray['itemFormElName'] = $parameterArray['itemFormElName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
|
||||
if ($fakeParameterArray['itemFormElName'] !== $originalFieldName) {
|
||||
// If calculated itemFormElName is different from originalFieldName
|
||||
// change the originalFieldName in TBE_EDITOR_fieldChanged. This is
|
||||
// especially relevant for wizards writing their content back to hidden fields
|
||||
if (!empty($fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'])) {
|
||||
$fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = str_replace($originalFieldName, $fakeParameterArray['itemFormElName'], $fakeParameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged']);
|
||||
}
|
||||
}
|
||||
$fakeParameterArray['itemFormElID'] = $parameterArray['itemFormElID'] . '_' . preg_replace('/[^a-zA-Z0-9_-]/', '_', $flexFormFieldName) . '_' . md5($fakeParameterArray['itemFormElName']);
|
||||
if (isset($flexFormRowData[$flexFormFieldName]['vDEF'])) {
|
||||
$fakeParameterArray['itemFormElValue'] = $flexFormRowData[$flexFormFieldName]['vDEF'];
|
||||
} else {
|
||||
$fakeParameterArray['itemFormElValue'] = $fakeParameterArray['fieldConf']['config']['default'];
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
// Set either flexFormFieldName or flexFormContainerFieldName, depending on if we are a "regular" field or a flex container section field
|
||||
if (empty($options['flexFormFieldName'])) {
|
||||
$options['flexFormFieldName'] = $flexFormFieldName;
|
||||
} else {
|
||||
$options['flexFormContainerFieldName'] = $flexFormFieldName;
|
||||
}
|
||||
$options['parameterArray'] = $fakeParameterArray;
|
||||
$options['elementBaseName'] = $this->data['elementBaseName'] . $flexFormFormPrefix . '[' . $flexFormFieldName . '][vDEF]';
|
||||
|
||||
if (!empty($flexFormFieldArray['config']['renderType'])) {
|
||||
$options['renderType'] = $flexFormFieldArray['config']['renderType'];
|
||||
} else {
|
||||
// Fallback to type if no renderType is given
|
||||
$options['renderType'] = $flexFormFieldArray['config']['type'];
|
||||
}
|
||||
$childResult = $this->nodeFactory->create($options)->render();
|
||||
|
||||
if (!empty($childResult['html'])) {
|
||||
// Possible line breaks in the label through xml: \n => <br/>, usage of nl2br() not possible, so it's done through str_replace (?!)
|
||||
$processedTitle = str_replace('\\n', '<br />', htmlspecialchars($fakeParameterArray['fieldConf']['label']));
|
||||
$html = [];
|
||||
$html[] = '<div class="form-section">';
|
||||
$html[] = '<div class="form-group t3js-formengine-palette-field t3js-formengine-validation-marker">';
|
||||
$html[] = '<label class="t3js-formengine-label">';
|
||||
$html[] = BackendUtility::wrapInHelp($parameterArray['_cshKey'], $flexFormFieldName, $processedTitle);
|
||||
$html[] = '</label>';
|
||||
$html[] = '<div class="formengine-field-item t3js-formengine-field-item">';
|
||||
$html[] = $childResult['html'];
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$resultArray['html'] .= implode(LF, $html);
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Entry container to a flex form element. This container is created by
|
||||
* SingleFieldContainer if a type='flex' field is rendered.
|
||||
*
|
||||
* It either forks a FlexFormTabsContainer or a FlexFormNoTabsContainer.
|
||||
*/
|
||||
class FlexFormEntryContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$flexFormDataStructureIdentifier = $this->data['parameterArray']['fieldConf']['config']['dataStructureIdentifier'];
|
||||
$flexFormDataStructureArray = $this->data['parameterArray']['fieldConf']['config']['ds'];
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureIdentifier'] = $flexFormDataStructureIdentifier;
|
||||
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray;
|
||||
$options['flexFormRowData'] = $this->data['parameterArray']['itemFormElValue'];
|
||||
$options['renderType'] = 'flexFormNoTabsContainer';
|
||||
|
||||
// Enable tabs if there is more than one sheet
|
||||
if (count($flexFormDataStructureArray['sheets']) > 1) {
|
||||
$options['renderType'] = 'flexFormTabsContainer';
|
||||
}
|
||||
|
||||
$resultArray = $this->nodeFactory->create($options)->render();
|
||||
$resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/FormEngineFlexForm';
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle a flex form that has no tabs.
|
||||
*
|
||||
* This container is called by FlexFormEntryContainer if only a default sheet
|
||||
* exists. It evaluates the display condition and hands over rendering of single
|
||||
* fields to FlexFormElementContainer.
|
||||
*/
|
||||
class FlexFormNoTabsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Default field information enabled for this element.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldInformation = [
|
||||
'tcaDescription' => [
|
||||
'renderType' => 'tcaDescription',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$fieldName = $this->data['fieldName']; // field name of the flex form field in DB
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Determine this single sheet name, most often it ends up with sDEF, except if only one sheet was defined
|
||||
$flexFormSheetNames = array_keys($flexFormDataStructureArray['sheets']);
|
||||
$sheetName = array_pop($flexFormSheetNames);
|
||||
$flexFormRowDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?: [];
|
||||
|
||||
unset($flexFormDataStructureArray['meta']);
|
||||
|
||||
if (!is_array($flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'])) {
|
||||
$resultArray['html'] = 'Data Structure ERROR: No [\'ROOT\'][\'el\'] element found in flex form definition.';
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
// Assemble key for loading the correct CSH file
|
||||
// @todo: what is that good for? That is for the title of single elements ... see FlexFormElementContainer!
|
||||
$dsPointerFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['ds_pointerField'], true);
|
||||
$parameterArray['_cshKey'] = $table . '.' . $fieldName;
|
||||
foreach ($dsPointerFields as $key) {
|
||||
if (is_string($row[$key]) && $row[$key] !== '') {
|
||||
$parameterArray['_cshKey'] .= '.' . $row[$key];
|
||||
} elseif (is_array($row[$key]) && isset($row[$key][0]) && is_string($row[$key][0]) && $row[$key][0] !== '') {
|
||||
$parameterArray['_cshKey'] .= '.' . $row[$key][0];
|
||||
}
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureArray'] = $flexFormDataStructureArray['sheets'][$sheetName]['ROOT']['el'];
|
||||
$options['flexFormRowData'] = $flexFormRowDataSubPart;
|
||||
$options['flexFormSheetName'] = $sheetName;
|
||||
$options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
|
||||
$options['parameterArray'] = $parameterArray;
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$resultArray['html'] = '<div>' . $fieldInformationResult['html'] . '</div>';
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
$options['renderType'] = 'flexFormElementContainer';
|
||||
$childResult = $this->nodeFactory->create($options)->render();
|
||||
return $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle flex form sections.
|
||||
*
|
||||
* This container is created by FlexFormElementContainer if a "single" element is in
|
||||
* fact a section. For each existing section container it creates as FlexFormContainerContainer
|
||||
* to render its inner fields.
|
||||
* Additionally, a button for each possible container is rendered with information for the
|
||||
* ajax controller that fetches one on click.
|
||||
*/
|
||||
class FlexFormSectionContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$flexFormDataStructureIdentifier = $this->data['flexFormDataStructureIdentifier'];
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
$flexFormFieldName = $this->data['flexFormFieldName'];
|
||||
$flexFormSheetName = $this->data['flexFormSheetName'];
|
||||
|
||||
$userHasAccessToDefaultLanguage = $this->getBackendUserAuthentication()->checkLanguageAccess(0);
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Render each existing container
|
||||
foreach ($flexFormDataStructureArray['children'] as $flexFormContainerIdentifier => $containerDataStructure) {
|
||||
$existingContainerData = $flexFormRowData[$flexFormContainerIdentifier];
|
||||
// @todo: This relies on the fact that "_TOGGLE" is *below* the real data in the saved xml structure
|
||||
$existingSectionContainerDataStructureType = key($existingContainerData);
|
||||
$existingContainerData = $existingContainerData[$existingSectionContainerDataStructureType];
|
||||
$options = $this->data;
|
||||
$options['flexFormRowData'] = $existingContainerData['el'];
|
||||
$options['flexFormDataStructureArray'] = $containerDataStructure;
|
||||
$options['flexFormFormPrefix'] = $this->data['flexFormFormPrefix'] . '[' . $flexFormFieldName . ']' . '[el]';
|
||||
$options['flexFormContainerName'] = $existingSectionContainerDataStructureType;
|
||||
$options['flexFormContainerIdentifier'] = $flexFormContainerIdentifier;
|
||||
$options['flexFormContainerElementCollapsed'] = (bool)$flexFormRowData[$flexFormContainerIdentifier]['_TOGGLE'];
|
||||
$options['renderType'] = 'flexFormContainerContainer';
|
||||
$flexFormContainerContainerResult = $this->nodeFactory->create($options)->render();
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $flexFormContainerContainerResult);
|
||||
}
|
||||
|
||||
// "New container" handling: Creates buttons for each possible container with all relevant information for the ajax call.
|
||||
$containerTemplatesHtml = [];
|
||||
foreach ($flexFormDataStructureArray['el'] as $flexFormContainerName => $flexFormFieldDefinition) {
|
||||
$containerTitle = '';
|
||||
if (!empty(trim($flexFormFieldDefinition['title']))) {
|
||||
$containerTitle = $languageService->sL(trim($flexFormFieldDefinition['title']));
|
||||
}
|
||||
$containerTemplateHtml = [];
|
||||
$containerTemplateHtml[] = '<a';
|
||||
$containerTemplateHtml[] = 'href="#"';
|
||||
$containerTemplateHtml[] = 'class="btn btn-default t3js-flex-container-add"';
|
||||
$containerTemplateHtml[] = 'data-vanillauid="' . (int)$this->data['vanillaUid'] . '"';
|
||||
// no int cast for databaseRow uid, this can be "NEW1234..."
|
||||
$containerTemplateHtml[] = 'data-databaserowuid="' . htmlspecialchars($this->data['databaseRow']['uid']) . '"';
|
||||
$containerTemplateHtml[] = 'data-command="' . htmlspecialchars($this->data['command']) . '"';
|
||||
$containerTemplateHtml[] = 'data-tablename="' . htmlspecialchars($this->data['tableName']) . '"';
|
||||
$containerTemplateHtml[] = 'data-fieldname="' . htmlspecialchars($this->data['fieldName']) . '"';
|
||||
$containerTemplateHtml[] = 'data-recordtypevalue="' . $this->data['recordTypeValue'] . '"';
|
||||
$containerTemplateHtml[] = 'data-datastructureidentifier="' . htmlspecialchars($flexFormDataStructureIdentifier) . '"';
|
||||
$containerTemplateHtml[] = 'data-flexformsheetname="' . htmlspecialchars($flexFormSheetName) . '"';
|
||||
$containerTemplateHtml[] = 'data-flexformfieldname="' . htmlspecialchars($flexFormFieldName) . '"';
|
||||
$containerTemplateHtml[] = 'data-flexformcontainername="' . htmlspecialchars($flexFormContainerName) . '"';
|
||||
$containerTemplateHtml[] = '>';
|
||||
$containerTemplateHtml[] = $iconFactory->getIcon('actions-document-new', Icon::SIZE_SMALL)->render();
|
||||
$containerTemplateHtml[] = htmlspecialchars(GeneralUtility::fixed_lgd_cs($containerTitle, 30));
|
||||
$containerTemplateHtml[] = '</a>';
|
||||
$containerTemplatesHtml[] = implode(LF, $containerTemplateHtml);
|
||||
}
|
||||
// Create new elements links
|
||||
$createElementsHtml = [];
|
||||
if ($userHasAccessToDefaultLanguage) {
|
||||
$createElementsHtml[] = '<div class="t3-form-field-add-flexsection">';
|
||||
$createElementsHtml[] = '<div class="btn-group">';
|
||||
$createElementsHtml[] = implode('', $containerTemplatesHtml);
|
||||
$createElementsHtml[] = '</div>';
|
||||
$createElementsHtml[] = '</div>';
|
||||
}
|
||||
|
||||
$sectionTitle = '';
|
||||
if (!empty(trim($flexFormDataStructureArray['title']))) {
|
||||
$sectionTitle = $languageService->sL(trim($flexFormDataStructureArray['title']));
|
||||
}
|
||||
|
||||
// Wrap child stuff
|
||||
$toggleAll = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.toggleall'));
|
||||
$html = [];
|
||||
$html[] = '<div class="panel panel-tab">';
|
||||
$html[] = '<div class="panel-body">';
|
||||
$html[] = '<div class="t3-form-field-container t3-form-flex">';
|
||||
$html[] = '<div class="t3-form-field-label-flexsection">';
|
||||
$html[] = '<h4>';
|
||||
$html[] = htmlspecialchars($sectionTitle);
|
||||
$html[] = '</h4>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div class="t3js-form-field-toggle-flexsection t3-form-flexsection-toggle">';
|
||||
$html[] = '<a class="btn btn-default" href="#" title="' . $toggleAll . '">';
|
||||
$html[] = $iconFactory->getIcon('actions-move-right', Icon::SIZE_SMALL)->render() . $toggleAll;
|
||||
$html[] = '</a>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '<div';
|
||||
$html[] = 'class="panel-group panel-hover t3-form-field-container-flexsection t3-flex-container"';
|
||||
$html[] = 'data-t3-flex-allow-restructure="' . ($userHasAccessToDefaultLanguage ? '1' : '0') . '"';
|
||||
$html[] = '>';
|
||||
$html[] = $resultArray['html'];
|
||||
$html[] = '</div>';
|
||||
$html[] = implode(LF, $createElementsHtml);
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
$html[] = '</div>';
|
||||
|
||||
$resultArray['html'] = implode(LF, $html);
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle flex forms that have tabs (multiple "sheets").
|
||||
*
|
||||
* This container is called by FlexFormEntryContainer. It resolves each
|
||||
* sheet and hands rendering of single sheet content over to FlexFormElementContainer.
|
||||
*/
|
||||
class FlexFormTabsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Default field information enabled for this element.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldInformation = [
|
||||
'tcaDescription' => [
|
||||
'renderType' => 'tcaDescription',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$fieldName = $this->data['fieldName']; // field name of the flex form field in DB
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
$flexFormDataStructureArray = $this->data['flexFormDataStructureArray'];
|
||||
$flexFormRowData = $this->data['flexFormRowData'];
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$resultArray['requireJsModules'][] = 'TYPO3/CMS/Backend/Tabs';
|
||||
|
||||
$domIdPrefix = 'DTM-' . GeneralUtility::shortMD5($this->data['parameterArray']['itemFormElName']);
|
||||
$tabCounter = 0;
|
||||
$tabElements = [];
|
||||
foreach ($flexFormDataStructureArray['sheets'] as $sheetName => $sheetDataStructure) {
|
||||
$flexFormRowSheetDataSubPart = $flexFormRowData['data'][$sheetName]['lDEF'] ?: [];
|
||||
|
||||
if (!is_array($sheetDataStructure['ROOT']['el'])) {
|
||||
$resultArray['html'] .= LF . 'No Data Structure ERROR: No [\'ROOT\'][\'el\'] found for sheet "' . $sheetName . '".';
|
||||
continue;
|
||||
}
|
||||
|
||||
$tabCounter++;
|
||||
|
||||
// Assemble key for loading the correct CSH file
|
||||
// @todo: what is that good for? That is for the title of single elements ... see FlexFormElementContainer!
|
||||
$dsPointerFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['columns'][$fieldName]['config']['ds_pointerField'], true);
|
||||
$parameterArray['_cshKey'] = $table . '.' . $fieldName;
|
||||
foreach ($dsPointerFields as $key) {
|
||||
if (is_string($row[$key]) && $row[$key] !== '') {
|
||||
$parameterArray['_cshKey'] .= '.' . $row[$key];
|
||||
} elseif (is_array($row[$key]) && isset($row[$key][0]) && is_string($row[$key][0]) && $row[$key][0] !== '') {
|
||||
$parameterArray['_cshKey'] .= '.' . $row[$key][0];
|
||||
}
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
$options['flexFormDataStructureArray'] = $sheetDataStructure['ROOT']['el'];
|
||||
$options['flexFormRowData'] = $flexFormRowSheetDataSubPart;
|
||||
$options['flexFormSheetName'] = $sheetName;
|
||||
$options['flexFormFormPrefix'] = '[data][' . $sheetName . '][lDEF]';
|
||||
$options['parameterArray'] = $parameterArray;
|
||||
// Merge elements of this tab into a single list again and hand over to
|
||||
// palette and single field container to render this group
|
||||
$options['tabAndInlineStack'][] = [
|
||||
'tab',
|
||||
$domIdPrefix . '-' . $tabCounter,
|
||||
];
|
||||
$options['renderType'] = 'flexFormElementContainer';
|
||||
$childReturn = $this->nodeFactory->create($options)->render();
|
||||
|
||||
if ($childReturn['html'] !== '') {
|
||||
$tabElements[] = [
|
||||
'label' => !empty(trim($sheetDataStructure['ROOT']['sheetTitle'])) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetTitle'])) : $sheetName,
|
||||
'content' => $childReturn['html'],
|
||||
'description' => trim($sheetDataStructure['ROOT']['sheetDescription']) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetDescription'])) : '',
|
||||
'linkTitle' => trim($sheetDataStructure['ROOT']['sheetShortDescr']) ? $languageService->sL(trim($sheetDataStructure['ROOT']['sheetShortDescr'])) : '',
|
||||
];
|
||||
}
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childReturn, false);
|
||||
}
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$resultArray['html'] = '<div>' . $fieldInformationResult['html'] . '</div>';
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
$resultArray['html'] .= $this->renderTabMenu($tabElements, $domIdPrefix);
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* A container rendering a "full record". This is an entry container used as first
|
||||
* step into the rendering tree..
|
||||
*
|
||||
* This container determines the to be rendered fields depending on the record type,
|
||||
* initializes possible language base data, finds out if tabs should be rendered and
|
||||
* then calls either TabsContainer or a NoTabsContainer for further processing.
|
||||
*/
|
||||
class FullRecordContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$table = $this->data['tableName'];
|
||||
$recordTypeValue = $this->data['recordTypeValue'];
|
||||
|
||||
// Load the description content for the table if requested
|
||||
if (!empty($this->data['processedTca']['interface']['always_description'])) {
|
||||
$languageService = $this->getLanguageService();
|
||||
$languageService->loadSingleTableDescription($table);
|
||||
}
|
||||
|
||||
// List of items to be rendered
|
||||
$itemList = $this->data['processedTca']['types'][$recordTypeValue]['showitem'];
|
||||
|
||||
$fieldsArray = GeneralUtility::trimExplode(',', $itemList, true);
|
||||
|
||||
// Streamline the fields array
|
||||
// First, make sure there is always a --div-- definition for the first element
|
||||
if (strpos($fieldsArray[0], '--div--') !== 0) {
|
||||
array_unshift($fieldsArray, '--div--;LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.generalTab');
|
||||
}
|
||||
// If first tab has no label definition, add "general" label
|
||||
$firstTabHasLabel = count(GeneralUtility::trimExplode(';', $fieldsArray[0])) > 1;
|
||||
if (!$firstTabHasLabel) {
|
||||
$fieldsArray[0] = '--div--;LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.generalTab';
|
||||
}
|
||||
// If there are at least two --div-- definitions, inner container will be a TabContainer, else a NoTabContainer
|
||||
$tabCount = 0;
|
||||
foreach ($fieldsArray as $field) {
|
||||
if (strpos($field, '--div--') === 0) {
|
||||
$tabCount++;
|
||||
}
|
||||
}
|
||||
$hasTabs = true;
|
||||
if ($tabCount < 2) {
|
||||
// Remove first tab definition again if there is only one tab defined
|
||||
array_shift($fieldsArray);
|
||||
$hasTabs = false;
|
||||
}
|
||||
|
||||
$data = $this->data;
|
||||
$data['fieldsArray'] = $fieldsArray;
|
||||
if ($hasTabs) {
|
||||
$data['renderType'] = 'tabsContainer';
|
||||
} else {
|
||||
$data['renderType'] = 'noTabsContainer';
|
||||
}
|
||||
|
||||
return $this->nodeFactory->create($data)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\Folder;
|
||||
use TYPO3\CMS\Core\Resource\OnlineMedia\Helpers\OnlineMediaHelperRegistry;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
use TYPO3\CMS\Core\Utility\StringUtility;
|
||||
|
||||
/**
|
||||
* Inline element entry container.
|
||||
*
|
||||
* This container is the entry step to rendering an inline element. It is created by SingleFieldContainer.
|
||||
*
|
||||
* The code creates the main structure for the single inline elements, initializes
|
||||
* the inlineData array, that is manipulated and also returned back in its manipulated state.
|
||||
* The "control" stuff of inline elements is rendered here, for example the "create new" button.
|
||||
*
|
||||
* For each existing inline relation an InlineRecordContainer is called for further processing.
|
||||
*/
|
||||
class InlineControlContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Inline data array used in JS, returned as JSON object to frontend
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $inlineData = [];
|
||||
|
||||
/**
|
||||
* @var InlineStackProcessor
|
||||
*/
|
||||
protected $inlineStackProcessor;
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected $requireJsModules = [];
|
||||
|
||||
/**
|
||||
* Default field information enabled for this element.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $defaultFieldInformation = [
|
||||
'tcaDescription' => [
|
||||
'renderType' => 'tcaDescription',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array Default wizards
|
||||
*/
|
||||
protected $defaultFieldWizard = [
|
||||
'localizationStateSelector' => [
|
||||
'renderType' => 'localizationStateSelector',
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Container objects give $nodeFactory down to other containers.
|
||||
*
|
||||
* @param NodeFactory $nodeFactory
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(NodeFactory $nodeFactory, array $data)
|
||||
{
|
||||
parent::__construct($nodeFactory, $data);
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$this->inlineData = $this->data['inlineData'];
|
||||
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$this->inlineStackProcessor = $inlineStackProcessor;
|
||||
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$field = $this->data['fieldName'];
|
||||
$parameterArray = $this->data['parameterArray'];
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$config = $parameterArray['fieldConf']['config'];
|
||||
$foreign_table = $config['foreign_table'];
|
||||
$isReadOnly = isset($config['readOnly']) && $config['readOnly'];
|
||||
$language = 0;
|
||||
$languageFieldName = $GLOBALS['TCA'][$table]['ctrl']['languageField'];
|
||||
if (BackendUtility::isTableLocalizable($table)) {
|
||||
$language = isset($row[$languageFieldName][0]) ? (int)$row[$languageFieldName][0] : (int)$row[$languageFieldName];
|
||||
}
|
||||
|
||||
// Add the current inline job to the structure stack
|
||||
$newStructureItem = [
|
||||
'table' => $table,
|
||||
'uid' => $row['uid'],
|
||||
'field' => $field,
|
||||
'config' => $config,
|
||||
];
|
||||
// Extract FlexForm parts (if any) from element name, e.g. array('vDEF', 'lDEF', 'FlexField', 'vDEF')
|
||||
if (!empty($parameterArray['itemFormElName'])) {
|
||||
$flexFormParts = $this->extractFlexFormParts($parameterArray['itemFormElName']);
|
||||
if ($flexFormParts !== null) {
|
||||
$newStructureItem['flexform'] = $flexFormParts;
|
||||
}
|
||||
}
|
||||
$inlineStackProcessor->pushStableStructureItem($newStructureItem);
|
||||
|
||||
// Transport the flexform DS identifier fields to the FormInlineAjaxController
|
||||
if (!empty($newStructureItem['flexform'])
|
||||
&& isset($this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'])
|
||||
) {
|
||||
$config['dataStructureIdentifier'] = $this->data['processedTca']['columns'][$field]['config']['dataStructureIdentifier'];
|
||||
}
|
||||
|
||||
// Hand over original returnUrl to FormInlineAjaxController. Needed if opening for instance a
|
||||
// nested element in a new view to then go back to the original returnUrl and not the url of
|
||||
// the inline ajax controller
|
||||
$config['originalReturnUrl'] = $this->data['returnUrl'];
|
||||
|
||||
// e.g. data[<table>][<uid>][<field>]
|
||||
$nameForm = $inlineStackProcessor->getCurrentStructureFormPrefix();
|
||||
// e.g. data-<pid>-<table1>-<uid1>-<field1>-<table2>-<uid2>-<field2>
|
||||
$nameObject = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
|
||||
|
||||
$config['inline']['first'] = false;
|
||||
$firstChild = reset($this->data['parameterArray']['fieldConf']['children']);
|
||||
if (isset($firstChild['databaseRow']['uid'])) {
|
||||
$config['inline']['first'] = $firstChild['databaseRow']['uid'];
|
||||
}
|
||||
$config['inline']['last'] = false;
|
||||
$lastChild = end($this->data['parameterArray']['fieldConf']['children']);
|
||||
if (isset($lastChild['databaseRow']['uid'])) {
|
||||
$config['inline']['last'] = $lastChild['databaseRow']['uid'];
|
||||
}
|
||||
|
||||
$top = $inlineStackProcessor->getStructureLevel(0);
|
||||
|
||||
$this->inlineData['config'][$nameObject] = [
|
||||
'table' => $foreign_table,
|
||||
'md5' => md5($nameObject)
|
||||
];
|
||||
$configJson = json_encode($config);
|
||||
$this->inlineData['config'][$nameObject . '-' . $foreign_table] = [
|
||||
'min' => $config['minitems'],
|
||||
'max' => $config['maxitems'],
|
||||
'sortable' => $config['appearance']['useSortable'],
|
||||
'top' => [
|
||||
'table' => $top['table'],
|
||||
'uid' => $top['uid']
|
||||
],
|
||||
'context' => [
|
||||
'config' => $configJson,
|
||||
'hmac' => GeneralUtility::hmac($configJson, 'InlineContext'),
|
||||
],
|
||||
];
|
||||
$this->inlineData['nested'][$nameObject] = $this->data['tabAndInlineStack'];
|
||||
|
||||
$uniqueMax = 0;
|
||||
$uniqueIds = [];
|
||||
|
||||
if ($config['foreign_unique']) {
|
||||
// Add inlineData['unique'] with JS unique configuration
|
||||
// @todo: Improve validation and throw an exception if type is neither select nor group here
|
||||
$type = $config['selectorOrUniqueConfiguration']['config']['type'] === 'select' ? 'select' : 'groupdb';
|
||||
foreach ($parameterArray['fieldConf']['children'] as $child) {
|
||||
// Determine used unique ids, skip not localized records
|
||||
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$value = $child['databaseRow'][$config['foreign_unique']];
|
||||
// We're assuming there is only one connected value here for both select and group
|
||||
if ($type === 'select') {
|
||||
// A select field is an array of uids. See TcaSelectItems data provider for details.
|
||||
// Pick first entry, ends up as eg. $value = 42.
|
||||
$value = $value['0'];
|
||||
} else {
|
||||
// A group field is an array of arrays containing uid + table + title + row.
|
||||
// See TcaGroup data provider for details.
|
||||
// Pick the first one (always on 0), and use uid + table only. Exclude title + row
|
||||
// since the entire inlineData['unique'] array ends up in JavaScript in the end
|
||||
// and we don't need and want the title and the entire row data in the frontend.
|
||||
// Ends up as $value = [ 'uid' => '42', 'table' => 'tx_my_table' ]
|
||||
$value = [
|
||||
'uid' => $value[0]['uid'],
|
||||
'table' => $value[0]['table'],
|
||||
];
|
||||
}
|
||||
// Note structure of $value is different in select vs. group: It's a uid for select, but an
|
||||
// array with uid + table for group. This is handled differently on JavaScript side, search
|
||||
// for 'groupdb' in jsfunc.inline.js for details.
|
||||
$uniqueIds[$child['databaseRow']['uid']] = $value;
|
||||
}
|
||||
}
|
||||
$possibleRecords = $config['selectorOrUniquePossibleRecords'];
|
||||
$possibleRecordsUidToTitle = [];
|
||||
foreach ($possibleRecords as $possibleRecord) {
|
||||
$possibleRecordsUidToTitle[$possibleRecord[1]] = $possibleRecord[0];
|
||||
}
|
||||
$uniqueMax = $config['appearance']['useCombination'] || empty($possibleRecords) ? -1 : count($possibleRecords);
|
||||
$this->inlineData['unique'][$nameObject . '-' . $foreign_table] = [
|
||||
'max' => $uniqueMax,
|
||||
'used' => $uniqueIds,
|
||||
'type' => $type,
|
||||
'table' => $foreign_table,
|
||||
'elTable' => $config['selectorOrUniqueConfiguration']['foreignTable'],
|
||||
'field' => $config['foreign_unique'],
|
||||
'selector' => $config['selectorOrUniqueConfiguration']['isSelector'] ? $type : false,
|
||||
'possible' => $possibleRecordsUidToTitle,
|
||||
];
|
||||
}
|
||||
|
||||
$resultArray['inlineData'] = $this->inlineData;
|
||||
|
||||
// @todo: It might be a good idea to have something like "isLocalizedRecord" or similar set by a data provider
|
||||
$uidOfDefaultRecord = $row[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
|
||||
$isLocalizedParent = $language > 0
|
||||
&& ($uidOfDefaultRecord[0] ?? $uidOfDefaultRecord) > 0
|
||||
&& MathUtility::canBeInterpretedAsInteger($row['uid']);
|
||||
$numberOfFullLocalizedChildren = 0;
|
||||
$numberOfNotYetLocalizedChildren = 0;
|
||||
foreach ($this->data['parameterArray']['fieldConf']['children'] as $child) {
|
||||
if (!$child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$numberOfFullLocalizedChildren++;
|
||||
}
|
||||
if ($isLocalizedParent && $child['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$numberOfNotYetLocalizedChildren++;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the localization links if needed
|
||||
$localizationLinks = '';
|
||||
if ($numberOfNotYetLocalizedChildren) {
|
||||
// Add the "Localize all records" link before all child records:
|
||||
if (isset($config['appearance']['showAllLocalizationLink']) && $config['appearance']['showAllLocalizationLink']) {
|
||||
$localizationLinks = ' ' . $this->getLevelInteractionLink('localize', $nameObject . '-' . $foreign_table, $config);
|
||||
}
|
||||
// Add the "Synchronize with default language" link before all child records:
|
||||
if (isset($config['appearance']['showSynchronizationLink']) && $config['appearance']['showSynchronizationLink']) {
|
||||
$localizationLinks .= ' ' . $this->getLevelInteractionLink('synchronize', $nameObject . '-' . $foreign_table, $config);
|
||||
}
|
||||
}
|
||||
|
||||
// Define how to show the "Create new record" link - if there are more than maxitems, hide it
|
||||
if ($isReadOnly || $numberOfFullLocalizedChildren >= $config['maxitems'] || ($uniqueMax > 0 && $numberOfFullLocalizedChildren >= $uniqueMax)) {
|
||||
$config['inline']['inlineNewButtonStyle'] = 'display: none;';
|
||||
$config['inline']['inlineNewRelationButtonStyle'] = 'display: none;';
|
||||
$config['inline']['inlineOnlineMediaAddButtonStyle'] = 'display: none;';
|
||||
}
|
||||
|
||||
// Render the level links (create new record):
|
||||
$levelLinks = '';
|
||||
if (!empty($config['appearance']['enabledControls']['new'])) {
|
||||
$levelLinks = $this->getLevelInteractionLink('newRecord', $nameObject . '-' . $foreign_table, $config);
|
||||
}
|
||||
// Wrap all inline fields of a record with a <div> (like a container)
|
||||
$html = '<div class="form-group" id="' . $nameObject . '">';
|
||||
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$html .= $fieldInformationResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldInformationResult, false);
|
||||
|
||||
// Add the level links before all child records:
|
||||
if ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'top') {
|
||||
$html .= '<div class="form-group t3js-formengine-validation-marker">' . $levelLinks . $localizationLinks . '</div>';
|
||||
}
|
||||
|
||||
// If it's required to select from possible child records (reusable children), add a selector box
|
||||
if (!$isReadOnly && $config['foreign_selector'] && $config['appearance']['showPossibleRecordsSelector'] !== false) {
|
||||
if ($config['selectorOrUniqueConfiguration']['config']['type'] === 'select') {
|
||||
$selectorBox = $this->renderPossibleRecordsSelectorTypeSelect($config, $uniqueIds);
|
||||
} else {
|
||||
$selectorBox = $this->renderPossibleRecordsSelectorTypeGroupDB($config);
|
||||
}
|
||||
$html .= $selectorBox . $localizationLinks;
|
||||
}
|
||||
|
||||
$title = $languageService->sL(trim($parameterArray['fieldConf']['label']));
|
||||
$html .= '<div class="panel-group panel-hover" data-title="' . htmlspecialchars($title) . '" id="' . $nameObject . '_records">';
|
||||
|
||||
$sortableRecordUids = [];
|
||||
foreach ($this->data['parameterArray']['fieldConf']['children'] as $options) {
|
||||
$options['inlineParentUid'] = $row['uid'];
|
||||
$options['inlineFirstPid'] = $this->data['inlineFirstPid'];
|
||||
// @todo: this can be removed if this container no longer sets additional info to $config
|
||||
$options['inlineParentConfig'] = $config;
|
||||
$options['inlineData'] = $this->inlineData;
|
||||
$options['inlineStructure'] = $inlineStackProcessor->getStructure();
|
||||
$options['inlineExpandCollapseStateArray'] = $this->data['inlineExpandCollapseStateArray'];
|
||||
$options['renderType'] = 'inlineRecordContainer';
|
||||
$childResult = $this->nodeFactory->create($options)->render();
|
||||
$html .= $childResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childResult, false);
|
||||
if (!$options['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
// Don't add record to list of "valid" uids if it is only the default
|
||||
// language record of a not yet localized child
|
||||
$sortableRecordUids[] = $options['databaseRow']['uid'];
|
||||
}
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
$fieldWizardResult = $this->renderFieldWizard();
|
||||
$fieldWizardHtml = $fieldWizardResult['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $fieldWizardResult, false);
|
||||
$html .= $fieldWizardHtml;
|
||||
|
||||
// Add the level links after all child records:
|
||||
if (!$isReadOnly && ($config['appearance']['levelLinksPosition'] === 'both' || $config['appearance']['levelLinksPosition'] === 'bottom')) {
|
||||
$html .= $levelLinks . $localizationLinks;
|
||||
}
|
||||
if (is_array($config['customControls'])) {
|
||||
$html .= '<div id="' . $nameObject . '_customControls">';
|
||||
foreach ($config['customControls'] as $customControlConfig) {
|
||||
if (!isset($customControlConfig['userFunc'])) {
|
||||
trigger_error('Support for customControl without a userFunc key will be removed in TYPO3 v10.0.', E_USER_DEPRECATED);
|
||||
$customControlConfig = [
|
||||
'userFunc' => $customControlConfig
|
||||
];
|
||||
}
|
||||
$parameters = [
|
||||
'table' => $table,
|
||||
'field' => $field,
|
||||
'row' => $row,
|
||||
'nameObject' => $nameObject,
|
||||
'nameForm' => $nameForm,
|
||||
'config' => $config,
|
||||
'customControlConfig' => $customControlConfig,
|
||||
];
|
||||
$html .= GeneralUtility::callUserFunction($customControlConfig['userFunc'], $parameters, $this);
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
// Add Drag&Drop functions for sorting to FormEngine::$additionalJS_post
|
||||
if (count($sortableRecordUids) > 1 && $config['appearance']['useSortable']) {
|
||||
$resultArray['additionalJavaScriptPost'][] = 'inline.createDragAndDropSorting("' . $nameObject . '_records' . '");';
|
||||
}
|
||||
$resultArray['requireJsModules'] = array_merge($resultArray['requireJsModules'], $this->requireJsModules);
|
||||
|
||||
// Publish the uids of the child records in the given order to the browser
|
||||
$html .= '<input type="hidden" name="' . $nameForm . '" value="' . implode(',', $sortableRecordUids) . '" '
|
||||
. ' data-formengine-validation-rules="' . htmlspecialchars($this->getValidationDataAsJsonString(['type' => 'inline', 'minitems' => $config['minitems'], 'maxitems' => $config['maxitems']])) . '"'
|
||||
. ' class="inlineRecord" />';
|
||||
// Close the wrap for all inline fields (container)
|
||||
$html .= '</div>';
|
||||
|
||||
$resultArray['html'] = $html;
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the HTML code of a general link to be used on a level of inline children.
|
||||
* The possible keys for the parameter $type are 'newRecord', 'localize' and 'synchronize'.
|
||||
*
|
||||
* @param string $type The link type, values are 'newRecord', 'localize' and 'synchronize'.
|
||||
* @param string $objectPrefix The "path" to the child record to create (e.g. 'data-parentPageId-partenTable-parentUid-parentField-childTable]')
|
||||
* @param array $conf TCA configuration of the parent(!) field
|
||||
* @return string The HTML code of the new link, wrapped in a div
|
||||
*/
|
||||
protected function getLevelInteractionLink($type, $objectPrefix, $conf = [])
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
|
||||
$attributes = [];
|
||||
switch ($type) {
|
||||
case 'newRecord':
|
||||
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew'));
|
||||
$icon = 'actions-add';
|
||||
$className = 'typo3-newRecordLink';
|
||||
$attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
|
||||
$attributes['onclick'] = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($objectPrefix) . ')';
|
||||
if (!empty($conf['inline']['inlineNewButtonStyle'])) {
|
||||
$attributes['style'] = $conf['inline']['inlineNewButtonStyle'];
|
||||
}
|
||||
if (!empty($conf['appearance']['newRecordLinkAddTitle'])) {
|
||||
$title = htmlspecialchars(sprintf(
|
||||
$languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createnew.link'),
|
||||
$languageService->sL($GLOBALS['TCA'][$conf['foreign_table']]['ctrl']['title'])
|
||||
));
|
||||
} elseif (isset($conf['appearance']['newRecordLinkTitle']) && $conf['appearance']['newRecordLinkTitle'] !== '') {
|
||||
$title = htmlspecialchars($languageService->sL($conf['appearance']['newRecordLinkTitle']));
|
||||
}
|
||||
break;
|
||||
case 'localize':
|
||||
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localizeAllRecords'));
|
||||
$icon = 'actions-document-localize';
|
||||
$className = 'typo3-localizationLink';
|
||||
$attributes['class'] = 'btn btn-default';
|
||||
$attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'localize\')';
|
||||
break;
|
||||
case 'synchronize':
|
||||
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:synchronizeWithOriginalLanguage'));
|
||||
$icon = 'actions-document-synchronize';
|
||||
$className = 'typo3-synchronizationLink';
|
||||
$attributes['class'] = 'btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'];
|
||||
$attributes['onclick'] = 'return inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($objectPrefix) . ', \'synchronize\')';
|
||||
break;
|
||||
default:
|
||||
$title = '';
|
||||
$icon = '';
|
||||
$className = '';
|
||||
}
|
||||
// Create the link:
|
||||
$icon = $icon ? $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() : '';
|
||||
$link = $this->wrapWithAnchor($icon . ' ' . $title, '#', $attributes);
|
||||
return '<div' . ($className ? ' class="' . $className . '"' : '') . 'title="' . $title . '">' . $link . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a text with an anchor and returns the HTML representation.
|
||||
*
|
||||
* @param string $text The text to be wrapped by an anchor
|
||||
* @param string $link The link to be used in the anchor
|
||||
* @param array $attributes Array of attributes to be used in the anchor
|
||||
* @return string The wrapped text as HTML representation
|
||||
*/
|
||||
protected function wrapWithAnchor($text, $link, $attributes = [])
|
||||
{
|
||||
$attributes['href'] = trim($link ?: '#');
|
||||
return '<a ' . GeneralUtility::implodeAttributes($attributes, true, true) . '>' . $text . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a link that opens an element browser in a new window.
|
||||
* For group/db there is no way to use a "selector" like a <select>|</select>-box.
|
||||
*
|
||||
* @param array $inlineConfiguration TCA inline configuration of the parent(!) field
|
||||
* @return string A HTML link that opens an element browser in a new window
|
||||
*/
|
||||
protected function renderPossibleRecordsSelectorTypeGroupDB(array $inlineConfiguration)
|
||||
{
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
$groupFieldConfiguration = $inlineConfiguration['selectorOrUniqueConfiguration']['config'];
|
||||
|
||||
$foreign_table = $inlineConfiguration['foreign_table'];
|
||||
$allowed = $groupFieldConfiguration['allowed'];
|
||||
$currentStructureDomObjectIdPrefix = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
|
||||
$objectPrefix = $currentStructureDomObjectIdPrefix . '-' . $foreign_table;
|
||||
$nameObject = $currentStructureDomObjectIdPrefix;
|
||||
$mode = 'db';
|
||||
$showUpload = false;
|
||||
$elementBrowserEnabled = true;
|
||||
if (!empty($inlineConfiguration['appearance']['createNewRelationLinkTitle'])) {
|
||||
$createNewRelationText = htmlspecialchars($languageService->sL($inlineConfiguration['appearance']['createNewRelationLinkTitle']));
|
||||
} else {
|
||||
$createNewRelationText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
|
||||
}
|
||||
if (is_array($groupFieldConfiguration['appearance'])) {
|
||||
if (isset($groupFieldConfiguration['appearance']['elementBrowserType'])) {
|
||||
$mode = $groupFieldConfiguration['appearance']['elementBrowserType'];
|
||||
}
|
||||
if ($mode === 'file') {
|
||||
$showUpload = true;
|
||||
}
|
||||
if (isset($inlineConfiguration['appearance']['fileUploadAllowed'])) {
|
||||
$showUpload = (bool)$inlineConfiguration['appearance']['fileUploadAllowed'];
|
||||
}
|
||||
if (isset($groupFieldConfiguration['appearance']['elementBrowserAllowed'])) {
|
||||
$allowed = $groupFieldConfiguration['appearance']['elementBrowserAllowed'];
|
||||
}
|
||||
if (isset($inlineConfiguration['appearance']['elementBrowserEnabled'])) {
|
||||
$elementBrowserEnabled = (bool)$inlineConfiguration['appearance']['elementBrowserEnabled'];
|
||||
}
|
||||
}
|
||||
// Remove any white-spaces from the allowed extension lists
|
||||
$allowed = implode(',', GeneralUtility::trimExplode(',', $allowed, true));
|
||||
$browserParams = '|||' . $allowed . '|' . $objectPrefix . '|inline.checkUniqueElement||inline.importElement';
|
||||
$onClick = 'setFormValueOpenBrowser(' . GeneralUtility::quoteJSvalue($mode) . ', ' . GeneralUtility::quoteJSvalue($browserParams) . '); return false;';
|
||||
|
||||
$buttonStyle = '';
|
||||
if (isset($inlineConfiguration['inline']['inlineNewRelationButtonStyle'])) {
|
||||
$buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineNewRelationButtonStyle'] . '"';
|
||||
}
|
||||
$item = '';
|
||||
if ($elementBrowserEnabled) {
|
||||
$item .= '
|
||||
<a href="#" class="btn btn-default inlineNewRelationButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
|
||||
' . $buttonStyle . ' onclick="' . htmlspecialchars($onClick) . '" title="' . $createNewRelationText . '">
|
||||
' . $this->iconFactory->getIcon('actions-insert-record', Icon::SIZE_SMALL)->render() . '
|
||||
' . $createNewRelationText . '
|
||||
</a>';
|
||||
}
|
||||
|
||||
$isDirectFileUploadEnabled = (bool)$backendUser->uc['edit_docModuleUpload'];
|
||||
$allowedArray = GeneralUtility::trimExplode(',', $allowed, true);
|
||||
$onlineMediaAllowed = OnlineMediaHelperRegistry::getInstance()->getSupportedFileExtensions();
|
||||
if (!empty($allowedArray)) {
|
||||
$onlineMediaAllowed = array_intersect($allowedArray, $onlineMediaAllowed);
|
||||
}
|
||||
if ($showUpload && $isDirectFileUploadEnabled) {
|
||||
$folder = $backendUser->getDefaultUploadFolder(
|
||||
$this->data['tableName'] === 'pages' ? $this->data['vanillaUid'] : $this->data['parentPageRow']['uid'],
|
||||
$this->data['tableName'],
|
||||
$this->data['fieldName']
|
||||
);
|
||||
if (
|
||||
$folder instanceof Folder
|
||||
&& $folder->getStorage()->checkUserActionPermission('add', 'File')
|
||||
) {
|
||||
$maxFileSize = GeneralUtility::getMaxUploadFileSize() * 1024;
|
||||
$item .= ' <a href="#" class="btn btn-default t3js-drag-uploader inlineNewFileUploadButton ' . $this->inlineData['config'][$nameObject]['md5'] . '"
|
||||
' . $buttonStyle . '
|
||||
data-dropzone-target="#' . htmlspecialchars(StringUtility::escapeCssSelector($currentStructureDomObjectIdPrefix)) . '"
|
||||
data-insert-dropzone-before="1"
|
||||
data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
|
||||
data-file-allowed="' . htmlspecialchars($allowed) . '"
|
||||
data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
|
||||
data-max-file-size="' . htmlspecialchars($maxFileSize) . '"
|
||||
>';
|
||||
$item .= $this->iconFactory->getIcon('actions-upload', Icon::SIZE_SMALL)->render() . ' ';
|
||||
$item .= htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:file_upload.select-and-submit'));
|
||||
$item .= '</a>';
|
||||
|
||||
$this->requireJsModules[] = ['TYPO3/CMS/Backend/DragUploader' => 'function(dragUploader){dragUploader.initialize()}'];
|
||||
if (!empty($onlineMediaAllowed)) {
|
||||
$buttonStyle = '';
|
||||
if (isset($inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'])) {
|
||||
$buttonStyle = ' style="' . $inlineConfiguration['inline']['inlineOnlineMediaAddButtonStyle'] . '"';
|
||||
}
|
||||
$this->requireJsModules[] = 'TYPO3/CMS/Backend/OnlineMedia';
|
||||
$buttonText = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.button'));
|
||||
$placeholder = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.placeholder'));
|
||||
$buttonSubmit = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:online_media.new_media.submit'));
|
||||
$allowedMediaUrl = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.allowEmbedSources'));
|
||||
$item .= '
|
||||
<span class="btn btn-default t3js-online-media-add-btn ' . $this->inlineData['config'][$nameObject]['md5'] . '"
|
||||
' . $buttonStyle . '
|
||||
data-file-irre-object="' . htmlspecialchars($objectPrefix) . '"
|
||||
data-online-media-allowed="' . htmlspecialchars(implode(',', $onlineMediaAllowed)) . '"
|
||||
data-online-media-allowed-help-text="' . $allowedMediaUrl . '"
|
||||
data-target-folder="' . htmlspecialchars($folder->getCombinedIdentifier()) . '"
|
||||
title="' . $buttonText . '"
|
||||
data-btn-submit="' . $buttonSubmit . '"
|
||||
data-placeholder="' . $placeholder . '"
|
||||
>
|
||||
' . $this->iconFactory->getIcon('actions-online-media-add', Icon::SIZE_SMALL)->render() . '
|
||||
' . $buttonText . '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$item = '<div class="form-control-wrap">' . $item . '</div>';
|
||||
$allowedList = '';
|
||||
$allowedLabelKey = ($mode === 'file') ? 'allowedFileExtensions' : 'allowedRelations';
|
||||
$allowedLabel = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.' . $allowedLabelKey));
|
||||
foreach ($allowedArray as $allowedItem) {
|
||||
$allowedList .= '<span class="label label-success">' . strtoupper($allowedItem) . '</span> ';
|
||||
}
|
||||
if (!empty($allowedList)) {
|
||||
$item .= '<div class="help-block">' . $allowedLabel . '<br>' . $allowedList . '</div>';
|
||||
}
|
||||
$item = '<div class="form-group t3js-formengine-validation-marker">' . $item . '</div>';
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a selector as used for the select type, to select from all available
|
||||
* records and to create a relation to the embedding record (e.g. like MM).
|
||||
*
|
||||
* @param array $config TCA inline configuration of the parent(!) field
|
||||
* @param array $uniqueIds The uids that have already been used and should be unique
|
||||
* @return string A HTML <select> box with all possible records
|
||||
*/
|
||||
protected function renderPossibleRecordsSelectorTypeSelect(array $config, array $uniqueIds)
|
||||
{
|
||||
$possibleRecords = $config['selectorOrUniquePossibleRecords'];
|
||||
$nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
|
||||
// Create option tags:
|
||||
$opt = [];
|
||||
foreach ($possibleRecords as $p) {
|
||||
if (!in_array($p[1], $uniqueIds)) {
|
||||
$opt[] = '<option value="' . htmlspecialchars($p[1]) . '">' . htmlspecialchars($p[0]) . '</option>';
|
||||
}
|
||||
}
|
||||
// Put together the selector box:
|
||||
$size = (int)$config['size'];
|
||||
$size = $config['autoSizeMax'] ? MathUtility::forceIntegerInRange(count($possibleRecords) + 1, MathUtility::forceIntegerInRange($size, 1), $config['autoSizeMax']) : $size;
|
||||
$onChange = 'return inline.importNewRecord(' . GeneralUtility::quoteJSvalue($nameObject . '-' . $config['foreign_table']) . ')';
|
||||
$item = '
|
||||
<select id="' . $nameObject . '-' . $config['foreign_table'] . '_selector" class="form-control"' . ($size ? ' size="' . $size . '"' : '')
|
||||
. ' onchange="' . htmlspecialchars($onChange) . '"' . ($config['foreign_unique'] ? ' isunique="isunique"' : '') . '>
|
||||
' . implode('', $opt) . '
|
||||
</select>';
|
||||
|
||||
if ($size <= 1) {
|
||||
// Add a "Create new relation" link for adding new relations
|
||||
// This is necessary, if the size of the selector is "1" or if
|
||||
// there is only one record item in the select-box, that is selected by default
|
||||
// The selector-box creates a new relation on using an onChange event (see some line above)
|
||||
if (!empty($config['appearance']['createNewRelationLinkTitle'])) {
|
||||
$createNewRelationText = htmlspecialchars($this->getLanguageService()->sL($config['appearance']['createNewRelationLinkTitle']));
|
||||
} else {
|
||||
$createNewRelationText = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.createNewRelation'));
|
||||
}
|
||||
$item .= '
|
||||
<span class="input-group-btn">
|
||||
<a href="#" class="btn btn-default" onclick="' . htmlspecialchars($onChange) . '" title="' . $createNewRelationText . '">
|
||||
' . $this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL)->render() . $createNewRelationText . '
|
||||
</a>
|
||||
</span>';
|
||||
} else {
|
||||
$item .= '
|
||||
<span class="input-group-btn btn"></span>';
|
||||
}
|
||||
|
||||
// Wrap the selector and add a spacer to the bottom
|
||||
$item = '<div class="input-group form-group t3js-formengine-validation-marker ' . $this->inlineData['config'][$nameObject]['md5'] . '">' . $item . '</div>';
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts FlexForm parts of a form element name like
|
||||
* data[table][uid][field][sDEF][lDEF][FlexForm][vDEF]
|
||||
* Helper method used in inline
|
||||
*
|
||||
* @param string $formElementName The form element name
|
||||
* @return array|null
|
||||
*/
|
||||
protected function extractFlexFormParts($formElementName)
|
||||
{
|
||||
$flexFormParts = null;
|
||||
$matches = [];
|
||||
if (preg_match('#^data(?:\[[^]]+\]){3}(\[data\](?:\[[^]]+\]){4,})$#', $formElementName, $matches)) {
|
||||
$flexFormParts = GeneralUtility::trimExplode(
|
||||
'][',
|
||||
trim($matches[1], '[]')
|
||||
);
|
||||
}
|
||||
return $flexFormParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface;
|
||||
use TYPO3\CMS\Backend\Form\Exception\AccessDeniedContentEditException;
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Form\NodeFactory;
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFile;
|
||||
use TYPO3\CMS\Core\Resource\ResourceFactory;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Render a single inline record relation.
|
||||
*
|
||||
* This container is called by InlineControlContainer to render single existing records.
|
||||
* Furthermore it is called by FormEngine for an incoming ajax request to expand an existing record
|
||||
* or to create a new one.
|
||||
*
|
||||
* This container creates the outer HTML of single inline records - eg. drag and drop and delete buttons.
|
||||
* For rendering of the record itself processing is handed over to FullRecordContainer.
|
||||
*/
|
||||
class InlineRecordContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Inline data array used for JSON output
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $inlineData = [];
|
||||
|
||||
/**
|
||||
* @var InlineStackProcessor
|
||||
*/
|
||||
protected $inlineStackProcessor;
|
||||
|
||||
/**
|
||||
* Array containing instances of hook classes called once for IRRE objects
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $hookObjects = [];
|
||||
|
||||
/**
|
||||
* @var IconFactory
|
||||
*/
|
||||
protected $iconFactory;
|
||||
|
||||
/**
|
||||
* Default constructor
|
||||
*
|
||||
* @param NodeFactory $nodeFactory
|
||||
* @param array $data
|
||||
*/
|
||||
public function __construct(NodeFactory $nodeFactory, array $data)
|
||||
{
|
||||
parent::__construct($nodeFactory, $data);
|
||||
$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$this->inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$this->initHookObjects();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
* @throws AccessDeniedContentEditException
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$data = $this->data;
|
||||
$this->inlineData = $data['inlineData'];
|
||||
|
||||
$inlineStackProcessor = $this->inlineStackProcessor;
|
||||
$inlineStackProcessor->initializeByGivenStructure($data['inlineStructure']);
|
||||
|
||||
$record = $data['databaseRow'];
|
||||
$inlineConfig = $data['inlineParentConfig'];
|
||||
$foreignTable = $inlineConfig['foreign_table'];
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Send a mapping information to the browser via JSON:
|
||||
// e.g. data[<curTable>][<curId>][<curField>] => data-<pid>-<parentTable>-<parentId>-<parentField>-<curTable>-<curId>-<curField>
|
||||
$formPrefix = $inlineStackProcessor->getCurrentStructureFormPrefix();
|
||||
$domObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
|
||||
$this->inlineData['map'][$formPrefix] = $domObjectId;
|
||||
|
||||
$resultArray['inlineData'] = $this->inlineData;
|
||||
|
||||
// If there is a selector field, normalize it:
|
||||
if (!empty($inlineConfig['foreign_selector'])) {
|
||||
$foreign_selector = $inlineConfig['foreign_selector'];
|
||||
$valueToNormalize = $record[$foreign_selector];
|
||||
if (is_array($record[$foreign_selector])) {
|
||||
// @todo: this can be kicked again if always prepared rows are handled here
|
||||
$valueToNormalize = implode(',', $record[$foreign_selector]);
|
||||
}
|
||||
$record[$foreign_selector] = $this->normalizeUid($valueToNormalize);
|
||||
}
|
||||
|
||||
// Get the current naming scheme for DOM name/id attributes:
|
||||
$appendFormFieldNames = '[' . $foreignTable . '][' . $record['uid'] . ']';
|
||||
$objectId = $domObjectId . '-' . $foreignTable . '-' . $record['uid'];
|
||||
$class = '';
|
||||
$html = '';
|
||||
$combinationHtml = '';
|
||||
$isNewRecord = $data['command'] === 'new';
|
||||
$hiddenField = '';
|
||||
if (isset($data['processedTca']['ctrl']['enablecolumns']['disabled'])) {
|
||||
$hiddenField = $data['processedTca']['ctrl']['enablecolumns']['disabled'];
|
||||
}
|
||||
if (!$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
if ($isNewRecord || $data['isInlineChildExpanded']) {
|
||||
// Render full content ONLY IF this is an AJAX request, a new record, or the record is not collapsed
|
||||
$combinationHtml = '';
|
||||
if (isset($data['combinationChild'])) {
|
||||
$combinationChild = $this->renderCombinationChild($data, $appendFormFieldNames);
|
||||
$combinationHtml = $combinationChild['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $combinationChild, false);
|
||||
}
|
||||
$childArray = $this->renderChild($data);
|
||||
$html = $childArray['html'];
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray, false);
|
||||
} else {
|
||||
// This string is the marker for the JS-function to check if the full content has already been loaded
|
||||
$html = '<!--notloaded-->';
|
||||
}
|
||||
if ($isNewRecord) {
|
||||
// Add pid of record as hidden field
|
||||
$html .= '<input type="hidden" name="data' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[pid]" value="' . htmlspecialchars($record['pid']) . '"/>';
|
||||
// Tell DataHandler this record is expanded
|
||||
$ucFieldName = 'uc[inlineView]'
|
||||
. '[' . $data['inlineTopMostParentTableName'] . ']'
|
||||
. '[' . $data['inlineTopMostParentUid'] . ']'
|
||||
. $appendFormFieldNames;
|
||||
$html .= '<input type="hidden" name="' . htmlspecialchars($ucFieldName)
|
||||
. '" value="' . (int)$data['isInlineChildExpanded'] . '" />';
|
||||
} else {
|
||||
// Set additional field for processing for saving
|
||||
$html .= '<input type="hidden" name="cmd' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[delete]" value="1" disabled="disabled" />';
|
||||
if (!$data['isInlineChildExpanded'] && !empty($hiddenField)) {
|
||||
$checked = !empty($record[$hiddenField]) ? ' checked="checked"' : '';
|
||||
$html .= '<input type="checkbox" data-formengine-input-name="data'
|
||||
. htmlspecialchars($appendFormFieldNames)
|
||||
. '[' . htmlspecialchars($hiddenField) . ']" value="1"' . $checked . ' />';
|
||||
$html .= '<input type="input" name="data' . htmlspecialchars($appendFormFieldNames)
|
||||
. '[' . htmlspecialchars($hiddenField) . ']" value="' . htmlspecialchars($record[$hiddenField]) . '" />';
|
||||
}
|
||||
}
|
||||
// If this record should be shown collapsed
|
||||
$class = $data['isInlineChildExpanded'] ? 'panel-visible' : 'panel-collapsed';
|
||||
}
|
||||
$hiddenFieldHtml = implode(LF, $resultArray['additionalHiddenFields'] ?? []);
|
||||
|
||||
if ($inlineConfig['renderFieldsOnly']) {
|
||||
// Render "body" part only
|
||||
$html = $html . $combinationHtml;
|
||||
} else {
|
||||
// Render header row and content (if expanded)
|
||||
if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$class .= ' t3-form-field-container-inline-placeHolder';
|
||||
}
|
||||
if (!empty($hiddenField) && isset($record[$hiddenField]) && (int)$record[$hiddenField]) {
|
||||
$class .= ' t3-form-field-container-inline-hidden';
|
||||
}
|
||||
$class .= ($isNewRecord ? ' inlineIsNewRecord' : '');
|
||||
$tableUniqueOriginalValue = '';
|
||||
if (isset($data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']])) {
|
||||
$uniqueValueValues = $data['inlineData']['unique'][$domObjectId . '-' . $foreignTable]['used'][$record['uid']];
|
||||
// in case of site_language we don't have the full form engine options, so fallbacks need to be taken into account
|
||||
$tableUniqueOriginalValue = ($uniqueValueValues['table'] ?? $foreignTable) . '_' . ($uniqueValueValues['uid'] ?? $uniqueValueValues);
|
||||
}
|
||||
$html = '
|
||||
<div class="panel panel-default panel-condensed ' . trim($class) . '" id="' . htmlspecialchars($objectId) . '_div" data-table-unique-original-value="' . htmlspecialchars($tableUniqueOriginalValue) . '">
|
||||
<div class="panel-heading" data-toggle="formengine-inline" id="' . htmlspecialchars($objectId) . '_header" data-expandSingle="' . ($inlineConfig['appearance']['expandSingle'] ? 1 : 0) . '">
|
||||
<div class="form-irre-header">
|
||||
<div class="form-irre-header-cell form-irre-header-icon">
|
||||
<span class="caret"></span>
|
||||
</div>
|
||||
' . $this->renderForeignRecordHeader($data) . '
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-collapse" id="' . htmlspecialchars($objectId) . '_fields">' . $html . $hiddenFieldHtml . $combinationHtml . '</div>
|
||||
</div>';
|
||||
}
|
||||
|
||||
$resultArray['html'] = $html;
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render inner child
|
||||
*
|
||||
* @param array $data
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderChild(array $data)
|
||||
{
|
||||
$domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
|
||||
$data['tabAndInlineStack'][] = [
|
||||
'inline',
|
||||
$domObjectId . '-' . $data['tableName'] . '-' . $data['databaseRow']['uid'],
|
||||
];
|
||||
// @todo: ugly construct ...
|
||||
$data['inlineData'] = $this->inlineData;
|
||||
$data['renderType'] = 'fullRecordContainer';
|
||||
return $this->nodeFactory->create($data)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render child child
|
||||
*
|
||||
* Render a table with FormEngine, that occurs on an intermediate table but should be editable directly,
|
||||
* so two tables are combined (the intermediate table with attributes and the sub-embedded table).
|
||||
* -> This is a direct embedding over two levels!
|
||||
*
|
||||
* @param array $data
|
||||
* @param string $appendFormFieldNames The [<table>][<uid>] of the parent record (the intermediate table)
|
||||
* @return array Result array
|
||||
*/
|
||||
protected function renderCombinationChild(array $data, $appendFormFieldNames)
|
||||
{
|
||||
$childData = $data['combinationChild'];
|
||||
$parentConfig = $data['inlineParentConfig'];
|
||||
|
||||
// If field is set to readOnly, set all fields of the relation to readOnly as well
|
||||
if (isset($parentConfig['readOnly']) && $parentConfig['readOnly']) {
|
||||
foreach ($childData['processedTca']['columns'] as $columnName => $columnConfiguration) {
|
||||
$childData['processedTca']['columns'][$columnName]['config']['readOnly'] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
// Display Warning FlashMessage if it is not suppressed
|
||||
if (!isset($parentConfig['appearance']['suppressCombinationWarning']) || empty($parentConfig['appearance']['suppressCombinationWarning'])) {
|
||||
$combinationWarningMessage = 'LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.inline_use_combination';
|
||||
if (!empty($parentConfig['appearance']['overwriteCombinationWarningMessage'])) {
|
||||
$combinationWarningMessage = $parentConfig['appearance']['overwriteCombinationWarningMessage'];
|
||||
}
|
||||
$message = $this->getLanguageService()->sL($combinationWarningMessage);
|
||||
$markup = [];
|
||||
// @TODO: This is not a FlashMessage! The markup must be changed and special CSS
|
||||
// @TODO: should be created, in order to prevent confusion.
|
||||
$markup[] = '<div class="alert alert-warning">';
|
||||
$markup[] = ' <div class="media">';
|
||||
$markup[] = ' <div class="media-left">';
|
||||
$markup[] = ' <span class="fa-stack fa-lg">';
|
||||
$markup[] = ' <i class="fa fa-circle fa-stack-2x"></i>';
|
||||
$markup[] = ' <i class="fa fa-exclamation fa-stack-1x"></i>';
|
||||
$markup[] = ' </span>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' <div class="media-body">';
|
||||
$markup[] = ' <div class="alert-message">' . htmlspecialchars($message) . '</div>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = '</div>';
|
||||
$resultArray['html'] = implode(LF, $markup);
|
||||
}
|
||||
|
||||
$childArray = $this->renderChild($childData);
|
||||
$resultArray = $this->mergeChildReturnIntoExistingResult($resultArray, $childArray);
|
||||
|
||||
// If this is a new record, add a pid value to store this record and the pointer value for the intermediate table
|
||||
if ($childData['command'] === 'new') {
|
||||
$comboFormFieldName = 'data[' . $childData['tableName'] . '][' . $childData['databaseRow']['uid'] . '][pid]';
|
||||
$resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($comboFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['pid']) . '" />';
|
||||
}
|
||||
// If the foreign_selector field is also responsible for uniqueness, tell the browser the uid of the "other" side of the relation
|
||||
if ($childData['command'] === 'new' || $parentConfig['foreign_unique'] === $parentConfig['foreign_selector']) {
|
||||
$parentFormFieldName = 'data' . $appendFormFieldNames . '[' . $parentConfig['foreign_selector'] . ']';
|
||||
$resultArray['html'] .= '<input type="hidden" name="' . htmlspecialchars($parentFormFieldName) . '" value="' . htmlspecialchars($childData['databaseRow']['uid']) . '" />';
|
||||
}
|
||||
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the HTML header for a foreign record, such as the title, toggle-function, drag'n'drop, etc.
|
||||
* Later on the command-icons are inserted here.
|
||||
*
|
||||
* @param array $data Current data
|
||||
* @return string The HTML code of the header
|
||||
*/
|
||||
protected function renderForeignRecordHeader(array $data)
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$inlineConfig = $data['inlineParentConfig'];
|
||||
$foreignTable = $inlineConfig['foreign_table'];
|
||||
$rec = $data['databaseRow'];
|
||||
// Init:
|
||||
$domObjectId = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
|
||||
$objectId = $domObjectId . '-' . $foreignTable . '-' . $rec['uid'];
|
||||
|
||||
$recordTitle = $data['recordTitle'];
|
||||
if (!empty($recordTitle)) {
|
||||
// The user function may return HTML, therefore we can't escape it
|
||||
if (empty($data['processedTca']['ctrl']['formattedLabel_userFunc'])) {
|
||||
$recordTitle = BackendUtility::getRecordTitlePrep($recordTitle);
|
||||
}
|
||||
} else {
|
||||
$recordTitle = '<em>[' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) . ']</em>';
|
||||
}
|
||||
|
||||
$altText = BackendUtility::getRecordIconAltText($rec, $foreignTable);
|
||||
|
||||
$iconImg = '<span title="' . $altText . '" id="' . htmlspecialchars($objectId) . '_icon' . '">' . $this->iconFactory->getIconForRecord($foreignTable, $rec, Icon::SIZE_SMALL)->render() . '</span>';
|
||||
$label = '<span id="' . $objectId . '_label">' . $recordTitle . '</span>';
|
||||
$ctrl = $this->renderForeignRecordHeaderControl($data);
|
||||
$thumbnail = false;
|
||||
|
||||
// Renders a thumbnail for the header
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] && !empty($inlineConfig['appearance']['headerThumbnail']['field'])) {
|
||||
$fieldValue = $rec[$inlineConfig['appearance']['headerThumbnail']['field']];
|
||||
$fileUid = $fieldValue[0]['uid'];
|
||||
|
||||
if (!empty($fileUid)) {
|
||||
try {
|
||||
$fileObject = ResourceFactory::getInstance()->getFileObject($fileUid);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$fileObject = null;
|
||||
}
|
||||
if ($fileObject && $fileObject->isMissing()) {
|
||||
$thumbnail .= '<span class="label label-danger">'
|
||||
. htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
|
||||
. '</span> ' . htmlspecialchars($fileObject->getName()) . '<br />';
|
||||
} elseif ($fileObject) {
|
||||
$imageSetup = $inlineConfig['appearance']['headerThumbnail'];
|
||||
unset($imageSetup['field']);
|
||||
if (!empty($rec['crop'])) {
|
||||
$imageSetup['crop'] = $rec['crop'];
|
||||
}
|
||||
$imageSetup = array_merge(['width' => '45', 'height' => '45c'], $imageSetup);
|
||||
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
|
||||
&& GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileObject->getExtension())) {
|
||||
$processedImage = $fileObject->process(ProcessedFile::CONTEXT_IMAGEPREVIEW, $imageSetup);
|
||||
// Only use a thumbnail if the processing process was successful by checking if image width is set
|
||||
if ($processedImage->getProperty('width')) {
|
||||
$imageUrl = $processedImage->getPublicUrl(true);
|
||||
$thumbnail = '<img src="' . $imageUrl . '" ' .
|
||||
'width="' . $processedImage->getProperty('width') . '" ' .
|
||||
'height="' . $processedImage->getProperty('height') . '" ' .
|
||||
'alt="' . htmlspecialchars($altText) . '" ' .
|
||||
'title="' . htmlspecialchars($altText) . '">';
|
||||
}
|
||||
} else {
|
||||
$thumbnail = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($inlineConfig['appearance']['headerThumbnail']['field']) && $thumbnail) {
|
||||
$mediaContainer = '<div class="form-irre-header-cell form-irre-header-thumbnail" id="' . $objectId . '_thumbnailcontainer">' . $thumbnail . '</div>';
|
||||
} else {
|
||||
$mediaContainer = '<div class="form-irre-header-cell form-irre-header-icon" id="' . $objectId . '_iconcontainer">' . $iconImg . '</div>';
|
||||
}
|
||||
$header = $mediaContainer . '
|
||||
<div class="form-irre-header-cell form-irre-header-body">' . $label . '</div>
|
||||
<div class="form-irre-header-cell form-irre-header-control t3js-formengine-irre-control">' . $ctrl . '</div>';
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the control-icons for a record header (create new, sorting, delete, disable/enable).
|
||||
* Most of the parts are copy&paste from TYPO3\CMS\Recordlist\RecordList\DatabaseRecordList and
|
||||
* modified for the JavaScript calls here
|
||||
*
|
||||
* @param array $data Current data
|
||||
* @return string The HTML code with the control-icons
|
||||
*/
|
||||
protected function renderForeignRecordHeaderControl(array $data)
|
||||
{
|
||||
$rec = $data['databaseRow'];
|
||||
$inlineConfig = $data['inlineParentConfig'];
|
||||
$foreignTable = $inlineConfig['foreign_table'];
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
// Initialize:
|
||||
$cells = [
|
||||
'edit' => '',
|
||||
'hide' => '',
|
||||
'delete' => '',
|
||||
'info' => '',
|
||||
'new' => '',
|
||||
'sort.up' => '',
|
||||
'sort.down' => '',
|
||||
'dragdrop' => '',
|
||||
'localize' => '',
|
||||
'locked' => '',
|
||||
];
|
||||
$isNewItem = strpos($rec['uid'], 'NEW') === 0;
|
||||
$isParentReadOnly = isset($inlineConfig['readOnly']) && $inlineConfig['readOnly'];
|
||||
$isParentExisting = MathUtility::canBeInterpretedAsInteger($data['inlineParentUid']);
|
||||
$tcaTableCtrl = $GLOBALS['TCA'][$foreignTable]['ctrl'];
|
||||
$tcaTableCols = $GLOBALS['TCA'][$foreignTable]['columns'];
|
||||
$isPagesTable = $foreignTable === 'pages';
|
||||
$isSysFileReferenceTable = $foreignTable === 'sys_file_reference';
|
||||
$enableManualSorting = $tcaTableCtrl['sortby'] || $inlineConfig['MM'] || !$data['isOnSymmetricSide']
|
||||
&& $inlineConfig['foreign_sortby'] || $data['isOnSymmetricSide'] && $inlineConfig['symmetric_sortby'];
|
||||
$nameObject = $this->inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($data['inlineFirstPid']);
|
||||
$nameObjectFt = $nameObject . '-' . $foreignTable;
|
||||
$nameObjectFtId = $nameObjectFt . '-' . $rec['uid'];
|
||||
$calcPerms = $backendUser->calcPerms(BackendUtility::readPageAccess($rec['pid'], $backendUser->getPagePermsClause(Permission::PAGE_SHOW)));
|
||||
// If the listed table is 'pages' we have to request the permission settings for each page:
|
||||
$localCalcPerms = false;
|
||||
if ($isPagesTable) {
|
||||
$localCalcPerms = $backendUser->calcPerms(BackendUtility::getRecord('pages', $rec['uid']));
|
||||
}
|
||||
// This expresses the edit permissions for this particular element:
|
||||
$permsEdit = $isPagesTable && $localCalcPerms & Permission::PAGE_EDIT || !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT;
|
||||
// Controls: Defines which controls should be shown
|
||||
$enabledControls = $inlineConfig['appearance']['enabledControls'];
|
||||
// Hook: Can disable/enable single controls for specific child records:
|
||||
foreach ($this->hookObjects as $hookObj) {
|
||||
/** @var InlineElementHookInterface $hookObj */
|
||||
$hookObj->renderForeignRecordHeaderControl_preProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $enabledControls);
|
||||
}
|
||||
if ($data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$cells['localize'] = '<span title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize.isLocalizable')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-localize-status-low', Icon::SIZE_SMALL)->render() . '
|
||||
</span>';
|
||||
}
|
||||
// "Info": (All records)
|
||||
// @todo: hardcoded sys_file!
|
||||
if ($rec['table_local'] === 'sys_file') {
|
||||
$uid = $rec['uid_local'][0]['uid'];
|
||||
$table = '_FILE';
|
||||
} else {
|
||||
$uid = $rec['uid'];
|
||||
$table = $foreignTable;
|
||||
}
|
||||
if ($enabledControls['info']) {
|
||||
if ($isNewItem) {
|
||||
$cells['info'] = '<span class="btn btn-default disabled">' . $this->iconFactory->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
|
||||
} else {
|
||||
$cells['info'] = '
|
||||
<a class="btn btn-default" href="#" onclick="' . htmlspecialchars('top.TYPO3.InfoWindow.showItem(' . GeneralUtility::quoteJSvalue($table) . ', ' . GeneralUtility::quoteJSvalue($uid) . '); return false;') . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:showInfo')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-document-info', Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
// If the table is NOT a read-only table, then show these links:
|
||||
if (!$isParentReadOnly && !$tcaTableCtrl['readOnly'] && !$data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
// "New record after" link (ONLY if the records in the table are sorted by a "sortby"-row or if default values can depend on previous record):
|
||||
if ($enabledControls['new'] && ($enableManualSorting || $tcaTableCtrl['useColumnsForDefaultValues'])) {
|
||||
if (!$isPagesTable && $calcPerms & Permission::CONTENT_EDIT || $isPagesTable && $calcPerms & Permission::PAGE_NEW) {
|
||||
$onClick = 'return inline.createNewRecord(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ',' . GeneralUtility::quoteJSvalue($rec['uid']) . ')';
|
||||
$style = '';
|
||||
if ($inlineConfig['inline']['inlineNewButtonStyle']) {
|
||||
$style = ' style="' . $inlineConfig['inline']['inlineNewButtonStyle'] . '"';
|
||||
}
|
||||
$cells['new'] = '
|
||||
<a class="btn btn-default inlineNewButton ' . $this->inlineData['config'][$nameObject]['md5'] . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:new' . ($isPagesTable ? 'Page' : 'Record'))) . '" ' . $style . '>
|
||||
' . $this->iconFactory->getIcon('actions-' . ($isPagesTable ? 'page-new' : 'add'), Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
// "Up/Down" links
|
||||
if ($enabledControls['sort'] && $permsEdit && $enableManualSorting) {
|
||||
// Up
|
||||
$onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'1\')';
|
||||
$icon = 'actions-move-up';
|
||||
$class = '';
|
||||
if ($inlineConfig['inline']['first'] == $rec['uid']) {
|
||||
$class = ' disabled';
|
||||
$icon = 'empty-empty';
|
||||
}
|
||||
$cells['sort.up'] = '
|
||||
<a class="btn btn-default sortingUp' . $class . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveUp')) . '">
|
||||
' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
// Down
|
||||
$onClick = 'return inline.changeSorting(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ', \'-1\')';
|
||||
$icon = 'actions-move-down';
|
||||
$class = '';
|
||||
if ($inlineConfig['inline']['last'] == $rec['uid']) {
|
||||
$class = ' disabled';
|
||||
$icon = 'empty-empty';
|
||||
}
|
||||
|
||||
$cells['sort.down'] = '
|
||||
<a class="btn btn-default sortingDown' . $class . '" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:moveDown')) . '">
|
||||
' . $this->iconFactory->getIcon($icon, Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
}
|
||||
// "Edit" link:
|
||||
if (($rec['table_local'] === 'sys_file') && !$isNewItem && $backendUser->check('tables_modify', 'sys_file_metadata')) {
|
||||
$sys_language_uid = 0;
|
||||
if (!empty($rec['sys_language_uid'])) {
|
||||
$sys_language_uid = $rec['sys_language_uid'][0];
|
||||
}
|
||||
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getQueryBuilderForTable('sys_file_metadata');
|
||||
$recordInDatabase = $queryBuilder
|
||||
->select('uid')
|
||||
->from('sys_file_metadata')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'file',
|
||||
$queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
|
||||
),
|
||||
$queryBuilder->expr()->eq(
|
||||
'sys_language_uid',
|
||||
$queryBuilder->createNamedParameter($sys_language_uid, \PDO::PARAM_INT)
|
||||
)
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->execute()
|
||||
->fetch();
|
||||
if (!empty($recordInDatabase)) {
|
||||
$uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
|
||||
$url = (string)$uriBuilder->buildUriFromRoute('record_edit', [
|
||||
'edit[sys_file_metadata][' . (int)$recordInDatabase['uid'] . ']' => 'edit',
|
||||
'returnUrl' => $this->data['returnUrl']
|
||||
]);
|
||||
$title = $languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:cm.editMetadata');
|
||||
$cells['edit'] = '
|
||||
<a class="btn btn-default" href="' . htmlspecialchars($url) . '" title="' . htmlspecialchars($title) . '">
|
||||
' . $this->iconFactory->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
// "Delete" link:
|
||||
if ($enabledControls['delete'] && ($isPagesTable && $localCalcPerms & Permission::PAGE_DELETE
|
||||
|| !$isPagesTable && $calcPerms & Permission::CONTENT_EDIT
|
||||
|| $isSysFileReferenceTable && $calcPerms & Permission::PAGE_EDIT)
|
||||
) {
|
||||
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:delete'));
|
||||
$icon = $this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render();
|
||||
$cells['delete'] = '<a href="#" class="btn btn-default t3js-editform-delete-inline-record" data-objectid="' . htmlspecialchars($nameObjectFtId) . '" title="' . $title . '">' . $icon . '</a>';
|
||||
}
|
||||
|
||||
// "Hide/Unhide" links:
|
||||
$hiddenField = $tcaTableCtrl['enablecolumns']['disabled'];
|
||||
if ($enabledControls['hide'] && $permsEdit && $hiddenField && $tcaTableCols[$hiddenField] && (!$tcaTableCols[$hiddenField]['exclude'] || $backendUser->check('non_exclude_fields', $foreignTable . ':' . $hiddenField))) {
|
||||
$onClick = 'return inline.enableDisableRecord(' . GeneralUtility::quoteJSvalue($nameObjectFtId) . ',' .
|
||||
GeneralUtility::quoteJSvalue($hiddenField) . ')';
|
||||
$className = 't3js-' . $nameObjectFtId . '_disabled';
|
||||
if ($rec[$hiddenField]) {
|
||||
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:unHide' . ($isPagesTable ? 'Page' : '')));
|
||||
$cells['hide'] = '
|
||||
<a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
|
||||
' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
} else {
|
||||
$title = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_mod_web_list.xlf:hide' . ($isPagesTable ? 'Page' : '')));
|
||||
$cells['hide'] = '
|
||||
<a class="btn btn-default hiddenHandle ' . $className . '" href="#" onclick="
|
||||
' . htmlspecialchars($onClick) . '"' . 'title="' . $title . '">
|
||||
' . $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
// Drag&Drop Sorting: Sortable handler for script.aculo.us
|
||||
if ($enabledControls['dragdrop'] && $permsEdit && $enableManualSorting && $inlineConfig['appearance']['useSortable']) {
|
||||
$cells['dragdrop'] = '
|
||||
<span class="btn btn-default sortableHandle" data-id="' . htmlspecialchars($rec['uid']) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.move')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-move-move', Icon::SIZE_SMALL)->render() . '
|
||||
</span>';
|
||||
}
|
||||
} elseif ($data['isInlineDefaultLanguageRecordInLocalizedParentContext'] && $isParentExisting) {
|
||||
if ($enabledControls['localize'] && $data['isInlineDefaultLanguageRecordInLocalizedParentContext']) {
|
||||
$onClick = 'inline.synchronizeLocalizeRecords(' . GeneralUtility::quoteJSvalue($nameObjectFt) . ', ' . GeneralUtility::quoteJSvalue($rec['uid']) . ');';
|
||||
$cells['localize'] = '
|
||||
<a class="btn btn-default" href="#" onclick="' . htmlspecialchars($onClick) . '" title="' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_misc.xlf:localize')) . '">
|
||||
' . $this->iconFactory->getIcon('actions-document-localize', Icon::SIZE_SMALL)->render() . '
|
||||
</a>';
|
||||
}
|
||||
}
|
||||
// If the record is edit-locked by another user, we will show a little warning sign:
|
||||
if ($lockInfo = BackendUtility::isRecordLocked($foreignTable, $rec['uid'])) {
|
||||
$cells['locked'] = '
|
||||
<a class="btn btn-default" href="#" data-toggle="tooltip" data-title="' . htmlspecialchars($lockInfo['msg']) . '">
|
||||
' . '<span title="' . htmlspecialchars($lockInfo['msg']) . '">' . $this->iconFactory->getIcon('warning-in-use', Icon::SIZE_SMALL)->render() . '</span>' . '
|
||||
</a>';
|
||||
}
|
||||
// Hook: Post-processing of single controls for specific child records:
|
||||
foreach ($this->hookObjects as $hookObj) {
|
||||
$hookObj->renderForeignRecordHeaderControl_postProcess($data['inlineParentUid'], $foreignTable, $rec, $inlineConfig, $data['isInlineDefaultLanguageRecordInLocalizedParentContext'], $cells);
|
||||
}
|
||||
|
||||
$out = '';
|
||||
if (!empty($cells['edit']) || !empty($cells['hide']) || !empty($cells['delete'])) {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['edit'] . $cells['hide'] . $cells['delete'] . '</div>';
|
||||
unset($cells['edit'], $cells['hide'], $cells['delete']);
|
||||
}
|
||||
if (!empty($cells['info']) || !empty($cells['new']) || !empty($cells['sort.up']) || !empty($cells['sort.down']) || !empty($cells['dragdrop'])) {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['info'] . $cells['new'] . $cells['sort.up'] . $cells['sort.down'] . $cells['dragdrop'] . '</div>';
|
||||
unset($cells['info'], $cells['new'], $cells['sort.up'], $cells['sort.down'], $cells['dragdrop']);
|
||||
}
|
||||
if (!empty($cells['localize'])) {
|
||||
$out .= '<div class="btn-group btn-group-sm" role="group">' . $cells['localize'] . '</div>';
|
||||
unset($cells['localize']);
|
||||
}
|
||||
if (!empty($cells)) {
|
||||
$out .= ' <div class="btn-group btn-group-sm" role="group">' . implode('', $cells) . '</div>';
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a relation "uid" published by transferData, like "1|Company%201"
|
||||
*
|
||||
* @param string $string A transferData reference string, containing the uid
|
||||
* @return string The normalized uid
|
||||
*/
|
||||
protected function normalizeUid($string)
|
||||
{
|
||||
$parts = explode('|', $string);
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialized the hook objects for this class.
|
||||
* Each hook object has to implement the interface
|
||||
* \TYPO3\CMS\Backend\Form\Element\InlineElementHookInterface
|
||||
*
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
protected function initHookObjects()
|
||||
{
|
||||
$this->hookObjects = [];
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tceforms_inline.php']['tceformsInlineHook'] ?? [] as $className) {
|
||||
$processObject = GeneralUtility::makeInstance($className);
|
||||
if (!$processObject instanceof InlineElementHookInterface) {
|
||||
throw new \UnexpectedValueException($className . ' must implement interface ' . InlineElementHookInterface::class, 1202072000);
|
||||
}
|
||||
$this->hookObjects[] = $processObject;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Render a given list of field of a TCA table.
|
||||
*
|
||||
* This is an entry container called from FormEngine to handle a
|
||||
* list of specific fields. Access rights are checked here and globalOption array
|
||||
* is prepared for further processing of single fields by PaletteAndSingleContainer.
|
||||
*/
|
||||
class ListOfFieldsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$table = $this->data['tableName'];
|
||||
$fieldListToRender = $this->data['fieldListToRender'];
|
||||
$recordTypeValue = $this->data['recordTypeValue'];
|
||||
|
||||
// Load the description content for the table if requested
|
||||
if (!empty($GLOBALS['TCA'][$table]['interface']['always_description'])) {
|
||||
$languageService = $this->getLanguageService();
|
||||
$languageService->loadSingleTableDescription($table);
|
||||
}
|
||||
|
||||
$fieldListToRender = array_unique(GeneralUtility::trimExplode(',', $fieldListToRender, true));
|
||||
|
||||
$fieldsByShowitem = $this->data['processedTca']['types'][$recordTypeValue]['showitem'];
|
||||
$fieldsByShowitem = GeneralUtility::trimExplode(',', $fieldsByShowitem, true);
|
||||
|
||||
$finalFieldsList = [];
|
||||
foreach ($fieldListToRender as $fieldName) {
|
||||
foreach ($fieldsByShowitem as $fieldByShowitem) {
|
||||
$fieldByShowitemArray = $this->explodeSingleFieldShowItemConfiguration($fieldByShowitem);
|
||||
if ($fieldByShowitemArray['fieldName'] === $fieldName) {
|
||||
$finalFieldsList[] = implode(';', $fieldByShowitemArray);
|
||||
break;
|
||||
}
|
||||
if ($fieldByShowitemArray['fieldName'] === '--palette--'
|
||||
&& isset($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem'])
|
||||
&& is_string($this->data['processedTca']['palettes'][$fieldByShowitemArray['paletteName']]['showitem'])
|
||||
) {
|
||||
$paletteName = $fieldByShowitemArray['paletteName'];
|
||||
$paletteFields = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true);
|
||||
foreach ($paletteFields as $paletteField) {
|
||||
$paletteFieldArray = $this->explodeSingleFieldShowItemConfiguration($paletteField);
|
||||
if ($paletteFieldArray['fieldName'] === $fieldName) {
|
||||
$finalFieldsList[] = implode(';', $paletteFieldArray);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
$options['fieldsArray'] = $finalFieldsList;
|
||||
$options['renderType'] = 'paletteAndSingleContainer';
|
||||
return $this->nodeFactory->create($options)->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
/**
|
||||
* Handle a record that has no tabs.
|
||||
*
|
||||
* This container is called by FullRecordContainer and just wraps the output
|
||||
* of PaletteAndSingleContainer in some HTML.
|
||||
*/
|
||||
class NoTabsContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$options = $this->data;
|
||||
$options['renderType'] = 'paletteAndSingleContainer';
|
||||
$resultArray = $this->nodeFactory->create($options)->render();
|
||||
$resultArray['html'] = '<div class="tab-content">' . $resultArray['html'] . '</div>';
|
||||
return $resultArray;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Imaging\Icon;
|
||||
use TYPO3\CMS\Core\Imaging\IconFactory;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\Permission;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Fluid\View\StandaloneView;
|
||||
|
||||
/**
|
||||
* Render header and footer row.
|
||||
*
|
||||
* This is an entry container called from controllers.
|
||||
* It wraps the title and a footer around the main html.
|
||||
* It either calls a FullRecordContainer or ListOfFieldsContainer to render
|
||||
* a full record or only some fields from a full record.
|
||||
*/
|
||||
class OuterWrapContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
|
||||
$options = $this->data;
|
||||
if (empty($this->data['fieldListToRender'])) {
|
||||
$options['renderType'] = 'fullRecordContainer';
|
||||
} else {
|
||||
$options['renderType'] = 'listOfFieldsContainer';
|
||||
}
|
||||
$result = $this->nodeFactory->create($options)->render();
|
||||
|
||||
$childHtml = $result['html'];
|
||||
|
||||
$recordPath = '';
|
||||
// @todo: what is this >= 0 check for? wsol cases?!
|
||||
if ($this->data['effectivePid'] >= 0) {
|
||||
$permissionsClause = $backendUser->getPagePermsClause(Permission::PAGE_SHOW);
|
||||
$recordPath = BackendUtility::getRecordPath($this->data['effectivePid'], $permissionsClause, 15);
|
||||
}
|
||||
|
||||
$iconFactory = GeneralUtility::makeInstance(IconFactory::class);
|
||||
$icon = '<span title="' . htmlspecialchars($recordPath) . '">' . $iconFactory->getIconForRecord($table, $row, Icon::SIZE_SMALL)->render() . '</span>';
|
||||
|
||||
// @todo: Could this be done in a more clever way? Does it work at all?
|
||||
$tableTitle = $languageService->sL($this->data['processedTca']['ctrl']['title']);
|
||||
|
||||
if ($this->data['command'] === 'new') {
|
||||
$newOrUid = ' <span class="typo3-TCEforms-newToken">' . htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.new')) . '</span>';
|
||||
|
||||
// @todo: There is quite some stuff do to for WS overlays ...
|
||||
$workspacedPageRecord = BackendUtility::getRecordWSOL('pages', $this->data['effectivePid'], 'title');
|
||||
$pageTitle = BackendUtility::getRecordTitle('pages', $workspacedPageRecord, true, false);
|
||||
if ($table === 'pages') {
|
||||
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNewPage'));
|
||||
$pageTitle = sprintf($label, $tableTitle);
|
||||
} else {
|
||||
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNewRecord'));
|
||||
if ($this->data['effectivePid'] === 0) {
|
||||
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.createNewRecordRootLevel'));
|
||||
}
|
||||
$pageTitle = sprintf($label, $tableTitle, $pageTitle);
|
||||
}
|
||||
} else {
|
||||
$icon = BackendUtility::wrapClickMenuOnIcon($icon, $table, $row['uid']);
|
||||
$newOrUid = ' <span class="typo3-TCEforms-recUid">[' . htmlspecialchars($row['uid']) . ']</span>';
|
||||
|
||||
// @todo: getRecordTitlePrep applies an htmlspecialchars here
|
||||
$recordLabel = BackendUtility::getRecordTitlePrep($this->data['recordTitle']);
|
||||
if ($table === 'pages') {
|
||||
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editPage'));
|
||||
$pageTitle = sprintf($label, $tableTitle, $recordLabel);
|
||||
} else {
|
||||
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecord'));
|
||||
$workspacedPageRecord = BackendUtility::getRecordWSOL('pages', $row['pid'], 'uid,title');
|
||||
$pageTitle = BackendUtility::getRecordTitle('pages', $workspacedPageRecord, true, false);
|
||||
if (empty($recordLabel)) {
|
||||
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecordNoTitle'));
|
||||
}
|
||||
if ($this->data['effectivePid'] === 0) {
|
||||
$label = htmlspecialchars($languageService->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.editRecordRootLevel'));
|
||||
}
|
||||
if (!empty($recordLabel)) {
|
||||
// Use record title and prepend an edit label.
|
||||
$pageTitle = sprintf($label, $tableTitle, $recordLabel, $pageTitle);
|
||||
} else {
|
||||
// Leave out the record title since it is not set.
|
||||
$pageTitle = sprintf($label, $tableTitle, $pageTitle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$view = GeneralUtility::makeInstance(StandaloneView::class);
|
||||
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName(
|
||||
'EXT:backend/Resources/Private/Templates/OuterWrapContainer.html'
|
||||
));
|
||||
|
||||
$descriptionColumn = !empty($this->data['processedTca']['ctrl']['descriptionColumn'])
|
||||
? $this->data['processedTca']['ctrl']['descriptionColumn'] : null;
|
||||
if ($descriptionColumn !== null) {
|
||||
$view->assign('recordDescription', $this->data['databaseRow'][$descriptionColumn]);
|
||||
}
|
||||
$readOnlyRecord = !empty($this->data['processedTca']['ctrl']['readOnly'])
|
||||
? (bool)$this->data['processedTca']['ctrl']['readOnly'] : null;
|
||||
if ($readOnlyRecord === true) {
|
||||
$view->assign('recordReadonly', true);
|
||||
}
|
||||
$fieldInformationResult = $this->renderFieldInformation();
|
||||
$fieldInformationHtml = $fieldInformationResult['html'];
|
||||
$result = $this->mergeChildReturnIntoExistingResult($result, $fieldInformationResult, false);
|
||||
|
||||
$fieldWizardResult = $this->renderFieldWizard();
|
||||
$fieldWizardHtml = $fieldWizardResult['html'];
|
||||
$result = $this->mergeChildReturnIntoExistingResult($result, $fieldWizardResult, false);
|
||||
|
||||
$view->assignMultiple([
|
||||
'pageTitle' => $pageTitle,
|
||||
'fieldInformationHtml' => $fieldInformationHtml,
|
||||
'fieldWizardHtml' => $fieldWizardHtml,
|
||||
'childHtml' => $childHtml,
|
||||
'icon' => $icon,
|
||||
'tableTitle' => $tableTitle,
|
||||
'newOrUid' => $newOrUid,
|
||||
'isNewRecord' => $this->data['command'] === 'new'
|
||||
]);
|
||||
$result['html'] = $view->render();
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,431 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Utility\BackendUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Handle palettes and single fields.
|
||||
*
|
||||
* This container is called by TabsContainer, NoTabsContainer and ListOfFieldsContainer.
|
||||
*
|
||||
* This container mostly operates on TCA showItem of a specific type - the value is
|
||||
* coming in from upper containers as "fieldArray". It handles palettes with all its
|
||||
* different options and prepares rendering of single fields for the SingleFieldContainer.
|
||||
*/
|
||||
class PaletteAndSingleContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Final result array accumulating results from children and final HTML
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $resultArray = [];
|
||||
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$languageService = $this->getLanguageService();
|
||||
|
||||
/*
|
||||
* The first code block creates a target structure array to later create the final
|
||||
* HTML string. The single fields and sub containers are rendered here already and
|
||||
* other parts of the return array from children except html are accumulated in
|
||||
* $this->resultArray
|
||||
*
|
||||
$targetStructure = array(
|
||||
0 => array(
|
||||
'type' => 'palette',
|
||||
'fieldName' => 'palette1',
|
||||
'fieldLabel' => 'palette1',
|
||||
'elements' => array(
|
||||
0 => array(
|
||||
'type' => 'single',
|
||||
'fieldName' => 'paletteName',
|
||||
'fieldLabel' => 'element1',
|
||||
'fieldHtml' => 'element1',
|
||||
),
|
||||
1 => array(
|
||||
'type' => 'linebreak',
|
||||
),
|
||||
2 => array(
|
||||
'type' => 'single',
|
||||
'fieldName' => 'paletteName',
|
||||
'fieldLabel' => 'element2',
|
||||
'fieldHtml' => 'element2',
|
||||
),
|
||||
),
|
||||
),
|
||||
1 => array(
|
||||
'type' => 'single',
|
||||
'fieldName' => 'element3',
|
||||
'fieldLabel' => 'element3',
|
||||
'fieldHtml' => 'element3',
|
||||
),
|
||||
2 => array(
|
||||
'type' => 'palette2',
|
||||
'fieldName' => 'palette2',
|
||||
'fieldLabel' => '', // Palettes may not have a label
|
||||
'elements' => array(
|
||||
0 => array(
|
||||
'type' => 'single',
|
||||
'fieldName' => 'element4',
|
||||
'fieldLabel' => 'element4',
|
||||
'fieldHtml' => 'element4',
|
||||
),
|
||||
1 => array(
|
||||
'type' => 'linebreak',
|
||||
),
|
||||
2 => array(
|
||||
'type' => 'single',
|
||||
'fieldName' => 'element5',
|
||||
'fieldLabel' => 'element5',
|
||||
'fieldHtml' => 'element5',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
*/
|
||||
|
||||
// Create an intermediate structure of rendered sub elements and elements nested in palettes
|
||||
$targetStructure = [];
|
||||
$mainStructureCounter = -1;
|
||||
$fieldsArray = $this->data['fieldsArray'];
|
||||
$this->resultArray = $this->initializeResultArray();
|
||||
foreach ($fieldsArray as $fieldString) {
|
||||
$fieldConfiguration = $this->explodeSingleFieldShowItemConfiguration($fieldString);
|
||||
$fieldName = $fieldConfiguration['fieldName'];
|
||||
if ($fieldName === '--palette--') {
|
||||
$paletteElementArray = $this->createPaletteContentArray($fieldConfiguration['paletteName']);
|
||||
if (!empty($paletteElementArray)) {
|
||||
$mainStructureCounter++;
|
||||
$paletteLabel = $fieldConfiguration['fieldLabel'];
|
||||
if ($paletteLabel === null
|
||||
&& !empty($this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label'])
|
||||
) {
|
||||
// If there is no label in ['types']['aType']['showitem'] for this palette: "--palette--;;aPalette" but
|
||||
// not "--palette--;LLL:aLabelReference;aPalette", then use ['palettes']['aPalette']['label'] if given.
|
||||
$paletteLabel = $this->data['processedTca']['palettes'][$fieldConfiguration['paletteName']]['label'];
|
||||
}
|
||||
$targetStructure[$mainStructureCounter] = [
|
||||
'type' => 'palette',
|
||||
'fieldName' => $fieldConfiguration['paletteName'],
|
||||
'fieldLabel' => $languageService->sL($paletteLabel),
|
||||
'elements' => $paletteElementArray,
|
||||
];
|
||||
}
|
||||
} else {
|
||||
if (!is_array($this->data['processedTca']['columns'][$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$options = $this->data;
|
||||
$options['fieldName'] = $fieldName;
|
||||
|
||||
$options['renderType'] = 'singleFieldContainer';
|
||||
$childResultArray = $this->nodeFactory->create($options)->render();
|
||||
|
||||
if (!empty($childResultArray['html'])) {
|
||||
$mainStructureCounter++;
|
||||
$fieldLabel = '';
|
||||
if (!empty($this->data['processedTca']['columns'][$fieldName]['label'])) {
|
||||
$fieldLabel = $this->data['processedTca']['columns'][$fieldName]['label'];
|
||||
}
|
||||
$targetStructure[$mainStructureCounter] = [
|
||||
'type' => 'single',
|
||||
'fieldName' => $fieldConfiguration['fieldName'],
|
||||
'fieldLabel' => $fieldLabel,
|
||||
'fieldHtml' => $childResultArray['html'],
|
||||
];
|
||||
}
|
||||
|
||||
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $childResultArray, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Compile final content
|
||||
$content = [];
|
||||
foreach ($targetStructure as $element) {
|
||||
if ($element['type'] === 'palette') {
|
||||
$paletteName = $element['fieldName'];
|
||||
$paletteElementsHtml = $this->renderInnerPaletteContent($element);
|
||||
|
||||
$isHiddenPalette = !empty($this->data['processedTca']['palettes'][$paletteName]['isHiddenPalette']);
|
||||
|
||||
$paletteElementsHtml = '<div class="row">' . $paletteElementsHtml . '</div>';
|
||||
|
||||
$content[] = $this->fieldSetWrap($paletteElementsHtml, $isHiddenPalette, $element['fieldLabel']);
|
||||
} else {
|
||||
// Return raw HTML only in case of user element with no wrapping requested
|
||||
if ($this->isUserNoTableWrappingField($element)) {
|
||||
$content[] = $element['fieldHtml'];
|
||||
} else {
|
||||
$content[] = $this->fieldSetWrap($this->wrapSingleFieldContentWithLabelAndOuterDiv($element));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$finalResultArray = $this->resultArray;
|
||||
$finalResultArray['html'] = implode(LF, $content);
|
||||
return $finalResultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render single fields of a given palette
|
||||
*
|
||||
* @param string $paletteName The palette to render
|
||||
* @return array
|
||||
*/
|
||||
protected function createPaletteContentArray($paletteName)
|
||||
{
|
||||
// palette needs a palette name reference, otherwise it does not make sense to try rendering of it
|
||||
if (empty($paletteName) || empty($this->data['processedTca']['palettes'][$paletteName]['showitem'])) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$resultStructure = [];
|
||||
$foundRealElement = false; // Set to true if not only line breaks were rendered
|
||||
$fieldsArray = GeneralUtility::trimExplode(',', $this->data['processedTca']['palettes'][$paletteName]['showitem'], true);
|
||||
foreach ($fieldsArray as $fieldString) {
|
||||
$fieldArray = $this->explodeSingleFieldShowItemConfiguration($fieldString);
|
||||
$fieldName = $fieldArray['fieldName'];
|
||||
if ($fieldName === '--linebreak--') {
|
||||
$resultStructure[] = [
|
||||
'type' => 'linebreak',
|
||||
];
|
||||
} else {
|
||||
if (!is_array($this->data['processedTca']['columns'][$fieldName])) {
|
||||
continue;
|
||||
}
|
||||
$options = $this->data;
|
||||
$options['fieldName'] = $fieldName;
|
||||
|
||||
$options['renderType'] = 'singleFieldContainer';
|
||||
$singleFieldContentArray = $this->nodeFactory->create($options)->render();
|
||||
|
||||
if (!empty($singleFieldContentArray['html'])) {
|
||||
$foundRealElement = true;
|
||||
$fieldLabel = '';
|
||||
if (!empty($this->data['processedTca']['columns'][$fieldName]['label'])) {
|
||||
$fieldLabel = $this->data['processedTca']['columns'][$fieldName]['label'];
|
||||
}
|
||||
$resultStructure[] = [
|
||||
'type' => 'single',
|
||||
'fieldName' => $fieldName,
|
||||
'fieldLabel' => $fieldLabel,
|
||||
'fieldHtml' => $singleFieldContentArray['html'],
|
||||
];
|
||||
}
|
||||
$this->resultArray = $this->mergeChildReturnIntoExistingResult($this->resultArray, $singleFieldContentArray, false);
|
||||
}
|
||||
}
|
||||
|
||||
if ($foundRealElement) {
|
||||
return $resultStructure;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders inner content of single elements of a palette and wrap it as needed
|
||||
*
|
||||
* @param array $elementArray Array of elements
|
||||
* @return string Wrapped content
|
||||
*/
|
||||
protected function renderInnerPaletteContent(array $elementArray)
|
||||
{
|
||||
// Group fields
|
||||
$groupedFields = [];
|
||||
$row = 0;
|
||||
$lastLineWasLinebreak = true;
|
||||
foreach ($elementArray['elements'] as $element) {
|
||||
if ($element['type'] === 'linebreak') {
|
||||
if (!$lastLineWasLinebreak) {
|
||||
$row++;
|
||||
$groupedFields[$row][] = $element;
|
||||
$row++;
|
||||
$lastLineWasLinebreak = true;
|
||||
}
|
||||
} else {
|
||||
$lastLineWasLinebreak = false;
|
||||
$groupedFields[$row][] = $element;
|
||||
}
|
||||
}
|
||||
|
||||
$result = [];
|
||||
// Process fields
|
||||
foreach ($groupedFields as $fields) {
|
||||
$numberOfItems = count($fields);
|
||||
$colWidth = (int)floor(12 / $numberOfItems);
|
||||
// Column class calculation
|
||||
$colClass = 'col-md-12';
|
||||
$colClear = [];
|
||||
if ($colWidth == 6) {
|
||||
$colClass = 'col-sm-6';
|
||||
$colClear = [
|
||||
2 => 'visible-sm-block visible-md-block visible-lg-block',
|
||||
];
|
||||
} elseif ($colWidth === 4) {
|
||||
$colClass = 'col-sm-4';
|
||||
$colClear = [
|
||||
3 => 'visible-sm-block visible-md-block visible-lg-block',
|
||||
];
|
||||
} elseif ($colWidth === 3) {
|
||||
$colClass = 'col-sm-6 col-md-3';
|
||||
$colClear = [
|
||||
2 => 'visible-sm-block',
|
||||
4 => 'visible-md-block visible-lg-block',
|
||||
];
|
||||
} elseif ($colWidth <= 2) {
|
||||
$colClass = 'checkbox-column col-sm-6 col-md-3 col-lg-2';
|
||||
$colClear = [
|
||||
2 => 'visible-sm-block',
|
||||
4 => 'visible-md-block',
|
||||
6 => 'visible-lg-block'
|
||||
];
|
||||
}
|
||||
|
||||
// Render fields
|
||||
for ($counter = 0; $counter < $numberOfItems; $counter++) {
|
||||
$element = $fields[$counter];
|
||||
if ($element['type'] === 'linebreak') {
|
||||
if ($counter !== $numberOfItems) {
|
||||
$result[] = '<div class="clearfix"></div>';
|
||||
}
|
||||
} else {
|
||||
$result[] = $this->wrapSingleFieldContentWithLabelAndOuterDiv($element, [$colClass]);
|
||||
|
||||
// Breakpoints
|
||||
if ($counter + 1 < $numberOfItems && !empty($colClear)) {
|
||||
foreach ($colClear as $rowBreakAfter => $clearClass) {
|
||||
if (($counter + 1) % $rowBreakAfter === 0) {
|
||||
$result[] = '<div class="clearfix ' . $clearClass . '"></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode(LF, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap content in a field set
|
||||
*
|
||||
* @param string $content Incoming content
|
||||
* @param bool $paletteHidden TRUE if the palette is hidden
|
||||
* @param string $label Given label
|
||||
* @return string Wrapped content
|
||||
*/
|
||||
protected function fieldSetWrap($content, $paletteHidden = false, $label = '')
|
||||
{
|
||||
$fieldSetClass = 'form-section';
|
||||
if ($paletteHidden) {
|
||||
$fieldSetClass .= ' hide';
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$result[] = '<fieldset class="' . $fieldSetClass . '">';
|
||||
|
||||
if (!empty($label)) {
|
||||
$result[] = '<h4 class="form-section-headline">' . htmlspecialchars($label) . '</h4>';
|
||||
}
|
||||
|
||||
$result[] = $content;
|
||||
$result[] = '</fieldset>';
|
||||
return implode(LF, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap a single element
|
||||
*
|
||||
* @param array $element Given element as documented above
|
||||
* @param array $additionalPaletteClasses Additional classes to be added to HTML
|
||||
* @return string Wrapped element
|
||||
*/
|
||||
protected function wrapSingleFieldContentWithLabelAndOuterDiv(array $element, array $additionalPaletteClasses = [])
|
||||
{
|
||||
$fieldName = $element['fieldName'];
|
||||
|
||||
$paletteFieldClasses = [
|
||||
'form-group',
|
||||
't3js-formengine-validation-marker',
|
||||
't3js-formengine-palette-field',
|
||||
];
|
||||
foreach ($additionalPaletteClasses as $class) {
|
||||
$paletteFieldClasses[] = $class;
|
||||
}
|
||||
|
||||
$label = BackendUtility::wrapInHelp($this->data['tableName'], $fieldName, htmlspecialchars($element['fieldLabel']));
|
||||
|
||||
if ($GLOBALS['TYPO3_CONF_VARS']['BE']['debug'] && $this->getBackendUser()->isAdmin()) {
|
||||
$label .= '<code>[' . htmlspecialchars($fieldName) . ']</code>';
|
||||
}
|
||||
|
||||
$content = [];
|
||||
$content[] = '<div class="' . implode(' ', $paletteFieldClasses) . '">';
|
||||
$content[] = '<label class="t3js-formengine-label">';
|
||||
$content[] = $label;
|
||||
$content[] = '</label>';
|
||||
$content[] = $element['fieldHtml'];
|
||||
$content[] = '</div>';
|
||||
|
||||
return implode(LF, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* TRUE if field is of type user and to wrapping is requested
|
||||
*
|
||||
* @param array $element Current element from "target structure" array
|
||||
* @return bool TRUE if user and noTableWrapping is set
|
||||
*/
|
||||
protected function isUserNoTableWrappingField($element)
|
||||
{
|
||||
$fieldName = $element['fieldName'];
|
||||
if (
|
||||
$this->data['processedTca']['columns'][$fieldName]['config']['type'] === 'user'
|
||||
&& !empty($this->data['processedTca']['columns'][$fieldName]['config']['noTableWrapping'])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current BE user.
|
||||
*
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUser()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Backend\Form\Container;
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Backend\Form\InlineStackProcessor;
|
||||
use TYPO3\CMS\Backend\Form\Utility\FormEngineUtility;
|
||||
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Type\Bitmask\JsConfirmation;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\MathUtility;
|
||||
|
||||
/**
|
||||
* Container around a "single field".
|
||||
*
|
||||
* This container is the last one in the chain before processing is handed over to single element classes.
|
||||
* If a single field is of type flex or inline, it however creates FlexFormEntryContainer or InlineControlContainer.
|
||||
*
|
||||
* The container does various checks and processing for a given single fields.
|
||||
*/
|
||||
class SingleFieldContainer extends AbstractContainer
|
||||
{
|
||||
/**
|
||||
* Entry method
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @return array As defined in initializeResultArray() of AbstractNode
|
||||
*/
|
||||
public function render()
|
||||
{
|
||||
$backendUser = $this->getBackendUserAuthentication();
|
||||
$resultArray = $this->initializeResultArray();
|
||||
|
||||
$table = $this->data['tableName'];
|
||||
$row = $this->data['databaseRow'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
|
||||
$parameterArray = [];
|
||||
$parameterArray['fieldConf'] = $this->data['processedTca']['columns'][$fieldName];
|
||||
|
||||
$isOverlay = false;
|
||||
|
||||
// This field decides whether the current record is an overlay (as opposed to being a standalone record)
|
||||
// Based on this decision we need to trigger field exclusion or special rendering (like readOnly)
|
||||
if (isset($this->data['processedTca']['ctrl']['transOrigPointerField'])
|
||||
&& is_array($this->data['processedTca']['columns'][$this->data['processedTca']['ctrl']['transOrigPointerField']])
|
||||
) {
|
||||
$parentValue = $row[$this->data['processedTca']['ctrl']['transOrigPointerField']];
|
||||
if (MathUtility::canBeInterpretedAsInteger($parentValue)) {
|
||||
$isOverlay = (bool)$parentValue;
|
||||
} elseif (is_array($parentValue)) {
|
||||
// This case may apply if the value has been converted to an array by the select or group data provider
|
||||
$isOverlay = !empty($parentValue) ? (bool)$parentValue[0] : false;
|
||||
} else {
|
||||
throw new \InvalidArgumentException(
|
||||
'The given value "' . $parentValue . '" for the original language field ' . $this->data['processedTca']['ctrl']['transOrigPointerField']
|
||||
. ' of table ' . $table . ' is invalid.',
|
||||
1470742770
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// A couple of early returns in case the field should not be rendered
|
||||
// Check if this field is configured and editable according to exclude fields and other configuration
|
||||
if (// Return if BE-user has no access rights to this field, @todo: another user access rights check!
|
||||
$parameterArray['fieldConf']['exclude'] && !$backendUser->check('non_exclude_fields', $table . ':' . $fieldName)
|
||||
// Return if field should not be rendered in translated records
|
||||
|| $isOverlay && empty($parameterArray['fieldConf']['l10n_display']) && $parameterArray['fieldConf']['l10n_mode'] === 'exclude'
|
||||
|| $this->inlineFieldShouldBeSkipped()
|
||||
) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
$parameterArray['fieldTSConfig'] = [];
|
||||
if (isset($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
|
||||
&& is_array($this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'])
|
||||
) {
|
||||
$parameterArray['fieldTSConfig'] = $this->data['pageTsConfig']['TCEFORM.'][$table . '.'][$fieldName . '.'];
|
||||
}
|
||||
if ($parameterArray['fieldTSConfig']['disabled']) {
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
// Override fieldConf by fieldTSconfig:
|
||||
$parameterArray['fieldConf']['config'] = FormEngineUtility::overrideFieldConf($parameterArray['fieldConf']['config'], $parameterArray['fieldTSConfig']);
|
||||
$parameterArray['itemFormElName'] = 'data[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
|
||||
$parameterArray['itemFormElID'] = 'data_' . $table . '_' . $row['uid'] . '_' . $fieldName;
|
||||
$newElementBaseName = $this->data['elementBaseName'] . '[' . $table . '][' . $row['uid'] . '][' . $fieldName . ']';
|
||||
|
||||
// The value to show in the form field.
|
||||
$parameterArray['itemFormElValue'] = $row[$fieldName];
|
||||
// Set field to read-only if configured for translated records to show default language content as readonly
|
||||
if ($parameterArray['fieldConf']['l10n_display']
|
||||
&& GeneralUtility::inList($parameterArray['fieldConf']['l10n_display'], 'defaultAsReadonly')
|
||||
&& $isOverlay
|
||||
) {
|
||||
$parameterArray['fieldConf']['config']['readOnly'] = true;
|
||||
$parameterArray['itemFormElValue'] = $this->data['defaultLanguageRow'][$fieldName];
|
||||
}
|
||||
|
||||
if (strpos($this->data['processedTca']['ctrl']['type'], ':') === false) {
|
||||
$typeField = $this->data['processedTca']['ctrl']['type'];
|
||||
} else {
|
||||
$typeField = substr($this->data['processedTca']['ctrl']['type'], 0, strpos($this->data['processedTca']['ctrl']['type'], ':'));
|
||||
}
|
||||
// Create a JavaScript code line which will ask the user to save/update the form due to changing the element.
|
||||
// This is used for eg. "type" fields and others configured with "onChange"
|
||||
if (!empty($this->data['processedTca']['ctrl']['type']) && $fieldName === $typeField
|
||||
|| isset($parameterArray['fieldConf']['onChange']) && $parameterArray['fieldConf']['onChange'] === 'reload'
|
||||
) {
|
||||
if ($backendUser->jsConfirmation(JsConfirmation::TYPE_CHANGE)) {
|
||||
$alertMsgOnChange = 'top.TYPO3.Modal.confirm('
|
||||
. 'TYPO3.lang["FormEngine.refreshRequiredTitle"],'
|
||||
. ' TYPO3.lang["FormEngine.refreshRequiredContent"]'
|
||||
. ')'
|
||||
. '.on('
|
||||
. '"button.clicked",'
|
||||
. ' function(e) { if (e.target.name == "ok" && TBE_EDITOR.checkSubmit(-1)) { TBE_EDITOR.submitForm() } top.TYPO3.Modal.dismiss(); }'
|
||||
. ');';
|
||||
} else {
|
||||
$alertMsgOnChange = 'if (TBE_EDITOR.checkSubmit(-1)){ TBE_EDITOR.submitForm() };';
|
||||
}
|
||||
} else {
|
||||
$alertMsgOnChange = '';
|
||||
}
|
||||
|
||||
// JavaScript code for event handlers:
|
||||
$parameterArray['fieldChangeFunc'] = [];
|
||||
$parameterArray['fieldChangeFunc']['TBE_EDITOR_fieldChanged'] = 'TBE_EDITOR.fieldChanged('
|
||||
. GeneralUtility::quoteJSvalue($table) . ','
|
||||
. GeneralUtility::quoteJSvalue($row['uid']) . ','
|
||||
. GeneralUtility::quoteJSvalue($fieldName) . ','
|
||||
. GeneralUtility::quoteJSvalue($parameterArray['itemFormElName'])
|
||||
. ');';
|
||||
if ($alertMsgOnChange) {
|
||||
$parameterArray['fieldChangeFunc']['alert'] = $alertMsgOnChange;
|
||||
}
|
||||
|
||||
// If this is the child of an inline type and it is the field creating the label
|
||||
if ($this->isInlineChildAndLabelField($table, $fieldName)) {
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
|
||||
$inlineDomObjectId = $inlineStackProcessor->getCurrentStructureDomObjectIdPrefix($this->data['inlineFirstPid']);
|
||||
$inlineObjectId = implode(
|
||||
'-',
|
||||
[
|
||||
$inlineDomObjectId,
|
||||
$table,
|
||||
$row['uid']
|
||||
]
|
||||
);
|
||||
$parameterArray['fieldChangeFunc']['inline'] = 'inline.handleChangedField('
|
||||
. GeneralUtility::quoteJSvalue($parameterArray['itemFormElName']) . ','
|
||||
. GeneralUtility::quoteJSvalue($inlineObjectId)
|
||||
. ');';
|
||||
}
|
||||
|
||||
// Based on the type of the item, call a render function on a child element
|
||||
$options = $this->data;
|
||||
$options['parameterArray'] = $parameterArray;
|
||||
$options['elementBaseName'] = $newElementBaseName;
|
||||
if (!empty($parameterArray['fieldConf']['config']['renderType'])) {
|
||||
$options['renderType'] = $parameterArray['fieldConf']['config']['renderType'];
|
||||
} else {
|
||||
// Fallback to type if no renderType is given
|
||||
$options['renderType'] = $parameterArray['fieldConf']['config']['type'];
|
||||
}
|
||||
$resultArray = $this->nodeFactory->create($options)->render();
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the $table is the child of an inline type AND the $field is the label field of this table.
|
||||
* This function is used to dynamically update the label while editing. This has no effect on labels,
|
||||
* that were processed by a FormEngine-hook on saving.
|
||||
*
|
||||
* @param string $table The table to check
|
||||
* @param string $field The field on this table to check
|
||||
* @return bool Is inline child and field is responsible for the label
|
||||
*/
|
||||
protected function isInlineChildAndLabelField($table, $field)
|
||||
{
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
|
||||
$level = $inlineStackProcessor->getStructureLevel(-1);
|
||||
if ($level['config']['foreign_label']) {
|
||||
$label = $level['config']['foreign_label'];
|
||||
} else {
|
||||
$label = $this->data['processedTca']['ctrl']['label'];
|
||||
}
|
||||
return $level['config']['foreign_table'] === $table && $label === $field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering of inline fields should be skipped under certain circumstances
|
||||
*
|
||||
* @return bool TRUE if field should be skipped based on inline configuration
|
||||
*/
|
||||
protected function inlineFieldShouldBeSkipped()
|
||||
{
|
||||
$table = $this->data['tableName'];
|
||||
$fieldName = $this->data['fieldName'];
|
||||
$fieldConfig = $this->data['processedTca']['columns'][$fieldName]['config'];
|
||||
|
||||
/** @var InlineStackProcessor $inlineStackProcessor */
|
||||
$inlineStackProcessor = GeneralUtility::makeInstance(InlineStackProcessor::class);
|
||||
$inlineStackProcessor->initializeByGivenStructure($this->data['inlineStructure']);
|
||||
$structureDepth = $inlineStackProcessor->getStructureDepth();
|
||||
|
||||
$skipThisField = false;
|
||||
if ($structureDepth > 0) {
|
||||
$searchArray = [
|
||||
'%OR' => [
|
||||
'config' => [
|
||||
0 => [
|
||||
'%AND' => [
|
||||
'foreign_table' => $table,
|
||||
'%OR' => [
|
||||
'%AND' => [
|
||||
'appearance' => ['useCombination' => true],
|
||||
'foreign_selector' => $fieldName
|
||||
],
|
||||
'MM' => $fieldConfig['MM']
|
||||
]
|
||||
]
|
||||
],
|
||||
1 => [
|
||||
'%AND' => [
|
||||
'foreign_table' => $fieldConfig['foreign_table'],
|
||||
'foreign_selector' => $fieldConfig['foreign_field']
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
// Get the parent record from structure stack
|
||||
$level = $inlineStackProcessor->getStructureLevel(-1);
|
||||
// If we have symmetric fields, check on which side we are and hide fields, that are set automatically:
|
||||
if ($this->data['isOnSymmetricSide']) {
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_field'] = $fieldName;
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['symmetric_sortby'] = $fieldName;
|
||||
} else {
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_field'] = $fieldName;
|
||||
$searchArray['%OR']['config'][0]['%AND']['%OR']['foreign_sortby'] = $fieldName;
|
||||
}
|
||||
$skipThisField = $this->arrayCompareComplex($level, $searchArray);
|
||||
}
|
||||
return $skipThisField;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles complex comparison requests on an array.
|
||||
* A request could look like the following:
|
||||
*
|
||||
* $searchArray = array(
|
||||
* '%AND' => array(
|
||||
* 'key1' => 'value1',
|
||||
* 'key2' => 'value2',
|
||||
* '%OR' => array(
|
||||
* 'subarray' => array(
|
||||
* 'subkey' => 'subvalue'
|
||||
* ),
|
||||
* 'key3' => 'value3',
|
||||
* 'key4' => 'value4'
|
||||
* )
|
||||
* )
|
||||
* );
|
||||
*
|
||||
* It is possible to use the array keys '%AND.1', '%AND.2', etc. to prevent
|
||||
* overwriting the sub-array. It could be necessary, if you use complex comparisons.
|
||||
*
|
||||
* The example above means, key1 *AND* key2 (and their values) have to match with
|
||||
* the $subjectArray and additional one *OR* key3 or key4 have to meet the same
|
||||
* condition.
|
||||
* It is also possible to compare parts of a sub-array (e.g. "subarray"), so this
|
||||
* function recurses down one level in that sub-array.
|
||||
*
|
||||
* @param array $subjectArray The array to search in
|
||||
* @param array $searchArray The array with keys and values to search for
|
||||
* @param string $type Use '%AND' or '%OR' for comparison
|
||||
* @return bool The result of the comparison
|
||||
*/
|
||||
protected function arrayCompareComplex($subjectArray, $searchArray, $type = '')
|
||||
{
|
||||
$localMatches = 0;
|
||||
$localEntries = 0;
|
||||
if (is_array($searchArray) && !empty($searchArray)) {
|
||||
// If no type was passed, try to determine
|
||||
if (!$type) {
|
||||
reset($searchArray);
|
||||
$type = key($searchArray);
|
||||
$searchArray = current($searchArray);
|
||||
}
|
||||
// We use '%AND' and '%OR' in uppercase
|
||||
$type = strtoupper($type);
|
||||
// Split regular elements from sub elements
|
||||
foreach ($searchArray as $key => $value) {
|
||||
$localEntries++;
|
||||
// Process a sub-group of OR-conditions
|
||||
if ($key === '%OR') {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%OR') ? 1 : 0;
|
||||
} elseif ($key === '%AND') {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray, $value, '%AND') ? 1 : 0;
|
||||
} elseif (is_array($value) && $this->isAssociativeArray($searchArray)) {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray[$key], $value, $type) ? 1 : 0;
|
||||
} elseif (is_array($value)) {
|
||||
$localMatches += $this->arrayCompareComplex($subjectArray, $value, $type) ? 1 : 0;
|
||||
} else {
|
||||
if (isset($subjectArray[$key]) && isset($value)) {
|
||||
// Boolean match:
|
||||
if (is_bool($value)) {
|
||||
$localMatches += !($subjectArray[$key] xor $value) ? 1 : 0;
|
||||
} elseif (is_numeric($subjectArray[$key]) && is_numeric($value)) {
|
||||
$localMatches += $subjectArray[$key] == $value ? 1 : 0;
|
||||
} else {
|
||||
$localMatches += $subjectArray[$key] === $value ? 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If one or more matches are required ('OR'), return TRUE after the first successful match
|
||||
if ($type === '%OR' && $localMatches > 0) {
|
||||
return true;
|
||||
}
|
||||
// If all matches are required ('AND') and we have no result after the first run, return FALSE
|
||||
if ($type === '%AND' && $localMatches == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return the result for '%AND' (if nothing was checked, TRUE is returned)
|
||||
return $localEntries === $localMatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an object is an associative array.
|
||||
*
|
||||
* @param mixed $object The object to be checked
|
||||
* @return bool Returns TRUE, if the object is an associative array
|
||||
*/
|
||||
protected function isAssociativeArray($object)
|
||||
{
|
||||
return is_array($object) && !empty($object) && array_keys($object) !== range(0, count($object) - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BackendUserAuthentication
|
||||
*/
|
||||
protected function getBackendUserAuthentication()
|
||||
{
|
||||
return $GLOBALS['BE_USER'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
Loaded 100 of 12716 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user