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