Commit fd5eae7e authored by Benni Mack's avatar Benni Mack
Browse files

[TASK] Rewrite Module Menu

Currently rendering of the module menu of the backend left side is
quite complex. This is because there are several parts introduced
in recent TYPO3 versions, that have only substituted parts of the
existing code. So the "old" code was still in use. To give a better
understanding I will first introduce all the parts relevant to the
rendering of the module menu.

PHP side
 * The mother of all classes is ModuleLoader.php, originally made by
   Kasper, which takes TBE_MODULES and compiles a long list of all
   modules into an array. (ModuleLoader->load()). Currently this class
   is kept as is for the time being.
 * For 4.1/4.2, the ModuleMenuView was introduced, a wrapper class to
   access the data of ModuleLoader and also does some other stuff like
   saving the Open/Collapsed state of the modules via AJAX (note: this
   will be handled browser-internally via LocalStorage from now on, as
   it saves HTTP requests).
   It does one other thing: Rendering of the Logout Button.
   This is completely in the wrong place right now, as the Logout
   button isn't within the module menu frame anymore since 4.x,
   but on the top right area of the backend.
 * The BackendController class is responsible for adding all HTML and
   JS code code to the backend.php page so it can be rendered. It
   instantiates the ModuleMenuView class to call the LogoutButton
   function.
 * As one of the first steps of rewriting the module menu a couple of
   versions ago, there was a ModuleController, which was again a
   wrapper for ModuleLoader while putting everything together in a
   ModuleRepository, along with Module objects and a singleton object
   ModuleStorage. This new version allows for nesting of up to three
   levels (incl. modfuncs), along with clear objects instead of arrays.

JS side
 * The module menu isn't output with a single line of PHP code, as it
   is defined as a ExtJS storage which fetches the module menu
   asynchronously. The HTML code is built in a ExtJS template, which
   leads to the funny effect that the raw backend.php only zero HTML
   code output for the module menu.
   An extra AJAX request is fired to load up the module, which then is
   prepared and templated by ExtJS.

The changes:
 * The logout button rendering is now done in the BackendController
   and not in the ModuleMenuView class.
 * The menu is now outputted in the BackendController at the same
   time as backend.php by a standalone Fluid template. The menu is
   built by a simple call to the ModuleRepository which returns a
   ModuleStorage.
 * ModuleMenuView, ModuleMenuController are not needed anymore, and
   are deprecated.
 * The JS is rewritten so that it works as before. These parts are filled
   up with jQuery code.
 * The collapsed/open state of the main modules is now done via the
   local storage JS functionality of the browsers to avoid further
   unnecessary HTTP loading operations.

