[BUGFIX] Make empty $GLOBALS['TBE_MODULES_EXT'] traversable
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Domain / Repository / Module / BackendModuleRepository.php
1 <?php
2 namespace TYPO3\CMS\Backend\Domain\Repository\Module;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Module\ModuleLoader;
18 use TYPO3\CMS\Core\Imaging\IconFactory;
19 use TYPO3\CMS\Core\Imaging\IconRegistry;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21
22 /**
23 * Repository for backend module menu
24 * compiles all data from $GLOBALS[TBE_MODULES]
25 */
26 class BackendModuleRepository implements \TYPO3\CMS\Core\SingletonInterface
27 {
28 /**
29 * @var \TYPO3\CMS\Backend\Module\ModuleStorage
30 */
31 protected $moduleStorage;
32
33 /**
34 * Constructs the module menu and gets the Singleton instance of the menu
35 */
36 public function __construct()
37 {
38 $this->moduleStorage = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Module\ModuleStorage::class);
39
40 $rawData = $this->getRawModuleMenuData();
41
42 $this->convertRawModuleDataToModuleMenuObject($rawData);
43 $this->createMenuEntriesForTbeModulesExt();
44 }
45
46 /**
47 * loads all module information in the module storage
48 *
49 * @param array $excludeGroupNames
50 * @return \SplObjectStorage
51 */
52 public function loadAllowedModules(array $excludeGroupNames = [])
53 {
54 if (empty($excludeGroupNames)) {
55 return $this->moduleStorage->getEntries();
56 }
57
58 $modules = new \SplObjectStorage();
59 foreach ($this->moduleStorage->getEntries() as $moduleGroup) {
60 if (!in_array($moduleGroup->getName(), $excludeGroupNames, true)) {
61 if ($moduleGroup->getChildren()->count() > 0) {
62 $modules->attach($moduleGroup);
63 }
64 }
65 }
66
67 return $modules;
68 }
69
70 /**
71 * @param string $groupName
72 * @return \SplObjectStorage|false
73 **/
74 public function findByGroupName($groupName = '')
75 {
76 foreach ($this->moduleStorage->getEntries() as $moduleGroup) {
77 if ($moduleGroup->getName() === $groupName) {
78 return $moduleGroup;
79 }
80 }
81
82 return false;
83 }
84
85 /**
86 * Finds a module menu entry by name
87 *
88 * @param string $name
89 * @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule|bool
90 */
91 public function findByModuleName($name)
92 {
93 $entries = $this->moduleStorage->getEntries();
94 $entry = $this->findByModuleNameInGivenEntries($name, $entries);
95 return $entry;
96 }
97
98 /**
99 * Finds a module menu entry by name in a given storage
100 *
101 * @param string $name
102 * @param \SplObjectStorage $entries
103 * @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule|bool
104 */
105 public function findByModuleNameInGivenEntries($name, \SplObjectStorage $entries)
106 {
107 foreach ($entries as $entry) {
108 if ($entry->getName() === $name) {
109 return $entry;
110 }
111 $children = $entry->getChildren();
112 if (!empty($children)) {
113 $childRecord = $this->findByModuleNameInGivenEntries($name, $children);
114 if ($childRecord !== false) {
115 return $childRecord;
116 }
117 }
118 }
119 return false;
120 }
121
122 /**
123 * Creates the module menu object structure from the raw data array
124 *
125 * @param array $rawModuleData
126 */
127 protected function convertRawModuleDataToModuleMenuObject(array $rawModuleData)
128 {
129 foreach ($rawModuleData as $module) {
130 $entry = $this->createEntryFromRawData($module);
131 if (isset($module['subitems']) && !empty($module['subitems'])) {
132 foreach ($module['subitems'] as $subitem) {
133 $subEntry = $this->createEntryFromRawData($subitem);
134 $entry->addChild($subEntry);
135 }
136 }
137 $this->moduleStorage->attachEntry($entry);
138 }
139 }
140
141 /**
142 * Creates a menu entry object from an array
143 *
144 * @param array $module
145 * @return \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule
146 */
147 protected function createEntryFromRawData(array $module)
148 {
149 /** @var $entry \TYPO3\CMS\Backend\Domain\Model\Module\BackendModule */
150 $entry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Domain\Model\Module\BackendModule::class);
151 if (!empty($module['name']) && is_string($module['name'])) {
152 $entry->setName($module['name']);
153 }
154 if (!empty($module['title']) && is_string($module['title'])) {
155 $entry->setTitle($this->getLanguageService()->sL($module['title']));
156 }
157 if (!empty($module['onclick']) && is_string($module['onclick'])) {
158 $entry->setOnClick($module['onclick']);
159 }
160 if (!empty($module['link']) && is_string($module['link'])) {
161 $entry->setLink($module['link']);
162 } elseif (empty($module['link']) && !empty($module['path']) && is_string($module['path'])) {
163 $entry->setLink($module['path']);
164 }
165 if (!empty($module['description']) && is_string($module['description'])) {
166 $entry->setDescription($this->getLanguageService()->sL($module['description']));
167 }
168 if (!empty($module['icon'])) {
169 $entry->setIcon($module['icon']);
170 }
171 if (!empty($module['navigationComponentId']) && is_string($module['navigationComponentId'])) {
172 $entry->setNavigationComponentId($module['navigationComponentId']);
173 }
174 if (!empty($module['navigationFrameScript']) && is_string($module['navigationFrameScript'])) {
175 $entry->setNavigationFrameScript($module['navigationFrameScript']);
176 } elseif (!empty($module['parentNavigationFrameScript']) && is_string($module['parentNavigationFrameScript'])) {
177 $entry->setNavigationFrameScript($module['parentNavigationFrameScript']);
178 }
179 if (!empty($module['navigationFrameScriptParam']) && is_string($module['navigationFrameScriptParam'])) {
180 $entry->setNavigationFrameScriptParameters($module['navigationFrameScriptParam']);
181 }
182 return $entry;
183 }
184
185 /**
186 * Creates the "third level" menu entries (submodules for the info module for
187 * example) from the TBE_MODULES_EXT array
188 */
189 protected function createMenuEntriesForTbeModulesExt()
190 {
191 foreach ($GLOBALS['TBE_MODULES_EXT'] ?? [] as $mainModule => $tbeModuleExt) {
192 list($main) = explode('_', $mainModule);
193 $mainEntry = $this->findByModuleName($main);
194 if ($mainEntry === false) {
195 continue;
196 }
197
198 $subEntries = $mainEntry->getChildren();
199 if (empty($subEntries)) {
200 continue;
201 }
202 $matchingSubEntry = $this->findByModuleName($mainModule);
203 if ($matchingSubEntry !== false) {
204 if (isset($tbeModuleExt['MOD_MENU']) && isset($tbeModuleExt['MOD_MENU']['function'])) {
205 foreach ($tbeModuleExt['MOD_MENU']['function'] as $subModule) {
206 $entry = $this->createEntryFromRawData($subModule);
207 $matchingSubEntry->addChild($entry);
208 }
209 }
210 }
211 }
212 }
213
214 /**
215 * Return language service instance
216 *
217 * @return \TYPO3\CMS\Core\Localization\LanguageService
218 */
219 protected function getLanguageService()
220 {
221 return $GLOBALS['LANG'];
222 }
223
224 /**
225 * loads the module menu from the moduleloader based on $GLOBALS['TBE_MODULES']
226 * and compiles an array with all the data needed for menu etc.
227 *
228 * @return array
229 */
230 public function getRawModuleMenuData()
231 {
232 // Loads the backend modules available for the logged in user.
233 /** @var ModuleLoader $moduleLoader */
234 $moduleLoader = GeneralUtility::makeInstance(ModuleLoader::class);
235 $moduleLoader->observeWorkspaces = true;
236 $moduleLoader->load($GLOBALS['TBE_MODULES']);
237 $loadedModules = $moduleLoader->modules;
238
239 $modules = [];
240
241 // Unset modules that are meant to be hidden from the menu.
242 $loadedModules = $this->removeHiddenModules($loadedModules);
243 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
244 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
245 $dummyScript = (string)$uriBuilder->buildUriFromRoute('dummy');
246 foreach ($loadedModules as $moduleName => $moduleData) {
247 $moduleLink = '';
248 if (!is_array($moduleData['sub'])) {
249 $moduleLink = $moduleData['script'];
250 }
251 $moduleLink = GeneralUtility::resolveBackPath($moduleLink);
252 $moduleLabels = $moduleLoader->getLabelsForModule($moduleName);
253 $moduleKey = 'modmenu_' . $moduleName;
254 $modules[$moduleKey] = [
255 'name' => $moduleName,
256 'title' => $moduleLabels['title'],
257 'onclick' => 'top.goToModule(' . GeneralUtility::quoteJSvalue($moduleName) . ');',
258 'icon' => $this->getModuleIcon($moduleKey, $moduleData),
259 'link' => $moduleLink,
260 'description' => $moduleLabels['shortdescription']
261 ];
262 if (!is_array($moduleData['sub']) && $moduleData['script'] !== $dummyScript) {
263 // Work around for modules with own main entry, but being self the only submodule
264 $modules[$moduleKey]['subitems'][$moduleKey] = [
265 'name' => $moduleName,
266 'title' => $moduleLabels['title'],
267 'onclick' => 'top.goToModule(' . GeneralUtility::quoteJSvalue($moduleName) . ');',
268 'icon' => $this->getModuleIcon($moduleKey, $moduleData),
269 'link' => $moduleLink,
270 'originalLink' => $moduleLink,
271 'description' => $moduleLabels['shortdescription'],
272 'navigationFrameScript' => null,
273 'navigationFrameScriptParam' => null,
274 'navigationComponentId' => null
275 ];
276 } elseif (is_array($moduleData['sub'])) {
277 foreach ($moduleData['sub'] as $submoduleName => $submoduleData) {
278 if (isset($submoduleData['script'])) {
279 $submoduleLink = GeneralUtility::resolveBackPath($submoduleData['script']);
280 } else {
281 $submoduleLink = (string)$uriBuilder->buildUriFromRoute($submoduleData['name']);
282 }
283 $submoduleKey = $moduleName . '_' . $submoduleName;
284 $submoduleLabels = $moduleLoader->getLabelsForModule($submoduleKey);
285 $submoduleDescription = $submoduleLabels['shortdescription'];
286 $originalLink = $submoduleLink;
287 $navigationFrameScript = $submoduleData['navFrameScript'];
288 $modules[$moduleKey]['subitems'][$submoduleKey] = [
289 'name' => $moduleName . '_' . $submoduleName,
290 'title' => $submoduleLabels['title'],
291 'onclick' => 'top.goToModule(' . GeneralUtility::quoteJSvalue($moduleName . '_' . $submoduleName) . ');',
292 'icon' => $this->getModuleIcon($moduleKey, $submoduleData),
293 'link' => $submoduleLink,
294 'originalLink' => $originalLink,
295 'description' => $submoduleDescription,
296 'navigationFrameScript' => $navigationFrameScript,
297 'navigationFrameScriptParam' => $submoduleData['navFrameScriptParam'],
298 'navigationComponentId' => $submoduleData['navigationComponentId']
299 ];
300 // if the main module has a navframe script, inherit to the submodule,
301 // but only if it is not disabled explicitly (option is set to FALSE)
302 if ($moduleData['navFrameScript'] && $submoduleData['inheritNavigationComponentFromMainModule'] !== false) {
303 $modules[$moduleKey]['subitems'][$submoduleKey]['parentNavigationFrameScript'] = $moduleData['navFrameScript'];
304 }
305 }
306 }
307 }
308 return $modules;
309 }
310
311 /**
312 * Reads User configuration from options.hideModules and removes
313 * modules accordingly.
314 *
315 * @param array $loadedModules
316 * @return array
317 */
318 protected function removeHiddenModules($loadedModules)
319 {
320 $hiddenModules = $GLOBALS['BE_USER']->getTSConfig('options.hideModules');
321
322 // Hide modules if set in userTS.
323 if (!empty($hiddenModules['value'])) {
324 $hiddenMainModules = explode(',', $hiddenModules['value']);
325 foreach ($hiddenMainModules as $hiddenMainModule) {
326 unset($loadedModules[trim($hiddenMainModule)]);
327 }
328 }
329
330 // Hide sub-modules if set in userTS.
331 if (!empty($hiddenModules['properties']) && is_array($hiddenModules['properties'])) {
332 foreach ($hiddenModules['properties'] as $mainModuleName => $subModules) {
333 $hiddenSubModules = explode(',', $subModules);
334 foreach ($hiddenSubModules as $hiddenSubModule) {
335 unset($loadedModules[$mainModuleName]['sub'][trim($hiddenSubModule)]);
336 }
337 }
338 }
339
340 return $loadedModules;
341 }
342
343 /**
344 * gets the module icon
345 *
346 * @param string $moduleKey Module key
347 * @param array $moduleData the compiled data associated with it
348 * @return string Icon data, either sprite or <img> tag
349 */
350 protected function getModuleIcon($moduleKey, $moduleData)
351 {
352 $iconIdentifier = !(empty($moduleData['iconIdentifier']))
353 ? $moduleData['iconIdentifier']
354 : 'module-icon-' . $moduleKey;
355 $iconRegistry = GeneralUtility::makeInstance(IconRegistry::class);
356 if ($iconRegistry->isRegistered($iconIdentifier)) {
357 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
358 return $iconFactory->getIcon($iconIdentifier)->render();
359 }
360 return '';
361 }
362 }