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