'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[] = '
  • '; if ($hasDropDown) { $toolbar[] = ''; $toolbar[] = $toolbarItem->getItem(); $toolbar[] = ''; $toolbar[] = ''; } else { $toolbar[] = $toolbarItem->getItem(); } $toolbar[] = '
  • '; } } 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']; } }