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,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;
}
}