[TASK] Turn todos into @todo to find them easier
[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\Utility\GeneralUtility;
19
20 /**
21 * This document provides a class that loads the modules for the TYPO3 interface.
22 *
23 * Load Backend Interface modules
24 *
25 * Typically instantiated like this:
26 * $this->loadModules = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Module\ModuleLoader::class);
27 * $this->loadModules->load($TBE_MODULES);
28 *
29 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
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 = array();
40
41 /**
42 * Array with paths pointing to the location of modules from extensions
43 *
44 * @var array
45 */
46 public $absPathArray = array();
47
48 /**
49 * This array will hold the elements that should go into the select-list of modules for groups...
50 *
51 * @var array
52 */
53 public $modListGroup = array();
54
55 /**
56 * This array will hold the elements that should go into the select-list of modules for users...
57 *
58 * @var array
59 */
60 public $modListUser = array();
61
62 /**
63 * The backend user for use internally
64 *
65 * @var \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
66 */
67 public $BE_USER;
68
69 /**
70 * If set TRUE, workspace "permissions" will be observed so non-allowed modules will not be included in the array of modules.
71 *
72 * @var bool
73 */
74 public $observeWorkspaces = FALSE;
75
76 /**
77 * Contains the registered navigation components
78 *
79 * @var array
80 */
81 protected $navigationComponents = array();
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 object $BE_USER 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, $BE_USER = '') {
93 // Setting the backend user for use internally
94 if (is_object($BE_USER)) {
95 $this->BE_USER = $BE_USER;
96 } else {
97 $this->BE_USER = $GLOBALS['BE_USER'];
98 }
99 /*$modulesArray might look like this when entering this function.
100 Notice the two modules added by extensions - they have a path attachedArray
101 (
102 [web] => list,info,perm,func
103 [file] => list
104 [user] =>
105 [tools] => em,install,txphpmyadmin
106 [help] => about
107 [_PATHS] => Array
108 (
109 [system_install] => /www/htdocs/typo3/32/coreinstall/typo3/ext/install/mod/
110 [tools_txphpmyadmin] => /www/htdocs/typo3/32/coreinstall/typo3/ext/phpmyadmin/modsub/
111 ))
112 */
113 $this->absPathArray = $modulesArray['_PATHS'];
114 unset($modulesArray['_PATHS']);
115 // Unset the array for calling external backend module dispatchers in typo3/mod.php
116 unset($modulesArray['_dispatcher']);
117 // Unset the array for calling backend modules based on external backend module dispatchers in typo3/mod.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) then that path is returned relative to PATH_site
202 *
203 * @param string $name Module name
204 * @return string If found, the relative path from PATH_site
205 */
206 public function checkExtensionModule($name) {
207 if (isset($this->absPathArray[$name])) {
208 return rtrim(\TYPO3\CMS\Core\Utility\PathUtility::stripPathSitePrefix($this->absPathArray[$name]), '/');
209 }
210 }
211
212 /**
213 * Here we check for the module.
214 * Return values:
215 * 'notFound': If the module was not found in the path (no "conf.php" file)
216 * FALSE: If no access to the module (access check failed)
217 * array(): Configuration array, in case a valid module where access IS granted exists.
218 *
219 * @param string $name Module name
220 * @param string $fullpath Absolute path to module
221 * @return mixed See description of function
222 */
223 public function checkMod($name, $fullpath) {
224 if ($name == 'user_ws' && !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('version')) {
225 return FALSE;
226 }
227 // Check for own way of configuring module
228 if (is_array($GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction'])) {
229 $obj = $GLOBALS['TBE_MODULES']['_configuration'][$name]['configureModuleFunction'];
230 if (is_callable($obj)) {
231 $MCONF = call_user_func($obj, $name, $fullpath);
232 if ($this->checkModAccess($name, $MCONF) !== TRUE) {
233 return FALSE;
234 }
235 return $MCONF;
236 }
237 }
238 // Check if this is a submodule
239 if (strpos($name, '_') !== FALSE) {
240 list($mainModule, ) = explode('_', $name, 2);
241 }
242
243 $finalModuleConfiguration = array();
244
245 // merges $MCONF and $MLANG from conf.php and the additional configuration of the module
246 $setupInformation = $this->getModuleSetupInformation($name, $fullpath);
247
248 // Because 'path/../path' does not work
249 // clean up the configuration part
250 if (count($setupInformation['configuration']) > 0) {
251 if (!$setupInformation['configuration']['shy'] && $this->checkModAccess($name, $setupInformation['configuration']) && $this->checkModWorkspace($name, $setupInformation['configuration'])) {
252 $finalModuleConfiguration = $setupInformation['configuration'];
253 $finalModuleConfiguration['name'] = $name;
254 // Language processing. This will add module labels and image reference to the internal ->moduleLabels array of the LANG object.
255 if (is_object($GLOBALS['LANG'])) {
256 // $setupInformation['labels']['default']['tabs_images']['tab'] is for modules the reference
257 // to the module icon.
258 if (isset($setupInformation['configuration']['labels'])) {
259 $defaultLabels = $setupInformation['configuration']['labels'];
260 } else {
261 $defaultLabels = $setupInformation['labels']['default'];
262 }
263
264 // Here the path is transformed to an absolute reference.
265 if ($defaultLabels['tabs_images']['tab']) {
266 // Initializing search for alternative icon:
267 // Alternative icon key (might have an alternative set in $TBE_STYLES['skinImg']
268 $altIconKey = 'MOD:' . $name . '/' . $defaultLabels['tabs_images']['tab'];
269 $altIconAbsPath = is_array($GLOBALS['TBE_STYLES']['skinImg'][$altIconKey]) ? GeneralUtility::resolveBackPath(PATH_typo3 . $GLOBALS['TBE_STYLES']['skinImg'][$altIconKey][0]) : '';
270 // Setting icon, either default or alternative:
271 if ($altIconAbsPath && @is_file($altIconAbsPath)) {
272 $defaultLabels['tabs_images']['tab'] = $this->getRelativePath(PATH_typo3, $altIconAbsPath);
273 } else {
274 // Setting default icon:
275 $defaultLabels['tabs_images']['tab'] = $this->getRelativePath(PATH_typo3, $fullpath . '/' . $defaultLabels['tabs_images']['tab']);
276 }
277
278 // Finally, setting the icon with correct path:
279 if (substr($defaultLabels['tabs_images']['tab'], 0, 3) == '../') {
280 $defaultLabels['tabs_images']['tab'] = PATH_site . substr($defaultLabels['tabs_images']['tab'], 3);
281 } else {
282 $defaultLabels['tabs_images']['tab'] = PATH_typo3 . $defaultLabels['tabs_images']['tab'];
283 }
284 }
285
286 // If LOCAL_LANG references are used for labels of the module:
287 if ($defaultLabels['ll_ref']) {
288 // Now the 'default' key is loaded with the CURRENT language - not the english translation...
289 $defaultLabels['labels']['tablabel'] = $GLOBALS['LANG']->sL($defaultLabels['ll_ref'] . ':mlang_labels_tablabel');
290 $defaultLabels['labels']['tabdescr'] = $GLOBALS['LANG']->sL($defaultLabels['ll_ref'] . ':mlang_labels_tabdescr');
291 $defaultLabels['tabs']['tab'] = $GLOBALS['LANG']->sL($defaultLabels['ll_ref'] . ':mlang_tabs_tab');
292 $GLOBALS['LANG']->addModuleLabels($defaultLabels, $name . '_');
293 } else {
294 // ... otherwise use the old way:
295 $GLOBALS['LANG']->addModuleLabels($defaultLabels, $name . '_');
296 $GLOBALS['LANG']->addModuleLabels($setupInformation['labels'][$GLOBALS['LANG']->lang], $name . '_');
297 }
298 }
299
300 // Default script setup
301 if ($setupInformation['configuration']['script'] === '_DISPATCH') {
302 if ($setupInformation['configuration']['extbase']) {
303 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl('Tx_' . $name);
304 } else {
305 $finalModuleConfiguration['script'] = BackendUtility::getModuleUrl($name);
306 }
307 } elseif ($setupInformation['configuration']['script'] && file_exists($setupInformation['path'] . '/' . $setupInformation['configuration']['script'])) {
308 $finalModuleConfiguration['script'] = $this->getRelativePath(PATH_typo3, $fullpath . '/' . $setupInformation['configuration']['script']);
309 } else {
310 $finalModuleConfiguration['script'] = 'dummy.php';
311 }
312
313 // Default tab setting
314 if ($setupInformation['configuration']['defaultMod']) {
315 $finalModuleConfiguration['defaultMod'] = $setupInformation['configuration']['defaultMod'];
316 }
317
318 // Navigation Frame Script (GET params could be added)
319 if ($setupInformation['configuration']['navFrameScript']) {
320 $navFrameScript = explode('?', $setupInformation['configuration']['navFrameScript']);
321 $navFrameScript = $navFrameScript[0];
322 if (file_exists($setupInformation['path'] . '/' . $navFrameScript)) {
323 $finalModuleConfiguration['navFrameScript'] = $this->getRelativePath(PATH_typo3, $fullpath . '/' . $setupInformation['configuration']['navFrameScript']);
324 }
325 }
326
327 // additional params for Navigation Frame Script: "&anyParam=value&moreParam=1"
328 if ($setupInformation['configuration']['navFrameScriptParam']) {
329 $finalModuleConfiguration['navFrameScriptParam'] = $setupInformation['configuration']['navFrameScriptParam'];
330 }
331
332 // check if there is a navigation component (like the pagetree)
333 if (is_array($this->navigationComponents[$name])) {
334 $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$name]['componentId'];
335 // navigation component can be overriden by the main module component
336 } elseif ($mainModule && is_array($this->navigationComponents[$mainModule]) && $setupInformation['configuration']['inheritNavigationComponentFromMainModule'] !== FALSE) {
337 $finalModuleConfiguration['navigationComponentId'] = $this->navigationComponents[$mainModule]['componentId'];
338 }
339 } else {
340 return FALSE;
341 }
342 } else {
343 $finalModuleConfiguration = 'notFound';
344 }
345
346 return $finalModuleConfiguration;
347 }
348
349 /**
350 * fetches the conf.php file of a certain module, and also merges that with
351 * some additional configuration
352 *
353 * @param \string $moduleName the combined name of the module, can be "web", "web_info", or "tools_log"
354 * @param \string $pathToModuleDirectory the path where the module data is put, used for the conf.php or the modScript
355 * @return array an array with subarrays, named "configuration" (aka $MCONF), "labels" (previously known as $MLANG) and the stripped path
356 */
357 protected function getModuleSetupInformation($moduleName, $pathToModuleDirectory) {
358
359 // Because 'path/../path' does not work
360 $path = preg_replace('/\\/[^\\/.]+\\/\\.\\.\\//', '/', $pathToModuleDirectory);
361
362 $moduleSetupInformation = array(
363 'configuration' => array(),
364 'labels' => array(),
365 'path' => $path
366 );
367
368 if (@is_dir($path) && file_exists($path . '/conf.php')) {
369 $MCONF = array();
370 $MLANG = array();
371
372 // The conf-file is included. This must be valid PHP.
373 include $path . '/conf.php';
374
375 // move the global variables defined in conf.php into the local method
376 if (is_array($MCONF)) {
377 $moduleSetupInformation['configuration'] = $MCONF;
378 } else {
379 $moduleSetupInformation['configuration'] = array();
380 }
381 $moduleSetupInformation['labels'] = $MLANG;
382 }
383
384 // overlay them with additional setup information and configuration
385 if (is_array($GLOBALS['TBE_MODULES']['_configuration'][$moduleName])) {
386 $moduleSetupInformation['configuration'] = array_merge_recursive($moduleSetupInformation['configuration'], $GLOBALS['TBE_MODULES']['_configuration'][$moduleName]);
387 }
388
389 // add some default configuration
390 if (!isset($moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'])) {
391 $moduleSetupInformation['configuration']['inheritNavigationComponentFromMainModule'] = TRUE;
392 }
393
394 return $moduleSetupInformation;
395 }
396
397 /**
398 * Returns TRUE if the internal BE_USER has access to the module $name with $MCONF (based on security level set for that module)
399 *
400 * @param string $name Module name
401 * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about what access level the module has)
402 * @return bool TRUE if access is granted for $this->BE_USER
403 */
404 public function checkModAccess($name, $MCONF) {
405 if ($MCONF['access']) {
406 $access = strtolower($MCONF['access']);
407 // Checking if admin-access is required
408 // If admin-permissions is required then return TRUE if user is admin
409 if (strstr($access, 'admin')) {
410 if ($this->BE_USER->isAdmin()) {
411 return TRUE;
412 }
413 }
414 // This will add modules to the select-lists of user and groups
415 if (strstr($access, 'user')) {
416 $this->modListUser[] = $name;
417 }
418 if (strstr($access, 'group')) {
419 $this->modListGroup[] = $name;
420 }
421 // This checks if a user is permitted to access the module
422 if ($this->BE_USER->isAdmin() || $this->BE_USER->check('modules', $name)) {
423 return TRUE;
424 }
425 } else {
426 return TRUE;
427 }
428 }
429
430 /**
431 * Check if a module is allowed inside the current workspace for be user
432 * Processing happens only if $this->observeWorkspaces is TRUE
433 *
434 * @param string $name Module name
435 * @param array $MCONF MCONF array (module configuration array) from the modules conf.php file (contains settings about workspace restrictions)
436 * @return bool TRUE if access is granted for $this->BE_USER
437 */
438 public function checkModWorkspace($name, $MCONF) {
439 if ($this->observeWorkspaces) {
440 $status = TRUE;
441 if ($MCONF['workspaces']) {
442 $status = FALSE;
443 if ($this->BE_USER->workspace === 0 && GeneralUtility::inList($MCONF['workspaces'], 'online') || $this->BE_USER->workspace === -1 && GeneralUtility::inList($MCONF['workspaces'], 'offline') || $this->BE_USER->workspace > 0 && GeneralUtility::inList($MCONF['workspaces'], 'custom')) {
444 $status = TRUE;
445 }
446 } elseif ($this->BE_USER->workspace === -99) {
447 $status = FALSE;
448 }
449 return $status;
450 } else {
451 return TRUE;
452 }
453 }
454
455 /**
456 * Parses the moduleArray ($TBE_MODULES) into a internally useful structure.
457 * Returns an array where the keys are names of the module and the values may be TRUE (only module) or an array (of submodules)
458 *
459 * @param array $arr ModuleArray ($TBE_MODULES)
460 * @return array Output structure with available modules
461 */
462 public function parseModulesArray($arr) {
463 $theMods = array();
464 if (is_array($arr)) {
465 foreach ($arr as $mod => $subs) {
466 // Clean module name to alphanum
467 $mod = $this->cleanName($mod);
468 if ($mod) {
469 if ($subs) {
470 $subsArr = GeneralUtility::trimExplode(',', $subs);
471 foreach ($subsArr as $subMod) {
472 $subMod = $this->cleanName($subMod);
473 if ($subMod) {
474 $theMods[$mod][] = $subMod;
475 }
476 }
477 } else {
478 $theMods[$mod] = 1;
479 }
480 }
481 }
482 }
483 return $theMods;
484 }
485
486 /**
487 * The $str is cleaned so that it contains alphanumerical characters only. Modules must only consist of these characters
488 *
489 * @param string $str String to clean up
490 * @return string
491 */
492 public function cleanName($str) {
493 return preg_replace('/[^a-z0-9]/i', '', $str);
494 }
495
496 /**
497 * Get relative path for $destDir compared to $baseDir
498 *
499 * @param string $baseDir Base directory
500 * @param string $destDir Destination directory
501 * @return string The relative path of destination compared to base.
502 */
503 public function getRelativePath($baseDir, $destDir) {
504 // A special case , the dirs are equals
505 if ($baseDir == $destDir) {
506 return './';
507 }
508 // Remove beginning
509 $baseDir = ltrim($baseDir, '/');
510 $destDir = ltrim($destDir, '/');
511 $found = TRUE;
512 $slash_pos = 0;
513 do {
514 $slash_pos = strpos($destDir, '/');
515 if (substr($destDir, 0, $slash_pos) == substr($baseDir, 0, $slash_pos)) {
516 $baseDir = substr($baseDir, $slash_pos + 1);
517 $destDir = substr($destDir, $slash_pos + 1);
518 } else {
519 $found = FALSE;
520 }
521 } while ($found == TRUE);
522 $slashes = strlen($baseDir) - strlen(str_replace('/', '', $baseDir));
523 for ($i = 0; $i < $slashes; $i++) {
524 $destDir = '../' . $destDir;
525 }
526 return GeneralUtility::resolveBackPath($destDir);
527 }
528
529 }