Passed
Push — master ( 0e8f42...230bae )
by
unknown
16:04
created

resolveVendorFromExtensionAndControllerClassName()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

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