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