Resolves: #60633
Releases: 6.3
Change-Id: I5402c1345a2931340d7ec9ef1881877b39d1bfc3
Reviewed-on: http://review.typo3.org/31876


Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Tested-by: Anja Leichsenring's avatarAnja Leichsenring <aleichsenring@ab-softlab.de>
Reviewed-by: Benni Mack's avatarBenjamin Mack <benni@typo3.org>
Tested-by: Benni Mack's avatarBenjamin Mack <benni@typo3.org>
parent c9849067
......@@ -68,6 +68,7 @@ TYPO3.Viewport.configuration = {
layout: 'fit',
region: 'west',
id: 'typo3-module-menu',
contentEl: 'typo3-menu',
collapsible: false,
collapseMode: null,
floatable: true,
......
......@@ -53,13 +53,6 @@ class BackendController {
*/
protected $moduleLoader;
/**
* module menu generating object
*
* @var \TYPO3\CMS\Backend\View\ModuleMenuView
*/
protected $moduleMenu;
/**
* Pagerenderer
*
......@@ -83,10 +76,10 @@ class BackendController {
// Initializes the backend modules structure for use later.
$this->moduleLoader = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleLoader');
$this->moduleLoader->load($GLOBALS['TBE_MODULES']);
$this->moduleMenu = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\View\\ModuleMenuView');
$this->pageRenderer = $GLOBALS['TBE_TEMPLATE']->getPageRenderer();
$this->pageRenderer->loadScriptaculous('builder,effects,controls,dragdrop');
$this->pageRenderer->loadExtJS();
$this->pageRenderer->loadJquery(NULL, NULL, \TYPO3\CMS\Core\Page\PageRenderer::JQUERY_NAMESPACE_DEFAULT_NOCONFLICT);
$this->pageRenderer->enableExtJSQuickTips();
$this->pageRenderer->addJsInlineCode('consoleOverrideWithDebugPanel', '//already done', FALSE);
$this->pageRenderer->addExtDirectCode();
......@@ -169,7 +162,8 @@ class BackendController {
<div id="typo3-top-container" class="x-hide-display">
<div id="typo3-logo">' . $logo->render() . '</div>
<div id="typo3-top" class="typo3-top-toolbar">' . $this->renderToolbar() . '</div>
</div>';
</div>
' . $this->generateModuleMenu();
/******************************************************
* Now put the complete backend document together
******************************************************/
......@@ -189,6 +183,7 @@ class BackendController {
$this->generateJavascript();
$this->pageRenderer->addJsInlineCode('BackendInlineJavascript', $this->js, FALSE);
$this->loadResourcesForRegisteredNavigationComponents();
// Add state provider
$GLOBALS['TBE_TEMPLATE']->setExtDirectStateProvider();
$states = $GLOBALS['BE_USER']->uc['BackendComponents']['States'];
......@@ -199,9 +194,11 @@ class BackendController {
autoRead: false
}));
';
if ($states) {
$extOnReadyCode .= 'Ext.state.Manager.getProvider().initState(' . json_encode($states) . ');';
}
$extOnReadyCode .= '
TYPO3.Backend = new TYPO3.Viewport(TYPO3.Viewport.configuration);
if (typeof console === "undefined") {
......@@ -281,7 +278,7 @@ class BackendController {
}
$toolbar = '<ul id="typo3-toolbar">';
$toolbar .= '<li>' . $this->getLoggedInUserLabel() . '</li>';
$toolbar .= '<li class="separator"><div id="logout-button" class="toolbar-item no-separator">' . $this->moduleMenu->renderLogoutButton() . '</div></li>';
$toolbar .= '<li class="separator"><div id="logout-button" class="toolbar-item no-separator">' . $this->renderLogoutButton() . '</div></li>';
$i = 0;
$numberOfToolbarItems = count($this->toolbarItems);
foreach ($this->toolbarItems as $key => $toolbarItem) {
......@@ -719,4 +716,49 @@ class BackendController {
}
}
/**
* renders the logout button form
*
* @return string Html code snippet displaying the logout button
*/
protected function renderLogoutButton() {
// show logout or "exit" (from switch user mode) label
$buttonLabel = 'LLL:EXT:lang/locallang_core.xlf:' . ($GLOBALS['BE_USER']->user['ses_backuserid'] ? 'buttons.exit' : 'buttons.logout');
$buttonForm = '
<form action="logout.php" target="_top">
<input type="submit" id="logout-submit-button" value="' . $GLOBALS['LANG']->sL($buttonLabel, TRUE) . '" />
</form>';
return $buttonForm;
}
/**
* loads all modules from the repository
* and renders it with a template
*
* @return string
*/
protected function generateModuleMenu() {
/** @var $moduleRepository \TYPO3\CMS\Backend\Domain\Repository\Module\BackendModuleRepository */
$moduleRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Domain\\Repository\\Module\\BackendModuleRepository');
$moduleStorage = $moduleRepository->loadAllowedModules();
/** @var $view \TYPO3\CMS\Fluid\View\StandaloneView */
$view = GeneralUtility::makeInstance('TYPO3\\CMS\\Fluid\\View\\StandaloneView');
$view->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName('EXT:backend/Resources/Private/Templates/ModuleMenu/Main.html'));
$view->assign('modules', $moduleStorage);
return $view->render();
}
/**
* Returns the Module menu for the AJAX API
*
* @param array $params
* @param \TYPO3\CMS\Core\Http\AjaxRequestHandler $ajaxRequestHandler
* @return void
*/
public function getModuleMenuForReload($params, $ajaxRequestHandler) {
$content = $this->generateModuleMenu();
$ajaxRequestHandler->addContent('menu', $content);
$ajaxRequestHandler->setContentFormat('json');
}
}
......@@ -22,42 +22,52 @@ namespace TYPO3\CMS\Backend\Domain\Model\Module;
class BackendModule {
/**
* @var string $title
* @var string
*/
protected $title = '';
/**
* @var string $name
* @var string
*/
protected $name = '';
/**
* @var array $icon
* @var array
*/
protected $icon = array();
/**
* @var string $link
* @var string
*/
protected $link = '';
/**
* @var string $onClick
* @var string
*/
protected $onClick = '';
/**
* @var string $description
* @var string
*/
protected $description = '';
/**
* @var string $navigationComponentId
* @var string
*/
protected $navigationComponentId = '';
/**
* @var \SplObjectStorage $children
* @var string
*/
protected $navigationFrameScript = '';
/**
* @var string
*/
protected $navigationFrameScriptParameters = '';
/**
* @var \SplObjectStorage
*/
protected $children;
......@@ -211,6 +221,34 @@ class BackendModule {
return $this->navigationComponentId;
}
/**
* @param string $navigationFrameScript
*/
public function setNavigationFrameScript($navigationFrameScript) {
$this->navigationFrameScript = $navigationFrameScript;
}
/**
* @return string
*/
public function getNavigationFrameScript() {
return $this->navigationFrameScript;
}
/**
* @param string $navigationFrameScriptParameters
*/
public function setNavigationFrameScriptParameters($navigationFrameScriptParameters) {
$this->navigationFrameScriptParameters = $navigationFrameScriptParameters;
}
/**
* @return string
*/
public function getNavigationFrameScriptParameters() {
return $this->navigationFrameScriptParameters;
}
/**
* Set onClick
*
......
......@@ -14,23 +14,40 @@ namespace TYPO3\CMS\Backend\Domain\Repository\Module;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Backend\Utility\BackendUtility;
/**
* Repository for backend module menu
* compiles all data from $GLOBALS[TBE_MODULES]
*
* @author Susanne Moog <typo3@susannemoog.de>
*/
class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface {
/**
* @var \TYPO3\CMS\Backend\Module\ModuleStorage $moduleMenu
* @var \TYPO3\CMS\Backend\Module\ModuleStorage
*/
protected $moduleMenu;
protected $moduleStorage;
/**
* Constructs the module menu and gets the Singleton instance of the menu
*/
public function __construct() {
$this->moduleMenu = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleStorage');
$this->moduleStorage = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleStorage');
}
/**
* loads all module information in the module storage
*
* @return \SplObjectStorage
*/
public function loadAllowedModules() {
$rawData = $this->getRawModuleMenuData();
$this->convertRawModuleDataToModuleMenuObject($rawData);
$this->createMenuEntriesForTbeModulesExt();
return $this->moduleStorage->getEntries();
}
/**
......@@ -40,7 +57,7 @@ class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface {
* @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule|boolean
*/
public function findByModuleName($name) {
$entries = $this->moduleMenu->getEntries();
$entries = $this->moduleStorage->getEntries();
$entry = $this->findByModuleNameInGivenEntries($name, $entries);
return $entry;
}
......@@ -68,4 +85,287 @@ class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface {
return FALSE;
}
/**
* Creates the module menu object structure from the raw data array
*
* @param array $rawModuleData
* @return void
*/
protected function convertRawModuleDataToModuleMenuObject(array $rawModuleData) {
foreach ($rawModuleData as $module) {
$entry = $this->createEntryFromRawData($module);
if (isset($module['subitems']) && !empty($module['subitems'])) {
foreach ($module['subitems'] as $subitem) {
$subEntry = $this->createEntryFromRawData($subitem);
$entry->addChild($subEntry);
}
}
$this->moduleStorage->attachEntry($entry);
}
}
/**
* Creates a menu entry object from an array
*
* @param array $module
* @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule
*/
protected function createEntryFromRawData(array $module) {
/** @var $entry \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule */
$entry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Domain\\Model\\Module\\BackendModule');
if (!empty($module['name']) && is_string($module['name'])) {
$entry->setName($module['name']);
}
if (!empty($module['title']) && is_string($module['title'])) {
$entry->setTitle($this->getLanguageService()->sL($module['title']));
}
if (!empty($module['onclick']) && is_string($module['onclick'])) {
$entry->setOnClick($module['onclick']);
}
if (!empty($module['link']) && is_string($module['link'])) {
$entry->setLink($module['link']);
} elseif (empty($module['link']) && !empty($module['path']) && is_string($module['path'])) {
$entry->setLink($module['path']);
}
if (!empty($module['description']) && is_string($module['description'])) {
$entry->setDescription($module['description']);
}
if (!empty($module['icon']) && is_array($module['icon'])) {
$entry->setIcon($module['icon']);
}
if (!empty($module['navigationComponentId']) && is_string($module['navigationComponentId'])) {
$entry->setNavigationComponentId($module['navigationComponentId']);
}
if (!empty($module['navigationFrameScript']) && is_string($module['navigationFrameScript'])) {
$entry->setNavigationFrameScript($module['navigationFrameScript']);
} elseif (!empty($module['parentNavigationFrameScript']) && is_string($module['parentNavigationFrameScript'])) {
$entry->setNavigationFrameScript($module['parentNavigationFrameScript']);
}
if (!empty($module['navigationFrameScriptParam']) && is_string($module['navigationFrameScriptParam'])) {
$entry->setNavigationFrameScriptParameters($module['navigationFrameScriptParam']);
}
return $entry;
}
/**
* Creates the "third level" menu entries (submodules for the info module for
* example) from the TBE_MODULES_EXT array
*
* @return void
*/
protected function createMenuEntriesForTbeModulesExt() {
foreach ($GLOBALS['TBE_MODULES_EXT'] as $mainModule => $tbeModuleExt) {
list($main) = explode('_', $mainModule);
$mainEntry = $this->findByModuleName($main);
if ($mainEntry === FALSE) {
continue;
}
$subEntries = $mainEntry->getChildren();
if (empty($subEntries)) {
continue;
}
$matchingSubEntry = $this->findByModuleName($mainModule);
if ($matchingSubEntry !== FALSE) {
if (isset($tbeModuleExt['MOD_MENU']) && isset($tbeModuleExt['MOD_MENU']['function'])) {
foreach ($tbeModuleExt['MOD_MENU']['function'] as $subModule) {
$entry = $this->createEntryFromRawData($subModule);
$matchingSubEntry->addChild($entry);
}
}
}
}
}
/**
* Return language service instance
*
* @return \TYPO3\CMS\Lang\LanguageService
*/
protected function getLanguageService() {
return $GLOBALS['LANG'];
}
/**
* loads the module menu from the moduleloader based on $GLOBALS['TBE_MODULES']
* and compiles an array with all the data needed for menu etc.
*
* @return array
*/
public function getRawModuleMenuData() {
// Loads the backend modules available for the logged in user.
$moduleLoader = GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleLoader');
$moduleLoader->observeWorkspaces = TRUE;
$moduleLoader->load($GLOBALS['TBE_MODULES']);
$loadedModules = $moduleLoader->modules;
$modules = array();
// Unset modules that are meant to be hidden from the menu.
$loadedModules = $this->removeHiddenModules($loadedModules);
foreach ($loadedModules as $moduleName => $moduleData) {
$moduleLink = '';
if (!is_array($moduleData['sub'])) {
$moduleLink = $moduleData['script'];
}
$moduleLink = GeneralUtility::resolveBackPath($moduleLink);
$moduleKey = 'modmenu_' . $moduleName;
$moduleIcon = $this->getModuleIcon($moduleKey);
$modules[$moduleKey] = array(
'name' => $moduleName,
'title' => $GLOBALS['LANG']->moduleLabels['tabs'][$moduleName . '_tab'],
'onclick' => 'top.goToModule(\'' . $moduleName . '\');',
'icon' => $moduleIcon,
'link' => $moduleLink,
'description' => $GLOBALS['LANG']->moduleLabels['labels'][$moduleKey . 'label']
);
if (!is_array($moduleData['sub']) && $moduleData['script'] !== 'dummy.php') {
// Work around for modules with own main entry, but being self the only submodule
$modules[$moduleKey]['subitems'][$moduleKey] = array(
'name' => $moduleName,
'title' => $GLOBALS['LANG']->moduleLabels['tabs'][$moduleName . '_tab'],
'onclick' => 'top.goToModule(\'' . $moduleName . '\');',
'icon' => $this->getModuleIcon($moduleName . '_tab'),
'link' => $moduleLink,
'originalLink' => $moduleLink,
'description' => $GLOBALS['LANG']->moduleLabels['labels'][$moduleKey . 'label'],
'navigationFrameScript' => NULL,
'navigationFrameScriptParam' => NULL,
'navigationComponentId' => NULL
);
} elseif (is_array($moduleData['sub'])) {
foreach ($moduleData['sub'] as $submoduleName => $submoduleData) {
if (isset($submoduleData['script'])) {
$submoduleLink = GeneralUtility::resolveBackPath($submoduleData['script']);
} else {
$submoduleLink = BackendUtility::getModuleUrl($submoduleData['name']);
}
$submoduleKey = $moduleName . '_' . $submoduleName . '_tab';
$submoduleIcon = $this->getModuleIcon($submoduleKey);
$submoduleDescription = $GLOBALS['LANG']->moduleLabels['labels'][$submoduleKey . 'label'];
$originalLink = $submoduleLink;
if (isset($submoduleData['navigationFrameModule'])) {
$navigationFrameScript = BackendUtility::getModuleUrl(
$submoduleData['navigationFrameModule'],
isset($submoduleData['navigationFrameModuleParameters'])
? $submoduleData['navigationFrameModuleParameters']
: array()
);
} else {
$navigationFrameScript = $submoduleData['navFrameScript'];
}
$modules[$moduleKey]['subitems'][$submoduleKey] = array(
'name' => $moduleName . '_' . $submoduleName,
'title' => $GLOBALS['LANG']->moduleLabels['tabs'][$submoduleKey],
'onclick' => 'top.goToModule(\'' . $moduleName . '_' . $submoduleName . '\');',
'icon' => $submoduleIcon,
'link' => $submoduleLink,
'originalLink' => $originalLink,
'description' => $submoduleDescription,
'navigationFrameScript' => $navigationFrameScript,
'navigationFrameScriptParam' => $submoduleData['navFrameScriptParam'],
'navigationComponentId' => $submoduleData['navigationComponentId']
);
// if the main module has a navframe script, inherit to the submodule,
// but only if it is not disabled explicitly (option is set to FALSE)
if ($moduleData['navFrameScript'] && $submoduleData['inheritNavigationComponentFromMainModule'] !== FALSE) {
$modules[$moduleKey]['subitems'][$submoduleKey]['parentNavigationFrameScript'] = $moduleData['navFrameScript'];
}
}
}
}
return $modules;
}
/**
* Reads User configuration from options.hideModules and removes
* modules accordingly.
*
* @param array $loadedModules
* @return array
*/
protected function removeHiddenModules($loadedModules) {
$hiddenModules = $GLOBALS['BE_USER']->getTSConfig('options.hideModules');
// Hide modules if set in userTS.
if (!empty($hiddenModules['value'])) {
$hiddenMainModules = explode(',', $hiddenModules['value']);
foreach ($hiddenMainModules as $hiddenMainModule) {
unset($loadedModules[trim($hiddenMainModule)]);
}
}
// Hide sub-modules if set in userTS.
if (!empty($hiddenModules['properties']) && is_array($hiddenModules['properties'])) {
foreach ($hiddenModules['properties'] as $mainModuleName => $subModules) {
$hiddenSubModules = explode(',', $subModules);
foreach ($hiddenSubModules as $hiddenSubModule) {
unset($loadedModules[$mainModuleName]['sub'][trim($hiddenSubModule)]);
}
}
}
return $loadedModules;
}
/**
* gets the module icon and its size
*
* @param string $moduleKey Module key
* @return array Icon data array with 'filename', 'size', and 'html'
*/
protected function getModuleIcon($moduleKey) {
$icon = array(
'filename' => '',
'size' => '',
'title' => '',
'html' => ''
);
if (!empty($GLOBALS['LANG']->moduleLabels['tabs_images'][$moduleKey])) {
$imageReference = $GLOBALS['LANG']->moduleLabels['tabs_images'][$moduleKey];
$iconFileRelative = $this->getModuleIconRelative($imageReference);
if (!empty($iconFileRelative)) {
$iconTitle = $GLOBALS['LANG']->moduleLabels['tabs'][$moduleKey];
$iconFileAbsolute = $this->getModuleIconAbsolute($imageReference);
$iconSizes = @getimagesize($iconFileAbsolute);
$icon['filename'] = $iconFileRelative;
$icon['size'] = $iconSizes[3];
$icon['title'] = htmlspecialchars($iconTitle);
$icon['html'] = '<img src="' . $iconFileRelative . '" ' . $iconSizes[3] . ' title="' . htmlspecialchars($iconTitle) . '" alt="' . htmlspecialchars($iconTitle) . '" />';
}
}
return $icon;
}
/**
* Returns the filename readable for the script from PATH_typo3.
* That means absolute names are just returned while relative names are
* prepended with the path pointing back to typo3/ dir
*
* @param string $iconFilename Icon filename
* @return string Icon filename with absolute path
* @see getModuleIconRelative()
*/
protected function getModuleIconAbsolute($iconFilename) {
if (!GeneralUtility::isAbsPath($iconFilename)) {
$iconFilename = $GLOBALS['BACK_PATH'] . $iconFilename;
}
return $iconFilename;
}
/**
* Returns relative path to the icon filename for use in img-tags
*
* @param string $iconFilename Icon filename
* @return string Icon filename with relative path
* @see getModuleIconAbsolute()
*/
protected function getModuleIconRelative($iconFilename) {
if (GeneralUtility::isAbsPath($iconFilename)) {
$iconFilename = '../' . \TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($iconFilename);
}
return $iconFilename;
}
}
......@@ -33,8 +33,10 @@ class ModuleController {
/**
* Constructor
* @deprecated since TYPO3 CMS 6.3, not in use, as everything can be done via the ModuleMenuRepository directly
*/
public function __construct() {
\TYPO3\CMS\Core\Utility\GeneralUtility::logDeprecatedFunction();
$this->moduleMenu = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Module\\ModuleStorage');
$this->moduleMenuRepository = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Backend\\Domain\\Repository\\Module\\BackendModuleRepository');
}
......
......@@ -39,8 +39,10 @@ class ModuleMenuView {
/**
* Constructor, initializes several variables
* @deprecated since TYPO3 CMS 6.3, not in use, as everything can be done via the ModuleMenuRepository directly