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