[BUGFIX] Fix some PHP Notices thrown when rendering page module
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Module / ModuleLoader.php
1 <?php
2 namespace TYPO3\CMS\Backend\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\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
19 use TYPO3\CMS\Core\Localization\LanguageService;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21
22 /**
23 * This document provides a class that loads the modules for the TYPO3 interface.
24 *
25 * Load Backend Interface modules
26 *
27 * Typically instantiated like this:
28 * $this->loadModules = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Module\ModuleLoader::class);
29 * $this->loadModules->load($TBE_MODULES);
30 * @internal
31 */
32 class ModuleLoader
33 {
34 /**
35 * After the init() function this array will contain the structure of available modules for the backend user.
36 *
37 * @var array
38 */
39 public $modules = [];
40
41 /**
42 * This array will hold the elements that should go into the select-list of modules for groups...
43 *
44 * @var array
45 */
46 public $modListGroup = [];
47
48 /**
49 * This array will hold the elements that should go into the select-list of modules for users...
50 *
51 * @var array
52 */
53 public $modListUser = [];
54
55 /**
56 * The backend user for use internally
57 *
58 * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
59 */
60 public $BE_USER;
61
62 /**
63 * If set TRUE, workspace "permissions" will be observed so non-allowed modules will not be included in the array of modules.
64 *
65 * @var bool
66 */
67 public $observeWorkspaces = false;
68
69 /**
70 * Contains the registered navigation components
71 *
72 * @var array
73 */
74 protected $navigationComponents = [];
75
76 /**
77 * Labels for the modules
78 * @var array
79 */
80 protected $moduleLabels = [];
81
82 /**
83 * Init.
84 * The outcome of the load() function will be a $this->modules array populated with the backend module structure available to the BE_USER
85 * Further the global var $LANG will have labels and images for the modules loaded in an internal array.
86 *
87 * @param array $modulesArray Should be the global var $TBE_MODULES, $BE_USER can optionally be set to an alternative Backend user object than the global var $BE_USER (which is the currently logged in user)
88 * @param BackendUserAuthentication $beUser Optional backend user object to use. If not set, the global BE_USER object is used.
89 */
90 public function load($modulesArray, BackendUserAuthentication $beUser = null)
91 {
92 // Setting the backend user for use internally
93 $this->BE_USER = $beUser ?: $GLOBALS['BE_USER'];
94
95 // Unset the array for calling backend modules based on external backend module dispatchers in typo3/index.php
96 unset($modulesArray['_configuration']);
97 $this->navigationComponents = $modulesArray['_navigationComponents'];
98 unset($modulesArray['_navigationComponents']);
99 $mainModules = $this->parseModulesArray($modulesArray);
100
101 // Traverses the module setup and creates the internal array $this->modules
102 foreach ($mainModules as $mainModuleName => $subModules) {
103 $mainModuleConfiguration = $this->checkMod($mainModuleName);
104 // If $mainModuleConfiguration is not set (FALSE) there is no access to the module !(?)
105 if (is_array($mainModuleConfiguration)) {
106 $this->modules[$mainModuleName] = $mainModuleConfiguration;
107 // Load the submodules
108 if (is_array($subModules)) {
109 foreach ($subModules as $subModuleName) {
110 $subModuleConfiguration = $this->checkMod($mainModuleName . '_' . $subModuleName);
111 if (is_array($subModuleConfiguration)) {
112 $this->modules[$mainModuleName]['sub'][$subModuleName] = $subModuleConfiguration;
113 }
114 }
115 }
116 } elseif ($mainModuleConfiguration !== false) {
117 // Although the configuration was not found, still check if there are submodules
118 // This must be done in order to fill out the select-lists for modules correctly!!
119 if (is_array($subModules)) {
120 foreach ($subModules as $subModuleName) {
121 $this->checkMod($mainModuleName . '_' . $subModuleName);
122 }
123 }
124 }
125 }
126 }
127
128 /**
129 * Here we check for the module.
130 *
131 * Return values:
132 * 'notFound': If the module was not found in the path (no "conf.php" file)
133 * FALSE: If no access to the module (access check failed)
134 * array(): Configuration array, in case a valid module where access IS granted exists.
135 *
136 * @param string $name Module name
137 * @return string|bool|array See description of function
138 */
139 public function checkMod($name)
140 {
141 // Check for own way of configuring module
142 if (is_array($GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction'] ?? false)) {
143 $obj = $GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction'];
144 if (is_callable($obj)) {
145 $MCONF = call_user_func($obj, $name);
146 if ($this->checkModAccess($name, $MCONF) !== true) {
147 return false;
148 }
149 $this->addLabelsForModule($name, $MCONF['labels']);
150 return $MCONF;
151 }
152 }
153
154 // merge configuration and labels into one array
155 $setupInformation = $this->getModuleSetupInformation($name);
156
157 // clean up the configuration part
158 if (empty($setupInformation['configuration'])) {
159 return 'notFound';
160 }
161 $finalModuleConfiguration = $setupInformation['configuration'];
162 if (!empty($finalModuleConfiguration['shy'])
163 || !$this->checkModAccess($name, $setupInformation['configuration'])
164 || !$this->checkModWorkspace($name, $setupInformation['configuration'])
165 ) {
166 return false;
167 }
168 $finalModuleConfiguration['name'] = $name;
169 // Language processing. This will add module labels and image reference to the internal ->moduleLabels array of the LANG object.
170 $this->addLabelsForModule($name, ($finalModuleConfiguration['labels'] ?? $setupInformation['labels']));
171
172 // Default script setup
173 if (!empty($setupInformation['configuration']['script']) && $setupInformation['configuration']['script'] === '_DISPATCH' || isset($setupInformation['configuration']['routeTarget'])) {
174 if ($setupInformation['configuration']['extbase']) {
175 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl('Tx_' . $name);
176 } else {
177 // just go through BackendModuleRequestHandler where the routeTarget is resolved
178 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl($name);
179 }
180 } else {
181 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl('dummy');
182 }
183
184 if (!empty($setupInformation['configuration']['navigationFrameModule'])) {
185 $finalModuleConfiguration['navFrameScript'] = BackendUtility::getModuleUrl(
186 $setupInformation['configuration']['navigationFrameModule'],
187 !empty($setupInformation['configuration']['navigationFrameModuleParameters'])
188 ? $setupInformation['configuration']['navigationFrameModuleParameters']
189 : []
190 );
191 }
192
193 // Check if this is a submodule
194 $mainModule = '';
195 if (strpos($name, '_') !== false) {
196 list($mainModule, ) = explode('_', $name, 2);
197 }
198
199 // check if there is a navigation component (like the pagetree)
200 if (is_array($this->navigationComponents[$name] ?? false)) {
201 $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$name]['componentId'];
202 } elseif ($mainModule
203 && is_array($this->navigationComponents[$mainModule] ?? false)
204 && $setupInformation['configuration']['inheritNavigationComponentFromMainModule'] !== false) {
205
206 // navigation component can be overridden by the main module component
207 $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$mainModule]['componentId'];
208 }
209 return $finalModuleConfiguration;
210 }
211
212 /**
213 * fetches the conf.php file of a certain module, and also merges that with
214 * some additional configuration
215 *
216 * @param string $moduleName the combined name of the module, can be "web", "web_info", or "tools_log"
217 * @return array an array with subarrays, named "configuration" (aka $MCONF), "labels" (previously known as $MLANG) and the stripped path
218 */
219 protected function getModuleSetupInformation($moduleName)
220 {
221 $moduleSetupInformation = [
222 'configuration' => [],
223 'labels' => []
224 ];
225
226 $moduleConfiguration = !empty($GLOBALS['TBE_MODULES']['_configuration'][$moduleName])
227 ? $GLOBALS['TBE_MODULES']['_configuration'][$moduleName]
228 : null;
229 if ($moduleConfiguration !== null) {
230 // Overlay setup with additional labels
231 if (!empty($moduleConfiguration['labels']) && is_array($moduleConfiguration['labels'])) {
232 if (empty($moduleSetupInformation['labels']['default']) || !is_array($moduleSetupInformation['labels']['default'])) {
233 $moduleSetupInformation['labels']['default'] = $moduleConfiguration['labels'];
234 } else {
235 $moduleSetupInformation['labels']['default'] = array_replace_recursive($moduleSetupInformation['labels']['default'], $moduleConfiguration['labels']);
236 }
237 unset($moduleConfiguration['labels']);
238 }
239 // Overlay setup with additional configuration
240 if (is_array($moduleConfiguration)) {
241 $moduleSetupInformation['configuration'] = array_replace_recursive($moduleSetupInformation['configuration'], $moduleConfiguration);
242 }
243 }
244
245 // Add some default configuration
246 if (!isset($moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'])) {
247 $moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'] = true;
248 }
249
250 return $moduleSetupInformation;
251 }
252
253 /**
254 * Returns TRUE if the internal BE_USER has access to the module $name with $MCONF (based on security level set for that module)
255 *
256 * @param string $name Module name
257 * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about what access level the module has)
258 * @return bool TRUE if access is granted for $this->BE_USER
259 */
260 public function checkModAccess($name, $MCONF)
261 {
262 if (empty($MCONF['access'])) {
263 return true;
264 }
265 $access = strtolower($MCONF['access']);
266 // Checking if admin-access is required
267 // If admin-permissions is required then return TRUE if user is admin
268 if (strpos($access, 'admin') !== false && $this->BE_USER->isAdmin()) {
269 return true;
270 }
271 // This will add modules to the select-lists of user and groups
272 if (strpos($access, 'user') !== false) {
273 $this->modListUser[] = $name;
274 }
275 if (strpos($access, 'group') !== false) {
276 $this->modListGroup[] = $name;
277 }
278 // This checks if a user is permitted to access the module
279 if ($this->BE_USER->isAdmin() || $this->BE_USER->check('modules', $name)) {
280 return true;
281 }
282 return false;
283 }
284
285 /**
286 * Check if a module is allowed inside the current workspace for be user
287 * Processing happens only if $this->observeWorkspaces is TRUE
288 *
289 * @param string $name Module name (unused)
290 * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about workspace restrictions)
291 * @return bool TRUE if access is granted for $this->BE_USER
292 */
293 public function checkModWorkspace($name, $MCONF)
294 {
295 if (!$this->observeWorkspaces) {
296 return true;
297 }
298 $status = true;
299 if (!empty($MCONF['workspaces'])) {
300 $status = $this->BE_USER->workspace === 0 && GeneralUtility::inList($MCONF['workspaces'], 'online')
301 || $this->BE_USER->workspace === -1 && GeneralUtility::inList($MCONF['workspaces'], 'offline')
302 || $this->BE_USER->workspace > 0 && GeneralUtility::inList($MCONF['workspaces'], 'custom');
303 } elseif ($this->BE_USER->workspace === -99) {
304 $status = false;
305 }
306 return $status;
307 }
308
309 /**
310 * Parses the moduleArray ($TBE_MODULES) into an internally useful structure.
311 * Returns an array where the keys are names of the module and the values may be TRUE (only module) or an array (of submodules)
312 *
313 * @param array $arr ModuleArray ($TBE_MODULES)
314 * @return array Output structure with available modules
315 */
316 public function parseModulesArray($arr)
317 {
318 $theMods = [];
319 if (is_array($arr)) {
320 foreach ($arr as $mod => $subs) {
321 // Clean module name to alphanum
322 $mod = $this->cleanName($mod);
323 if ($mod) {
324 if ($subs) {
325 $subsArr = GeneralUtility::trimExplode(',', $subs);
326 foreach ($subsArr as $subMod) {
327 $subMod = $this->cleanName($subMod);
328 if ($subMod) {
329 $theMods[$mod][] = $subMod;
330 }
331 }
332 } else {
333 $theMods[$mod] = 1;
334 }
335 }
336 }
337 }
338 return $theMods;
339 }
340
341 /**
342 * The $str is cleaned so that it contains alphanumerical characters only.
343 * Module names must only consist of these characters
344 *
345 * @param string $str String to clean up
346 * @return string
347 */
348 public function cleanName($str)
349 {
350 return preg_replace('/[^a-z0-9]/i', '', $str);
351 }
352
353 /**
354 * Registers labels for a module in a unified way.
355 *
356 * Legacy info: This was previously named
357 * - labels->tablabel (now called "shortdescription")
358 * - labels->tabdescr (now called "description")
359 * - tabs->tab (now called "title")
360 *
361 * The LLL information is stored, not the actual translated string.
362 *
363 * @param string $moduleName the name of the module
364 * @param string|array $labels the information about the three labels
365 */
366 public function addLabelsForModule($moduleName, $labels)
367 {
368 // If LOCAL_LANG references are used for labels of the module:
369 if (is_string($labels)) {
370 // Extbase-based modules
371 $this->moduleLabels[$moduleName] = [
372 'shortdescription' => $labels . ':mlang_labels_tablabel',
373 'description' => $labels . ':mlang_labels_tabdescr',
374 'title' => $labels . ':mlang_tabs_tab',
375 ];
376 } elseif (isset($labels['title'])) {
377 // New way, where all labels can be LLL references
378 $this->moduleLabels[$moduleName] = $labels;
379 } elseif (isset($labels['ll_ref'])) {
380 // Classic, non-extbase module labels
381 $this->addLabelsForModule($moduleName, $labels['ll_ref']);
382 } else {
383 // Very old obsolete approach, don't use anymore, use one of the ways above.
384 if (is_object($this->getLanguageService())) {
385 $language = $this->getLanguageService()->lang;
386 } else {
387 $language = 'default';
388 }
389
390 if (empty($labels)) {
391 if (isset($this->getLanguageService()->moduleLabels['labels'][$moduleName . '_tablabel'])) {
392 $labels[$language]['labels']['tablabel'] = $this->getLanguageService()->moduleLabels['labels'][$moduleName . '_tablabel'];
393 }
394 if (isset($this->getLanguageService()->moduleLabels['labels'][$moduleName . '_tabdescr'])) {
395 $labels[$language]['labels']['tabdescr'] = $this->getLanguageService()->moduleLabels['labels'][$moduleName . '_tabdescr'];
396 }
397 if (isset($this->getLanguageService()->moduleLabels['tabs'][$moduleName . '_tab'])) {
398 $labels[$language]['tabs']['tab'] = $this->getLanguageService()->moduleLabels['tabs'][$moduleName . '_tab'];
399 }
400 }
401
402 if (isset($labels[$language]['ll_ref'])) {
403 $this->addLabelsForModule($moduleName, $labels[$language]['ll_ref']);
404 } elseif (isset($labels['default']['ll_ref'])) {
405 $this->addLabelsForModule($moduleName, $labels['default']['ll_ref']);
406 } else {
407 $this->moduleLabels[$moduleName] = [
408 'shortdescription' => isset($labels[$language]['labels']['tablabel']) ? $labels[$language]['labels']['tablabel'] : $labels['default']['labels']['tablabel'],
409 'description' => isset($labels[$language]['labels']['tabdescr']) ? $labels[$language]['labels']['tabdescr'] : $labels['default']['labels']['tabdescr'],
410 'title' => isset($labels[$language]['tabs']['tab']) ? $labels[$language]['tabs']['tab'] : $labels['default']['tabs']['tab'],
411 ];
412 }
413 }
414 }
415
416 /**
417 * Returns the labels for the given module
418 *
419 * @param string $moduleName
420 * @return array
421 */
422 public function getLabelsForModule($moduleName)
423 {
424 return isset($this->moduleLabels[$moduleName]) ? $this->moduleLabels[$moduleName] : [];
425 }
426
427 /**
428 * @return LanguageService
429 */
430 protected function getLanguageService()
431 {
432 return $GLOBALS['LANG'];
433 }
434 }