[TASK] Cleanup navigation frame module registration
[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\Core\Utility\PathUtility;
22 use TYPO3\CMS\Lang\LanguageService;
23
24 /**
25 * This document provides a class that loads the modules for the TYPO3 interface.
26 *
27 * Load Backend Interface modules
28 *
29 * Typically instantiated like this:
30 * $this->loadModules = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Module\ModuleLoader::class);
31 * $this->loadModules->load($TBE_MODULES);
32 * @internal
33 */
34 class ModuleLoader {
35
36 /**
37 * After the init() function this array will contain the structure of available modules for the backend user.
38 *
39 * @var array
40 */
41 public $modules = array();
42
43 /**
44 * Array with paths pointing to the location of modules from extensions
45 *
46 * @var array
47 */
48 public $absPathArray = array();
49
50 /**
51 * This array will hold the elements that should go into the select-list of modules for groups...
52 *
53 * @var array
54 */
55 public $modListGroup = array();
56
57 /**
58 * This array will hold the elements that should go into the select-list of modules for users...
59 *
60 * @var array
61 */
62 public $modListUser = array();
63
64 /**
65 * The backend user for use internally
66 *
67 * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
68 */
69 public $BE_USER;
70
71 /**
72 * If set TRUE, workspace "permissions" will be observed so non-allowed modules will not be included in the array of modules.
73 *
74 * @var bool
75 */
76 public $observeWorkspaces = FALSE;
77
78 /**
79 * Contains the registered navigation components
80 *
81 * @var array
82 */
83 protected $navigationComponents = array();
84
85 /**
86 * Init.
87 * The outcome of the load() function will be a $this->modules array populated with the backend module structure available to the BE_USER
88 * Further the global var $LANG will have labels and images for the modules loaded in an internal array.
89 *
90 * @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)
91 * @param BackendUserAuthentication $beUser Optional backend user object to use. If not set, the global BE_USER object is used.
92 * @return void
93 */
94 public function load($modulesArray, BackendUserAuthentication $beUser = NULL) {
95 // Setting the backend user for use internally
96 $this->BE_USER = $beUser ?: $GLOBALS['BE_USER'];
97
98 /*$modulesArray might look like this when entering this function.
99 Notice the two modules added by extensions - they have a path attachedArray
100 (
101 [web] => list,info,perm,func
102 [file] => list
103 [user] =>
104 [tools] => em,install,txphpmyadmin
105 [help] => about
106 [_PATHS] => Array
107 (
108 [system_install] => /www/htdocs/typo3/32/coreinstall/typo3/ext/install/mod/
109 [tools_txphpmyadmin] => /www/htdocs/typo3/32/coreinstall/typo3/ext/phpmyadmin/modsub/
110 ))
111 */
112 $this->absPathArray = $modulesArray['_PATHS'];
113 unset($modulesArray['_PATHS']);
114 // Unset the array for calling external backend module dispatchers in typo3/index.php
115 // (unused in Core, but in case some extension still sets this, we unset that)
116 unset($modulesArray['_dispatcher']);
117 // Unset the array for calling backend modules based on external backend module dispatchers in typo3/index.php
118 unset($modulesArray['_configuration']);
119 $this->navigationComponents = $modulesArray['_navigationComponents'];
120 unset($modulesArray['_navigationComponents']);
121 $theMods = $this->parseModulesArray($modulesArray);
122 // Originally modules were found in typo3/mod/
123 // User defined modules were found in ../typo3conf/
124 // Today almost all modules reside in extensions and they are found by the _PATHS array of the incoming $TBE_MODULES array
125 // Setting paths for 1) core modules (old concept from mod/) and 2) user-defined modules (from ../typo3conf)
126 $paths = array();
127 // Path of static modules
128 $paths['defMods'] = PATH_typo3 . 'mod/';
129 // Local modules (maybe frontend specific)
130 $paths['userMods'] = PATH_typo3 . '../typo3conf/';
131 // Traverses the module setup and creates the internal array $this->modules
132 foreach ($theMods as $mods => $subMod) {
133 $path = NULL;
134 $extModRelPath = $this->checkExtensionModule($mods);
135 // EXTENSION module:
136 if ($extModRelPath) {
137 $theMainMod = $this->checkMod($mods, PATH_site . $extModRelPath);
138 if (is_array($theMainMod) || $theMainMod != 'notFound') {
139 // ... just so it goes on... submodules cannot be within this path!
140 $path = 1;
141 }
142 } else {
143 // 'CLASSIC' module
144 // Checking for typo3/mod/ module existence...
145 $theMainMod = $this->checkMod($mods, $paths['defMods'] . $mods);
146 if (is_array($theMainMod) || $theMainMod != 'notFound') {
147 $path = $paths['defMods'];
148 } else {
149 // If not typo3/mod/ then it could be user-defined in typo3conf/ ...?
150 $theMainMod = $this->checkMod($mods, $paths['userMods'] . $mods);
151 if (is_array($theMainMod) || $theMainMod != 'notFound') {
152 $path = $paths['userMods'];
153 }
154 }
155 }
156 // If $theMainMod is not set (FALSE) there is no access to the module !(?)
157 if ($theMainMod && !is_null($path)) {
158 $this->modules[$mods] = $theMainMod;
159 // SUBMODULES - if any - are loaded
160 if (is_array($subMod)) {
161 foreach ($subMod as $valsub) {
162 $extModRelPath = $this->checkExtensionModule($mods . '_' . $valsub);
163 if ($extModRelPath) {
164 // EXTENSION submodule:
165 $theTempSubMod = $this->checkMod($mods . '_' . $valsub, PATH_site . $extModRelPath);
166 // Default sub-module in either main-module-path, be it the default or the userdefined.
167 if (is_array($theTempSubMod)) {
168 $this->modules[$mods]['sub'][$valsub] = $theTempSubMod;
169 }
170 } else {
171 // 'CLASSIC' submodule
172 // Checking for typo3/mod/xxx/ module existence...
173 // @todo what about $path = 1; from above and using $path as string here?
174 $theTempSubMod = $this->checkMod($mods . '_' . $valsub, $path . $mods . '/' . $valsub);
175 // Default sub-module in either main-module-path, be it the default or the userdefined.
176 if (is_array($theTempSubMod)) {
177 $this->modules[$mods]['sub'][$valsub] = $theTempSubMod;
178 } elseif ($path == $paths['defMods']) {
179 // If the submodule did not exist in the default module path, then check if there is a submodule in the submodule path!
180 $theTempSubMod = $this->checkMod($mods . '_' . $valsub, $paths['userMods'] . $mods . '/' . $valsub);
181 if (is_array($theTempSubMod)) {
182 $this->modules[$mods]['sub'][$valsub] = $theTempSubMod;
183 }
184 }
185 }
186 }
187 }
188 } else {
189 // This must be done in order to fill out the select-lists for modules correctly!!
190 if (is_array($subMod)) {
191 foreach ($subMod as $valsub) {
192 // @todo path can only be NULL here, or not?
193 $this->checkMod($mods . '_' . $valsub, $path . $mods . '/' . $valsub);
194 }
195 }
196 }
197 }
198 }
199
200 /**
201 * If the module name ($name) is a module from an extension (has path in $this->absPathArray)
202 * then that path is returned relative to PATH_site
203 *
204 * @param string $name Module name
205 * @return string If found, the relative path from PATH_site
206 */
207 public function checkExtensionModule($name) {
208 if (isset($this->absPathArray[$name])) {
209 return rtrim(PathUtility::stripPathSitePrefix($this->absPathArray[$name]), '/');
210 }
211 return '';
212 }
213
214 /**
215 * Here we check for the module.
216 *
217 * Return values:
218 * 'notFound': If the module was not found in the path (no "conf.php" file)
219 * FALSE: If no access to the module (access check failed)
220 * array(): Configuration array, in case a valid module where access IS granted exists.
221 *
222 * @param string $name Module name
223 * @param string $fullPath Absolute path to module
224 * @return string|bool|array See description of function
225 */
226 public function checkMod($name, $fullPath) {
227 if ($name === 'user_ws' && !ExtensionManagementUtility::isLoaded('version')) {
228 return FALSE;
229 }
230 // Check for own way of configuring module
231 if (is_array($GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction'])) {
232 $obj = $GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction'];
233 if (is_callable($obj)) {
234 $MCONF = call_user_func($obj, $name, $fullPath);
235 if ($this->checkModAccess($name, $MCONF) !== TRUE) {
236 return FALSE;
237 }
238 return $MCONF;
239 }
240 }
241
242 // merges $MCONF and $MLANG from conf.php and the additional configuration of the module
243 $setupInformation = $this->getModuleSetupInformation($name, $fullPath);
244
245 // Because 'path/../path' does not work
246 // clean up the configuration part
247 if (empty($setupInformation['configuration'])) {
248 return 'notFound';
249 }
250 if (
251 $setupInformation['configuration']['shy']
252 || !$this->checkModAccess($name, $setupInformation['configuration'])
253 || !$this->checkModWorkspace($name, $setupInformation['configuration'])
254 ) {
255 return FALSE;
256 }
257 $finalModuleConfiguration = $setupInformation['configuration'];
258 $finalModuleConfiguration['name'] = $name;
259 // Language processing. This will add module labels and image reference to the internal ->moduleLabels array of the LANG object.
260 $lang = $this->getLanguageService();
261 if (is_object($lang)) {
262 // $setupInformation['labels']['default']['tabs_images']['tab'] is for modules the reference
263 // to the module icon.
264 $defaultLabels = $setupInformation['labels']['default'];
265
266 // Here the path is transformed to an absolute reference.
267 if ($defaultLabels['tabs_images']['tab']) {
268 // Initializing search for alternative icon:
269 // Alternative icon key (might have an alternative set in $TBE_STYLES['skinImg']
270 $altIconKey = 'MOD:' . $name . '/' . $defaultLabels['tabs_images']['tab'];
271 $altIconAbsPath = is_array($GLOBALS['TBE_STYLES']['skinImg'][$altIconKey]) ? GeneralUtility::resolveBackPath(PATH_typo3 . $GLOBALS['TBE_STYLES']['skinImg'][$altIconKey][0]) : '';
272 // Setting icon, either default or alternative:
273 if ($altIconAbsPath && @is_file($altIconAbsPath)) {
274 $defaultLabels['tabs_images']['tab'] = $altIconAbsPath;
275 } else {
276 if (\TYPO3\CMS\Core\Utility\StringUtility::beginsWith($defaultLabels['tabs_images']['tab'], 'EXT:')) {
277 list($extensionKey, $relativePath) = explode('/', substr($defaultLabels['tabs_images']['tab'], 4), 2);
278 $defaultLabels['tabs_images']['tab'] = ExtensionManagementUtility::extPath($extensionKey) . $relativePath;
279 } else {
280 $defaultLabels['tabs_images']['tab'] = $fullPath . '/' . $defaultLabels['tabs_images']['tab'];
281 }
282 }
283
284 $defaultLabels['tabs_images']['tab'] = $this->getRelativePath(PATH_typo3, $defaultLabels['tabs_images']['tab']);
285
286 // Finally, setting the icon with correct path:
287 if (substr($defaultLabels['tabs_images']['tab'], 0, 3) === '../') {
288 $defaultLabels['tabs_images']['tab'] = PATH_site . substr($defaultLabels['tabs_images']['tab'], 3);
289 } else {
290 $defaultLabels['tabs_images']['tab'] = PATH_typo3 . $defaultLabels['tabs_images']['tab'];
291 }
292 }
293
294 // If LOCAL_LANG references are used for labels of the module:
295 if ($defaultLabels['ll_ref']) {
296 // Now the 'default' key is loaded with the CURRENT language - not the english translation...
297 $defaultLabels['labels']['tablabel'] = $lang->sL($defaultLabels['ll_ref'] . ':mlang_labels_tablabel');
298 $defaultLabels['labels']['tabdescr'] = $lang->sL($defaultLabels['ll_ref'] . ':mlang_labels_tabdescr');
299 $defaultLabels['tabs']['tab'] = $lang->sL($defaultLabels['ll_ref'] . ':mlang_tabs_tab');
300 $lang->addModuleLabels($defaultLabels, $name . '_');
301 } else {
302 // ... otherwise use the old way:
303 $lang->addModuleLabels($defaultLabels, $name . '_');
304 $lang->addModuleLabels($setupInformation['labels'][$lang->lang], $name . '_');
305 }
306 }
307
308 // Default script setup
309 if ($setupInformation['configuration']['script'] === '_DISPATCH') {
310 if ($setupInformation['configuration']['extbase']) {
311 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl('Tx_' . $name);
312 } else {
313 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl($name);
314 }
315 } elseif ($setupInformation['configuration']['script'] && file_exists($setupInformation['path'] . '/' . $setupInformation['configuration']['script'])) {
316 $finalModuleConfiguration['script'] = $this->getRelativePath(PATH_typo3, $fullPath . '/' . $setupInformation['configuration']['script']);
317 } else {
318 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl('dummy');
319 }
320
321 if (!empty($setupInformation['configuration']['navigationFrameModule'])) {
322 $finalModuleConfiguration['navFrameScript'] = BackendUtility::getModuleUrl(
323 $setupInformation['configuration']['navigationFrameModule'],
324 !empty($setupInformation['configuration']['navigationFrameModuleParameters'])
325 ? $setupInformation['configuration']['navigationFrameModuleParameters']
326 : array()
327 );
328 } elseif (!empty($setupInformation['configuration']['navFrameScript'])) {
329 // Navigation Frame Script (GET params could be added)
330 $navFrameScript = explode('?', $setupInformation['configuration']['navFrameScript']);
331 $navFrameScript = $navFrameScript[0];
332 if (file_exists($setupInformation['path'] . '/' . $navFrameScript)) {
333 $finalModuleConfiguration['navFrameScript'] = $this->getRelativePath(PATH_typo3, $fullPath . '/' . $setupInformation['configuration']['navFrameScript']);
334 }
335 // Additional params for Navigation Frame Script: "&anyParam=value&moreParam=1"
336 if ($setupInformation['configuration']['navFrameScriptParam']) {
337 $finalModuleConfiguration['navFrameScriptParam'] = $setupInformation['configuration']['navFrameScriptParam'];
338 }
339 }
340
341 // Check if this is a submodule
342 $mainModule = '';
343 if (strpos($name, '_') !== FALSE) {
344 list($mainModule, ) = explode('_', $name, 2);
345 }
346
347 // check if there is a navigation component (like the pagetree)
348 if (is_array($this->navigationComponents[$name])) {
349 $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$name]['componentId'];
350 // navigation component can be overriden by the main module component
351 } elseif ($mainModule && is_array($this->navigationComponents[$mainModule]) && $setupInformation['configuration']['inheritNavigationComponentFromMainModule'] !== FALSE) {
352 $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$mainModule]['componentId'];
353 }
354 return $finalModuleConfiguration;
355 }
356
357 /**
358 * fetches the conf.php file of a certain module, and also merges that with
359 * some additional configuration
360 *
361 * @param \string $moduleName the combined name of the module, can be "web", "web_info", or "tools_log"
362 * @param \string $pathToModuleDirectory the path where the module data is put, used for the conf.php or the modScript
363 * @return array an array with subarrays, named "configuration" (aka $MCONF), "labels" (previously known as $MLANG) and the stripped path
364 */
365 protected function getModuleSetupInformation($moduleName, $pathToModuleDirectory) {
366
367 // Because 'path/../path' does not work
368 $path = preg_replace('/\\/[^\\/.]+\\/\\.\\.\\//', '/', $pathToModuleDirectory);
369
370 $moduleSetupInformation = array(
371 'configuration' => array(),
372 'labels' => array(),
373 'path' => $path
374 );
375
376 if (@is_dir($path) && file_exists($path . '/conf.php')) {
377 $MCONF = array();
378 $MLANG = array();
379
380 // The conf-file is included. This must be valid PHP.
381 include $path . '/conf.php';
382
383 // Move the global variables defined in conf.php into the local method
384 if (is_array($MCONF)) {
385 $moduleSetupInformation['configuration'] = $MCONF;
386 } else {
387 $moduleSetupInformation['configuration'] = array();
388 }
389 $moduleSetupInformation['labels'] = $MLANG;
390 }
391
392 $moduleConfiguration = !empty($GLOBALS['TBE_MODULES']['_configuration'][$moduleName])
393 ? $GLOBALS['TBE_MODULES']['_configuration'][$moduleName]
394 : NULL;
395 if ($moduleConfiguration !== NULL) {
396 // Overlay setup with additional labels
397 if (!empty($moduleConfiguration['labels']) && is_array($moduleConfiguration['labels'])) {
398 if (empty($moduleSetupInformation['labels']['default']) || !is_array($moduleSetupInformation['labels']['default'])) {
399 $moduleSetupInformation['labels']['default'] = $moduleConfiguration['labels'];
400 } else {
401 $moduleSetupInformation['labels']['default'] = array_replace_recursive($moduleSetupInformation['labels']['default'], $moduleConfiguration['labels']);
402 }
403 unset($moduleConfiguration['labels']);
404 }
405 // Overlay setup with additional configuration
406 if (is_array($moduleConfiguration)) {
407 $moduleSetupInformation['configuration'] = array_replace_recursive($moduleSetupInformation['configuration'], $moduleConfiguration);
408 }
409 }
410
411 // Add some default configuration
412 if (!isset($moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'])) {
413 $moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'] = TRUE;
414 }
415
416 return $moduleSetupInformation;
417 }
418
419 /**
420 * Returns TRUE if the internal BE_USER has access to the module $name with $MCONF (based on security level set for that module)
421 *
422 * @param string $name Module name
423 * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about what access level the module has)
424 * @return bool TRUE if access is granted for $this->BE_USER
425 */
426 public function checkModAccess($name, $MCONF) {
427 if (empty($MCONF['access'])) {
428 return TRUE;
429 }
430 $access = strtolower($MCONF['access']);
431 // Checking if admin-access is required
432 // If admin-permissions is required then return TRUE if user is admin
433 if (strpos($access, 'admin') !== FALSE && $this->BE_USER->isAdmin()) {
434 return TRUE;
435 }
436 // This will add modules to the select-lists of user and groups
437 if (strpos($access, 'user') !== FALSE) {
438 $this->modListUser[] = $name;
439 }
440 if (strpos($access, 'group') !== FALSE) {
441 $this->modListGroup[] = $name;
442 }
443 // This checks if a user is permitted to access the module
444 if ($this->BE_USER->isAdmin() || $this->BE_USER->check('modules', $name)) {
445 return TRUE;
446 }
447 return FALSE;
448 }
449
450 /**
451 * Check if a module is allowed inside the current workspace for be user
452 * Processing happens only if $this->observeWorkspaces is TRUE
453 *
454 * @param string $name Module name (unused)
455 * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about workspace restrictions)
456 * @return bool TRUE if access is granted for $this->BE_USER
457 */
458 public function checkModWorkspace($name, $MCONF) {
459 if (!$this->observeWorkspaces) {
460 return TRUE;
461 }
462 $status = TRUE;
463 if (!empty($MCONF['workspaces'])) {
464 $status = $this->BE_USER->workspace === 0 && GeneralUtility::inList($MCONF['workspaces'], 'online')
465 || $this->BE_USER->workspace === -1 && GeneralUtility::inList($MCONF['workspaces'], 'offline')
466 || $this->BE_USER->workspace > 0 && GeneralUtility::inList($MCONF['workspaces'], 'custom');
467 } elseif ($this->BE_USER->workspace === -99) {
468 $status = FALSE;
469 }
470 return $status;
471 }
472
473 /**
474 * Parses the moduleArray ($TBE_MODULES) into an internally useful structure.
475 * Returns an array where the keys are names of the module and the values may be TRUE (only module) or an array (of submodules)
476 *
477 * @param array $arr ModuleArray ($TBE_MODULES)
478 * @return array Output structure with available modules
479 */
480 public function parseModulesArray($arr) {
481 $theMods = array();
482 if (is_array($arr)) {
483 foreach ($arr as $mod => $subs) {
484 // Clean module name to alphanum
485 $mod = $this->cleanName($mod);
486 if ($mod) {
487 if ($subs) {
488 $subsArr = GeneralUtility::trimExplode(',', $subs);
489 foreach ($subsArr as $subMod) {
490 $subMod = $this->cleanName($subMod);
491 if ($subMod) {
492 $theMods[$mod][] = $subMod;
493 }
494 }
495 } else {
496 $theMods[$mod] = 1;
497 }
498 }
499 }
500 }
501 return $theMods;
502 }
503
504 /**
505 * The $str is cleaned so that it contains alphanumerical characters only.
506 * Module names must only consist of these characters
507 *
508 * @param string $str String to clean up
509 * @return string
510 */
511 public function cleanName($str) {
512 return preg_replace('/[^a-z0-9]/i', '', $str);
513 }
514
515 /**
516 * Get relative path for $destDir compared to $baseDir
517 *
518 * @param string $baseDir Base directory
519 * @param string $destDir Destination directory
520 * @return string The relative path of destination compared to base.
521 */
522 public function getRelativePath($baseDir, $destDir) {
523 // A special case, the dirs are equal
524 if ($baseDir === $destDir) {
525 return './';
526 }
527 // Remove beginning
528 $baseDir = ltrim($baseDir, '/');
529 $destDir = ltrim($destDir, '/');
530 $found = TRUE;
531 do {
532 $slash_pos = strpos($destDir, '/');
533 if ($slash_pos !== FALSE && substr($destDir, 0, $slash_pos) == substr($baseDir, 0, $slash_pos)) {
534 $baseDir = substr($baseDir, $slash_pos + 1);
535 $destDir = substr($destDir, $slash_pos + 1);
536 } else {
537 $found = FALSE;
538 }
539 } while ($found);
540 $slashes = strlen($baseDir) - strlen(str_replace('/', '', $baseDir));
541 for ($i = 0; $i < $slashes; $i++) {
542 $destDir = '../' . $destDir;
543 }
544 return GeneralUtility::resolveBackPath($destDir);
545 }
546
547 /**
548 * @return LanguageService
549 */
550 protected function getLanguageService() {
551 return $GLOBALS['LANG'];
552 }
553
554 }