461cc2eb2121633a7e1d2a30d1ad31cfee46e02c
[Packages/TYPO3.CMS.git] / typo3 / sysext / extbase / Classes / Utility / Extension.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2009 Jochen Rau <jochen.rau@typoplanet.de>
6 * All rights reserved
7 *
8 * This script is part of the TYPO3 project. The TYPO3 project is
9 * free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * The GNU General Public License can be found at
15 * http://www.gnu.org/copyleft/gpl.html.
16 *
17 * This script is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * This copyright notice MUST APPEAR in all copies of the script!
23 ***************************************************************/
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 * @package Extbase
30 * @subpackage Utility
31 * @version $ID:$
32 */
33 class Tx_Extbase_Utility_Extension {
34
35 /**
36 * Add auto-generated TypoScript to configure the Extbase Dispatcher.
37 *
38 * When adding a frontend plugin you will have to add both an entry to the TCA definition
39 * of tt_content table AND to the TypoScript template which must initiate the rendering.
40 * Since the static template with uid 43 is the "content.default" and practically always
41 * used for rendering the content elements it's very useful to have this function automatically
42 * adding the necessary TypoScript for calling the appropriate controller and action of your plugin.
43 * It will also work for the extension "css_styled_content"
44 * FOR USE IN ext_localconf.php FILES
45 * Usage: 2
46 *
47 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
48 * @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!)
49 * @param string $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)
50 * @param string $nonCacheableControllerActions is an optional array of controller name and action names which should not be cached (array as defined in $controllerActions)
51 * @param string $defaultControllerAction is an optional array controller name (as array key) and action name (as array value) that should be called as default
52 * @return void
53 */
54 public static function configurePlugin($extensionName, $pluginName, array $controllerActions, array $nonCacheableControllerActions = array()) {
55 if (empty($pluginName)) {
56 throw new InvalidArgumentException('The plugin name must not be empty', 1239891987);
57 }
58 if (empty($extensionName)) {
59 throw new InvalidArgumentException('The extension name was invalid (must not be empty and must match /[A-Za-z][_A-Za-z0-9]/)', 1239891989);
60 }
61 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
62 $pluginSignature = strtolower($extensionName) . '_' . strtolower($pluginName);
63
64 $controllers = '';
65 foreach ($controllerActions as $controller => $actionsList) {
66 $controllers .= '
67 ' . $controller . '.actions = ' . $actionsList;
68 if (!empty($nonCacheableControllerActions[$controller])) {
69 $controllers .= '
70 ' . $controller . '.nonCacheableActions = ' . $nonCacheableControllerActions[$controller];
71 }
72 }
73
74 $switchableControllerActions = '
75 switchableControllerActions {' . $controllers . '
76 }';
77
78 reset($controllerActions);
79 $defaultController = key($controllerActions);
80 $controller = '
81 controller = ' . $defaultController;
82 $defaultAction = array_shift(t3lib_div::trimExplode(',', current($controllerActions)));
83 $action = '
84 action = ' . $defaultAction;
85
86 $pluginTemplate = 'plugin.tx_' . strtolower($extensionName) . ' {
87 settings {
88 }
89 persistence {
90 storagePid =
91 classes {
92 }
93 }
94 view {
95 templateRootPath =
96 layoutRootPath =
97 partialRootPath =
98 }
99 }';
100 t3lib_extMgm::addTypoScript($extensionName, 'setup', '
101 # Setting ' . $extensionName . ' plugin TypoScript
102 ' . $pluginTemplate);
103
104 $pluginContent = trim('
105 tt_content.list.20.' . $pluginSignature . ' = USER
106 tt_content.list.20.' . $pluginSignature . ' {
107 userFunc = tx_extbase_dispatcher->dispatch
108 pluginName = ' . $pluginName . '
109 extensionName = ' . $extensionName . '
110 ' . $controller .
111 $action .
112 $switchableControllerActions . '
113
114 settings =< plugin.tx_' . strtolower($extensionName) . '.settings
115 persistence =< plugin.tx_' . strtolower($extensionName) . '.persistence
116 view =< plugin.tx_' . strtolower($extensionName) . '.view
117 _LOCAL_LANG =< plugin.tx_' . strtolower($extensionName) . '._LOCAL_LANG
118 }');
119
120 t3lib_extMgm::addTypoScript($extensionName, 'setup', '
121 # Setting ' . $extensionName . ' plugin TypoScript
122 ' . $pluginContent, 43);
123 }
124
125 /**
126 * Register an Extbase PlugIn into backend's list of plugins
127 * FOR USE IN ext_tables.php FILES
128 *
129 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
130 * @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!)
131 * @param string $pluginTitle is a speaking title of the plugin that will be displayed in the drop down menu in the backend
132 * @return void
133 */
134 public static function registerPlugin($extensionName, $pluginName, $pluginTitle) {
135 if (empty($pluginName)) {
136 throw new InvalidArgumentException('The plugin name must not be empty', 1239891987);
137 }
138 if (empty($extensionName)) {
139 throw new InvalidArgumentException('The extension name was invalid (must not be empty and must match /[A-Za-z][_A-Za-z0-9]/)', 1239891989);
140 }
141 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
142 $pluginSignature = strtolower($extensionName) . '_' . strtolower($pluginName);
143
144 t3lib_extMgm::addPlugin(array($pluginTitle, $pluginSignature), 'list_type');
145 }
146
147 /**
148 * This method is called from t3lib_loadModules::checkMod and it replaces old conf.php.
149 *
150 * @param string $key The module name
151 * @param string $fullpath Absolute path to module
152 * @param array $MCONF Reference to the array holding the configuration of the module
153 * @param array $MLANG Reference to the array holding the localized module labels
154 * @return array Configuration of the module
155 */
156 public function configureModule($key, $fullpath, array $MCONF = array(), array $MLANG = array()) {
157 $path = preg_replace('/\/[^\/.]+\/\.\.\//', '/', $fullpath); // because 'path/../path' does not work
158 $config = $GLOBALS['TBE_MODULES']['_configuration'][$key]['config'];
159 define('TYPO3_MOD_PATH', $config['extRelPath']);
160
161 // Fill $MCONF
162 $MCONF['name'] = $key;
163 $MCONF['access'] = $config['access'];
164 $MCONF['script'] = '_DISPATCH';
165
166 if (substr($config['icon'], 0, 4) === 'EXT:') {
167 list($extKey, $local) = explode('/', substr($config['icon'], 4), 2);
168 $config['icon'] = t3lib_extMgm::extRelPath($extKey) . $local;
169 }
170
171 // Initialize search for alternative icon:
172 $altIconKey = 'MOD:' . $key . '/' . $config['icon']; // Alternative icon key (might have an alternative set in $TBE_STYLES['skinImg']
173 $altIconAbsPath = is_array($GLOBALS['TBE_STYLES']['skinImg'][$altIconKey]) ? t3lib_div::resolveBackPath(PATH_typo3.$GLOBALS['TBE_STYLES']['skinImg'][$altIconKey][0]) : '';
174
175 // Set icon, either default or alternative:
176 if ($altIconAbsPath && @is_file($altIconAbsPath)) {
177 $tabImage = $altIconAbsPath;
178 } else {
179 // Setting default icon:
180 $tabImage = $config['icon'];
181 }
182
183 // Fill $MLANG
184 $MLANG['default']['ll_ref'] = $config['labels'];
185
186 // Finally, set the icon with correct path:
187 if (substr($tabImage, 0 ,3) === '../') {
188 $MLANG['default']['tabs_images']['tab'] = PATH_site . substr($tabImage, 3);
189 } else {
190 $MLANG['default']['tabs_images']['tab'] = PATH_typo3 . $tabImage;
191 }
192
193 // If LOCAL_LANG references are used for labels of the module:
194 if ($MLANG['default']['ll_ref']) {
195 // Now the 'default' key is loaded with the CURRENT language - not the english translation...
196 $MLANG['default']['labels']['tablabel'] = $GLOBALS['LANG']->sL($MLANG['default']['ll_ref'] . ':mlang_labels_tablabel');
197 $MLANG['default']['labels']['tabdescr'] = $GLOBALS['LANG']->sL($MLANG['default']['ll_ref'] . ':mlang_labels_tabdescr');
198 $MLANG['default']['tabs']['tab'] = $GLOBALS['LANG']->sL($MLANG['default']['ll_ref'] . ':mlang_tabs_tab');
199 $GLOBALS['LANG']->addModuleLabels($MLANG['default'], $key . '_');
200 } else { // ... otherwise use the old way:
201 $GLOBALS['LANG']->addModuleLabels($MLANG['default'], $key . '_');
202 $GLOBALS['LANG']->addModuleLabels($MLANG[$GLOBALS['LANG']->lang], $key . '_');
203 }
204
205 // Fill $modconf
206 $modconf['script'] = 'mod.php?M=' . rawurlencode($key);
207 $modconf['name'] = $key;
208
209 // Default tab setting
210 if ($MCONF['defaultMod']) {
211 $modconf['defaultMod'] = $MCONF['defaultMod'];
212 }
213 // Navigation Frame Script (GET params could be added)
214 if ($MCONF['navFrameScript']) {
215 $navFrameScript = explode('?', $MCONF['navFrameScript']);
216 $navFrameScript = $navFrameScript[0];
217 if (file_exists($path . '/' . $navFrameScript)) {
218 $modconf['navFrameScript'] = $this->getRelativePath(PATH_typo3, $fullpath . '/' . $MCONF['navFrameScript']);
219 }
220 }
221
222 // Additional params for Navigation Frame Script: "&anyParam=value&moreParam=1"
223 if ($MCONF['navFrameScriptParam']) {
224 $modconf['navFrameScriptParam'] = $MCONF['navFrameScriptParam'];
225 }
226
227 return $modconf;
228 }
229
230 /**
231 * Registers an Extbase module (main or sub) to the backend interface.
232 * FOR USE IN ext_tables.php FILES
233 *
234 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
235 * @param string $main The main module key, $sub is the submodule key. So $main would be an index in the $TBE_MODULES array and $sub could be an element in the lists there. If $main is not set a blank $extensionName module is created
236 * @param string $sub The submodule key. If $sub is not set a blank $main module is created
237 * @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.
238 * @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)
239 * @param array $config The configuration options of the module (icon, locallang.xml file)
240 * @return void
241 */
242 public static function registerModule($extensionName, $main = '', $sub = '', $position = '', array $controllerActions, $config = array()) {
243 if (empty($extensionName)) {
244 throw new InvalidArgumentException('The extension name was invalid (must not be empty and must match /[A-Za-z][_A-Za-z0-9]/)', 1239891989);
245 }
246 $extensionKey = $extensionName; // FIXME This will break if the $extensionName is given as BlogExample
247 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
248
249 $path = t3lib_extMgm::extPath($extensionKey, 'Classes/');
250 $relPath = t3lib_extMgm::extRelPath($extensionKey) . 'Classes/';
251
252 if (!is_array($config) || count($config) == 0) {
253 $config['access'] = 'admin';
254 $config['icon'] = '';
255 $config['labels'] = '';
256 $config['extRelPath'] = $relPath;
257 }
258
259 if ((strlen($main) > 0) && !array_key_exists($main, $GLOBALS['TBE_MODULES'])) {
260 $main = $extensionName . self::convertLowerUnderscoreToUpperCamelCase($main);
261 } else {
262 $main = (strlen($main) > 0) ? $main : 'web'; // TODO By now, $main must default to 'web'
263 }
264
265 if ((strlen($sub) > 0)) {
266 $sub = $extensionName . self::convertLowerUnderscoreToUpperCamelCase($sub);
267 $key = $main . '_' . $sub;
268 } else {
269 $key = $main;
270 }
271
272 $moduleConfig = array(
273 'name' => $key,
274 'extensionKey' => $extensionKey,
275 'extensionName' => $extensionName,
276 'controllerActions' => $controllerActions,
277 'config' => $config,
278 );
279 $GLOBALS['TBE_MODULES']['_configuration'][$key] = $moduleConfig;
280 $GLOBALS['TBE_MODULES']['_configuration'][$key]['configureModuleFunction'] = array('Tx_Extbase_Utility_Extension', 'configureModule');
281
282 t3lib_extMgm::addModule($main, $sub, $position);
283 }
284
285 // TODO PHPdoc
286 public static function convertCamelCaseToLowerCaseUnderscored($string) {
287 // FIXME The cache doesn't work IMO as it is static (did I really implemented this? ;-))
288 static $conversionMap = array();
289 if (!isset($conversionMap[$string])) {
290 $conversionMap[$string] = strtolower(preg_replace('/(?<=\w)([A-Z])/', '_\\1', $string));
291 }
292 return $conversionMap[$string];
293 }
294
295 public static function convertUnderscoredToLowerCamelCase($string) {
296 $string = str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string))));
297 $string[0] = strtolower($string[0]);
298 return $string;
299 }
300
301 public static function convertLowerUnderscoreToUpperCamelCase($camelCasedString) {
302 return t3lib_div::underscoredToUpperCamelCase($camelCasedString);
303 }
304
305 /**
306 * Build the autoload registry for a given extension and place it ext_autoload.php.
307 *
308 * @param string $extensionKey Key of the extension
309 * @param string $extensionPath full path of the extension
310 * @return string HTML string which should be outputted
311 */
312 public function createAutoloadRegistryForExtension($extensionKey, $extensionPath) {
313 $classNameToFileMapping = array();
314 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionKey)));
315 $errors = $this->buildAutoloadRegistryForSinglePath($classNameToFileMapping, $extensionPath . 'Classes/', '.*tslib.*', '$extensionClassesPath . \'|\'');
316 if ($errors) {
317 return $errors;
318 }
319 $globalPrefix = '$extensionClassesPath = t3lib_extMgm::extPath(\'' . $extensionKey . '\') . \'Classes/\';';
320
321 $errors = array();
322 foreach ($classNameToFileMapping as $className => $fileName) {
323 if (!(strpos($className, 'tx_' . strtolower($extensionName)) === 0)) {
324 $errors[] = $className . ' does not start with Tx_' . $extensionName . ' and was not added to the autoloader registry.';
325 unset($classNameToFileMapping[$className]);
326 }
327 }
328 $autoloadFileString = $this->generateAutoloadPHPFileData($classNameToFileMapping, $globalPrefix);
329 if (!@file_put_contents($extensionPath . 'ext_autoload.php', $autoloadFileString)) {
330 $errors[] = '<b>' . $extensionPath . 'ext_autoload.php could not be written!</b>';
331 }
332 $errors[] = 'Wrote the following data: <pre>' . htmlspecialchars($autoloadFileString) . '</pre>';
333 return implode('<br />', $errors);
334 }
335
336 /**
337 * Generate autoload PHP file data. Takes an associative array with class name to file mapping, and outputs it as PHP.
338 * Does NOT escape the values in the associative array. Includes the <?php ... ?> syntax and an optional global prefix.
339 *
340 * @param array $classNameToFileMapping class name to file mapping
341 * @param string $globalPrefix Global prefix which is prepended to all code.
342 * @return string The full PHP string
343 */
344 protected function generateAutoloadPHPFileData($classNameToFileMapping, $globalPrefix = '') {
345 $output = '<?php' . PHP_EOL;
346 $output .= '// DO NOT CHANGE THIS FILE! It is automatically generated by Tx_Extbase_Utility_Extension::createAutoloadRegistryForExtension.' . PHP_EOL;
347 $output .= '// This file was generated on ' . date('Y-m-d H:i') . PHP_EOL;
348 $output .= PHP_EOL;
349 $output .= $globalPrefix . PHP_EOL;
350 $output .= 'return array(' . PHP_EOL;
351 foreach ($classNameToFileMapping as $className => $quotedFileName) {
352 $output .= ' \'' . $className . '\' => ' . $quotedFileName . ',' . PHP_EOL;
353 }
354 $output .= ');' . PHP_EOL;
355 $output .= '?>';
356 return $output;
357 }
358
359 /**
360 * Generate the $classNameToFileMapping for a given filePath.
361 *
362 * @param array $classNameToFileMapping (Reference to array) All values are appended to this array.
363 * @param string $path Path which should be crawled
364 * @param string $excludeRegularExpression Exclude regular expression, to exclude certain files from being processed
365 * @param string $valueWrap Wrap for the file name
366 * @return void
367 */
368 protected function buildAutoloadRegistryForSinglePath(&$classNameToFileMapping, $path, $excludeRegularExpression = '', $valueWrap = '\'|\'') {
369 // if (file_exists($path . 'Classes/')) {
370 // return "<b>This appears to be a new-style extension which has its PHP classes inside the Classes/ subdirectory. It is not needed to generate the autoload registry for these extensions.</b>";
371 // }
372 $extensionFileNames = t3lib_div::removePrefixPathFromList(t3lib_div::getAllFilesAndFoldersInPath(array(), $path, 'php', FALSE, 99, $excludeRegularExpression), $path);
373
374 foreach ($extensionFileNames as $extensionFileName) {
375 $classNamesInFile = $this->extractClassNames($path . $extensionFileName);
376 if (!count($classNamesInFile)) continue;
377 foreach ($classNamesInFile as $className) {
378 $classNameToFileMapping[strtolower($className)] = str_replace('|', $extensionFileName, $valueWrap);
379 }
380 }
381 }
382
383 /**
384 * Extracts class names from the given file.
385 *
386 * @param string $filePath File path (absolute)
387 * @return array Class names
388 */
389 protected function extractClassNames($filePath) {
390 $fileContent = php_strip_whitespace($filePath);
391 $classNames = array();
392 if (function_exists('token_get_all')) {
393 $tokens = token_get_all($fileContent);
394 while(1) {
395 // look for "class" or "interface"
396 $token = $this->findToken($tokens, array(T_ABSTRACT, T_CLASS, T_INTERFACE));
397 // fetch "class" token if "abstract" was found
398 if ($token === 'abstract') {
399 $token = $this->findToken($tokens, array(T_CLASS));
400 }
401 if ($token === false) {
402 // end of file
403 break;
404 }
405 // look for the name (a string) skipping only whitespace and comments
406 $token = $this->findToken($tokens, array(T_STRING), array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT));
407 if ($token === false) {
408 // unexpected end of file or token: remove found names because of parse error
409 t3lib_div::sysLog('Parse error in "' . $filePath. '".', 'Core', 2);
410 $classNames = array();
411 break;
412 }
413 $token = t3lib_div::strtolower($token);
414 // exclude XLASS classes
415 if (strncmp($token, 'ux_', 3)) {
416 $classNames[] = $token;
417 }
418 }
419 } else {
420 // TODO: parse PHP - skip coments and strings, apply regexp only on the remaining PHP code
421 $matches = array();
422 preg_match_all('/^[ \t]*(?:(?:abstract|final)?[ \t]*(?:class|interface))[ \t\n\r]+([a-zA-Z][a-zA-Z_0-9]*)/mS', $fileContent, $matches);
423 $classNames = array_map('t3lib_div::strtolower', $matches[1]);
424 }
425 return $classNames;
426 }
427
428 /**
429 * Find tokens in the tokenList
430 *
431 * @param array $tokenList list of tokens as returned by token_get_all()
432 * @param array $wantedToken the tokens to be found
433 * @param array $intermediateTokens optional: list of tokens that are allowed to skip when looking for the wanted token
434 * @return mixed
435 */
436 protected function findToken(array &$tokenList, array $wantedTokens, array $intermediateTokens = array()) {
437 $skipAllTokens = count($intermediateTokens) ? false : true;
438
439 $returnValue = false;
440 // Iterate with while since we need the current array position:
441 foreach ($tokenList as $token) {
442 // parse token (see http://www.php.net/manual/en/function.token-get-all.php for format of token list)
443 if (is_array($token)) {
444 list($id, $text) = $token;
445 } else {
446 $id = $text = $token;
447 }
448 if (in_array($id, $wantedTokens)) {
449 $returnValue = $text;
450 break;
451 }
452 // look for another token
453 if ($skipAllTokens || in_array($id, $intermediateTokens)) {
454 continue;
455 }
456 break;
457 }
458 return $returnValue;
459 }
460
461 /**
462 * Determines the plugin namespace of the specified plugin (defaults to "tx_[extensionName]_[pluginName]")
463 * If plugin.tx_$pluginSignature.view.pluginNamespace is set, this value is returned
464 * If pluginNamespace is not specified "tx_[extensionName]_[pluginName]" is returned.
465 *
466 * @param string $pluginSignature Plugin signature: strtolower($extensionName) . '_' . strtolower($pluginName)
467 * @return string plugin namespace
468 */
469 public static function getPluginNamespaceByPluginSignature($pluginSignature) {
470 $defaultPluginNamespace = 'tx_' . $pluginSignature;
471 $configurationManager = Tx_Extbase_Dispatcher::getConfigurationManager();
472 if (!isset($configurationManager) || !isset($GLOBALS['TSFE']->tmpl->setup['tt_content.']['list.']['20.']) || !is_array($GLOBALS['TSFE']->tmpl->setup['tt_content.']['list.']['20.'])) {
473 return $defaultPluginNamespace;
474 }
475 $pluginConfiguration = $GLOBALS['TSFE']->tmpl->setup['tt_content.']['list.']['20.'][$pluginSignature . '.'];
476 $frameworkConfiguration = $configurationManager->getFrameworkConfiguration($pluginConfiguration);
477 if (!isset($frameworkConfiguration['view']['pluginNamespace']) || empty($frameworkConfiguration['view']['pluginNamespace'])) {
478 return $defaultPluginNamespace;
479 }
480 return $frameworkConfiguration['view']['pluginNamespace'];
481 }
482
483 }
484 ?>