first commit

This commit is contained in:
salam committed 2026-10-19 08:18:39 +02:00
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;
}
}
@@ -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'];
}
}
@@ -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'];
}
}
@@ -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;
}
}
@@ -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>&nbsp;' . 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