[TASK] Use FQCN's when registering plugins/modules
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Utility / ExtensionUtility.php
1 <?php
2 namespace TYPO3\CMS\Extbase\Utility;
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 /**
18 * Utilities to manage plugins and modules of an extension. Also useful to auto-generate the autoloader registry
19 * file ext_autoload.php.
20 */
21 class ExtensionUtility
22 {
23 const PLUGIN_TYPE_PLUGIN = 'list_type';
24 const PLUGIN_TYPE_CONTENT_ELEMENT = 'CType';
25
26 /**
27 * Add auto-generated TypoScript to configure the Extbase Dispatcher.
28 *
29 * When adding a frontend plugin you will have to add both an entry to the TCA definition
30 * of tt_content table AND to the TypoScript template which must initiate the rendering.
31 * Including the plugin code after "defaultContentRendering" adds the necessary TypoScript
32 * for calling the appropriate controller and action of your plugin.
33 * FOR USE IN ext_localconf.php FILES
34 * Usage: 2
35 *
36 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
37 * @param string $pluginName must be a unique id for your plugin in UpperCamelCase (the string length of the extension key added to the length of the plugin name should be less than 32!)
38 * @param array $controllerActions is an array of allowed combinations of controller and action stored in an array (controller name as key and a comma separated list of action names as value, the first controller and its first action is chosen as default)
39 * @param array $nonCacheableControllerActions is an optional array of controller name and action names which should not be cached (array as defined in $controllerActions)
40 * @param string $pluginType either \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_PLUGIN (default) or \TYPO3\CMS\Extbase\Utility\ExtensionUtility::PLUGIN_TYPE_CONTENT_ELEMENT
41 * @throws \InvalidArgumentException
42 */
43 public static function configurePlugin($extensionName, $pluginName, array $controllerActions, array $nonCacheableControllerActions = [], $pluginType = self::PLUGIN_TYPE_PLUGIN)
44 {
45 self::checkPluginNameFormat($pluginName);
46 self::checkExtensionNameFormat($extensionName);
47
48 // Check if vendor name is prepended to extensionName in the format {vendorName}.{extensionName}
49 $vendorName = null;
50 $delimiterPosition = strrpos($extensionName, '.');
51 if ($delimiterPosition !== false) {
52 trigger_error(
53 'Calling method ' . __METHOD__ . 'with argument $extensionName containing the vendor name is deprecated and will stop working in TYPO3 11.0.',
54 E_USER_DEPRECATED
55 );
56 $vendorName = str_replace('.', '\\', substr($extensionName, 0, $delimiterPosition));
57 $extensionName = substr($extensionName, $delimiterPosition + 1);
58
59 if (!empty($vendorName)) {
60 self::checkVendorNameFormat($vendorName, $extensionName);
61 }
62 }
63 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
64
65 $pluginSignature = strtolower($extensionName . '_' . $pluginName);
66 if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName] ?? false)) {
67 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName] = [];
68 }
69 foreach ($controllerActions as $controllerClassName => $actionsList) {
70 if (class_exists($controllerClassName)) {
71 $controllerAlias = self::resolveControllerAliasFromControllerClassName($controllerClassName);
72 $vendorName = self::resolveVendorFromExtensionAndControllerClassName($extensionName, $controllerClassName);
73 if (!empty($vendorName)) {
74 self::checkVendorNameFormat($vendorName, $extensionName);
75 }
76 } else {
77 trigger_error(
78 'Calling ' . __METHOD__ . ' with controller aliases in argument $controllerActions is deprecated and will stop working in TYPO3 11.0.',
79 E_USER_DEPRECATED
80 );
81 $controllerAlias = $controllerClassName;
82 $controllerClassName = static::getControllerClassName((string)$vendorName, $extensionName, '', $controllerAlias);
83 }
84
85 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'][$controllerClassName] = [
86 'className' => $controllerClassName,
87 'alias' => $controllerAlias,
88 'actions' => \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $actionsList)
89 ];
90
91 if (isset($nonCacheableControllerActions[$controllerAlias]) && !empty($nonCacheableControllerActions[$controllerAlias])) {
92 trigger_error(
93 'Calling ' . __METHOD__ . ' with controller aliases in argument $nonCacheableControllerActions is deprecated and will stop working in TYPO3 11.0.',
94 E_USER_DEPRECATED
95 );
96 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'][$controllerClassName]['nonCacheableActions'] = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(
97 ',',
98 $nonCacheableControllerActions[$controllerAlias]
99 );
100 }
101
102 if (isset($nonCacheableControllerActions[$controllerClassName]) && !empty($nonCacheableControllerActions[$controllerClassName])) {
103 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['controllers'][$controllerClassName]['nonCacheableActions'] = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(
104 ',',
105 $nonCacheableControllerActions[$controllerClassName]
106 );
107 }
108 }
109
110 switch ($pluginType) {
111 case self::PLUGIN_TYPE_PLUGIN:
112 $pluginContent = trim('
113 tt_content.list.20.' . $pluginSignature . ' = USER
114 tt_content.list.20.' . $pluginSignature . ' {
115 userFunc = TYPO3\\CMS\\Extbase\\Core\\Bootstrap->run
116 extensionName = ' . $extensionName . '
117 pluginName = ' . $pluginName . (null !== $vendorName ? ("\n\t" . 'vendorName = ' . $vendorName) : '') . '
118 }');
119 break;
120 case self::PLUGIN_TYPE_CONTENT_ELEMENT:
121 $pluginContent = trim('
122 tt_content.' . $pluginSignature . ' =< lib.contentElement
123 tt_content.' . $pluginSignature . ' {
124 templateName = Generic
125 20 = USER
126 20 {
127 userFunc = TYPO3\\CMS\\Extbase\\Core\\Bootstrap->run
128 extensionName = ' . $extensionName . '
129 pluginName = ' . $pluginName . (null !== $vendorName ? ("\n\t\t" . 'vendorName = ' . $vendorName) : '') . '
130 }
131 }');
132 break;
133 default:
134 throw new \InvalidArgumentException('The pluginType "' . $pluginType . '" is not suported', 1289858856);
135 }
136 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['pluginType'] = $pluginType;
137 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTypoScript($extensionName, 'setup', '
138 # Setting ' . $extensionName . ' plugin TypoScript
139 ' . $pluginContent, 'defaultContentRendering');
140 }
141
142 /**
143 * Register an Extbase PlugIn into backend's list of plugins
144 * FOR USE IN Configuration/TCA/Overrides/tt_content.php
145 *
146 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
147 * @param string $pluginName must be a unique id for your plugin in UpperCamelCase (the string length of the extension key added to the length of the plugin name should be less than 32!)
148 * @param string $pluginTitle is a speaking title of the plugin that will be displayed in the drop down menu in the backend
149 * @param string $pluginIcon is an icon identifier or file path prepended with "EXT:", that will be displayed in the drop down menu in the backend (optional)
150 * @throws \InvalidArgumentException
151 */
152 public static function registerPlugin($extensionName, $pluginName, $pluginTitle, $pluginIcon = null)
153 {
154 self::checkPluginNameFormat($pluginName);
155 self::checkExtensionNameFormat($extensionName);
156
157 $delimiterPosition = strrpos($extensionName, '.');
158 if ($delimiterPosition !== false) {
159 $extensionName = substr($extensionName, $delimiterPosition + 1);
160 }
161 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
162 $pluginSignature = strtolower($extensionName) . '_' . strtolower($pluginName);
163
164 // At this point $extensionName is normalized, no matter which format the method was feeded with.
165 // Calculate the original extensionKey from this again.
166 $extensionKey = \TYPO3\CMS\Core\Utility\GeneralUtility::camelCaseToLowerCaseUnderscored($extensionName);
167
168 // pluginType is usually defined by configurePlugin() in the global array. Use this or fall back to default "list_type".
169 $pluginType = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['plugins'][$pluginName]['pluginType'] ?? 'list_type';
170
171 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPlugin(
172 [$pluginTitle, $pluginSignature, $pluginIcon],
173 $pluginType,
174 $extensionKey
175 );
176 }
177
178 /**
179 * Registers an Extbase module (main or sub) to the backend interface.
180 * FOR USE IN ext_tables.php FILES
181 *
182 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
183 * @param string $mainModuleName The main module key. So $main would be an index in the $TBE_MODULES array and $sub could be an element in the lists there. If $subModuleName is not set a blank $extensionName module is created
184 * @param string $subModuleName The submodule key.
185 * @param string $position This can be used to set the position of the $sub module within the list of existing submodules for the main module. $position has this syntax: [cmd]:[submodule-key]. cmd can be "after", "before" or "top" (or blank which is default). If "after"/"before" then submodule will be inserted after/before the existing submodule with [submodule-key] if found. If not found, the bottom of list. If "top" the module is inserted in the top of the submodule list.
186 * @param array $controllerActions is an array of allowed combinations of controller and action stored in an array (controller name as key and a comma separated list of action names as value, the first controller and its first action is chosen as default)
187 * @param array $moduleConfiguration The configuration options of the module (icon, locallang.xlf file)
188 * @throws \InvalidArgumentException
189 */
190 public static function registerModule($extensionName, $mainModuleName = '', $subModuleName = '', $position = '', array $controllerActions = [], array $moduleConfiguration = [])
191 {
192 self::checkExtensionNameFormat($extensionName);
193
194 // Check if vendor name is prepended to extensionName in the format {vendorName}.{extensionName}
195 $vendorName = null;
196 if (false !== $delimiterPosition = strrpos($extensionName, '.')) {
197 trigger_error(
198 'Calling method ' . __METHOD__ . 'with argument $extensionName containing the vendor name is deprecated and will stop working in TYPO3 11.0.',
199 E_USER_DEPRECATED
200 );
201 $vendorName = str_replace('.', '\\', substr($extensionName, 0, $delimiterPosition));
202 $extensionName = substr($extensionName, $delimiterPosition + 1);
203
204 if (!empty($vendorName)) {
205 self::checkVendorNameFormat($vendorName, $extensionName);
206 }
207 }
208
209 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
210 $defaultModuleConfiguration = [
211 'access' => 'admin',
212 'icon' => 'EXT:extbase/Resources/Public/Icons/Extension.png',
213 'labels' => ''
214 ];
215 if ($mainModuleName !== '' && !array_key_exists($mainModuleName, $GLOBALS['TBE_MODULES'])) {
216 $mainModuleName = $extensionName . \TYPO3\CMS\Core\Utility\GeneralUtility::underscoredToUpperCamelCase($mainModuleName);
217 } else {
218 $mainModuleName = $mainModuleName !== '' ? $mainModuleName : 'web';
219 }
220 // add mandatory parameter to use new pagetree
221 if ($mainModuleName === 'web') {
222 $defaultModuleConfiguration['navigationComponentId'] = 'TYPO3/CMS/Backend/PageTree/PageTreeElement';
223 }
224 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($defaultModuleConfiguration, $moduleConfiguration);
225 $moduleConfiguration = $defaultModuleConfiguration;
226 $moduleSignature = $mainModuleName;
227 if ($subModuleName !== '') {
228 $subModuleName = $extensionName . \TYPO3\CMS\Core\Utility\GeneralUtility::underscoredToUpperCamelCase($subModuleName);
229 $moduleSignature .= '_' . $subModuleName;
230 }
231 $moduleConfiguration['name'] = $moduleSignature;
232 $moduleConfiguration['extensionName'] = $extensionName;
233 $moduleConfiguration['routeTarget'] = \TYPO3\CMS\Extbase\Core\Bootstrap::class . '::handleBackendRequest';
234 if (!is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['modules'][$moduleSignature] ?? false)) {
235 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['modules'][$moduleSignature] = [];
236 }
237 foreach ($controllerActions as $controllerClassName => $actionsList) {
238 if (class_exists($controllerClassName)) {
239 $controllerAlias = self::resolveControllerAliasFromControllerClassName($controllerClassName);
240 $vendorName = self::resolveVendorFromExtensionAndControllerClassName($extensionName, $controllerClassName);
241 if (!empty($vendorName)) {
242 self::checkVendorNameFormat($vendorName, $extensionName);
243 }
244 } else {
245 trigger_error(
246 'Calling ' . __METHOD__ . ' with controller aliases is deprecated and will stop working in TYPO3 11.0.',
247 E_USER_DEPRECATED
248 );
249 $controllerAlias = $controllerClassName;
250 $controllerClassName = static::getControllerClassName($vendorName, $extensionName, '', $controllerAlias);
251 }
252
253 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['extensions'][$extensionName]['modules'][$moduleSignature]['controllers'][$controllerClassName] = [
254 'className' => $controllerClassName,
255 'alias' => $controllerAlias,
256 'actions' => \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $actionsList)
257 ];
258 }
259 if (null !== $vendorName) {
260 $moduleConfiguration['vendorName'] = $vendorName;
261 }
262 \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addModule($mainModuleName, $subModuleName, $position, null, $moduleConfiguration);
263 }
264
265 /**
266 * Returns the object name of the controller defined by the extension name and
267 * controller name
268 *
269 * @param string $vendor
270 * @param string $extensionKey
271 * @param string $subPackageKey
272 * @param string $controllerAlias
273 * @return string The controller's Object Name
274 * @throws \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchControllerException if the controller does not exist
275 */
276 public static function getControllerClassName(
277 string $vendor,
278 string $extensionKey,
279 string $subPackageKey,
280 string $controllerAlias
281 ): string {
282 $objectName = str_replace(
283 [
284 '@extension',
285 '@subpackage',
286 '@controller',
287 '@vendor',
288 '\\\\'
289 ],
290 [
291 $extensionKey,
292 $subPackageKey,
293 $controllerAlias,
294 $vendor,
295 '\\'
296 ],
297 '@vendor\@extension\@subpackage\Controller\@controllerController'
298 );
299
300 if ($objectName === false) {
301 throw new \TYPO3\CMS\Extbase\Mvc\Exception\NoSuchControllerException('The controller object "' . $objectName . '" does not exist.', 1220884009);
302 }
303 return trim($objectName, '\\');
304 }
305
306 /**
307 * @param string $controllerClassName
308 * @return string
309 */
310 public static function resolveControllerAliasFromControllerClassName(string $controllerClassName): string
311 {
312 if (strrpos($controllerClassName, 'Controller') === false) {
313 return '';
314 }
315
316 return trim(substr(
317 $controllerClassName,
318 (int)strrpos($controllerClassName, '\\'),
319 -strlen('Controller')
320 ), '\\');
321 }
322
323 /**
324 * @param string $extensionName
325 * @param string $controllerClassName
326 * @return string
327 */
328 public static function resolveVendorFromExtensionAndControllerClassName(string $extensionName, string $controllerClassName): string
329 {
330 if (strpos($controllerClassName, '\\') === false) {
331 // Does not work with non namespaced classes
332 return '';
333 }
334
335 if (false === $extensionNamePosition = strpos($controllerClassName, $extensionName)) {
336 // Does not work for classes that do not include the extension name as namespace part
337 return '';
338 }
339
340 if (--$extensionNamePosition < 0) {
341 return '';
342 }
343
344 return substr(
345 $controllerClassName,
346 0,
347 $extensionNamePosition
348 );
349 }
350
351 /**
352 * Register a type converter by class name.
353 *
354 * @param string $typeConverterClassName
355 */
356 public static function registerTypeConverter($typeConverterClassName)
357 {
358 if (!isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters']) ||
359 !is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'])
360 ) {
361 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'] = [];
362 }
363 if (!in_array($typeConverterClassName, $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'])) {
364 $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['extbase']['typeConverters'][] = $typeConverterClassName;
365 }
366 }
367
368 /**
369 * Check a given vendor name for CGL compliance.
370 * Log a deprecation message if it is not.
371 *
372 * @param string $vendorName The vendor name to check
373 * @param string $extensionName The extension name that is affected
374 */
375 protected static function checkVendorNameFormat($vendorName, $extensionName)
376 {
377 if (preg_match('/^[A-Z]/', $vendorName) !== 1) {
378 trigger_error('The vendor name from tx_' . $extensionName . ' must begin with a capital letter.', E_USER_DEPRECATED);
379 }
380 }
381
382 /**
383 * Check a given extension name for validity.
384 *
385 * @param string $extensionName The name of the extension
386 * @throws \InvalidArgumentException
387 */
388 protected static function checkExtensionNameFormat($extensionName)
389 {
390 if (empty($extensionName)) {
391 throw new \InvalidArgumentException('The extension name must not be empty', 1239891990);
392 }
393 }
394
395 /**
396 * Check a given plugin name for validity.
397 *
398 * @param string $pluginName The name of the plugin
399 * @throws \InvalidArgumentException
400 */
401 protected static function checkPluginNameFormat($pluginName)
402 {
403 if (empty($pluginName)) {
404 throw new \InvalidArgumentException('The plugin name must not be empty', 1239891988);
405 }
406 }
407 }