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