Add Extbase 1.0.1 to TYPO3core. Do NOT make changes inside! See misc/core_svn_rules...
[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 $nonCachableControllerActions 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 $nonCachableControllerActions = 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 $controllerCounter = 1;
65 $hasMultipleActionsCounter = 0;
66 $controllers = '';
67 foreach ($controllerActions as $controller => $actionsList) {
68 $controllers .= '
69 ' . $controllerCounter . '.controller = ' . $controller . '
70 ' . $controllerCounter . '.actions = ' . $actionsList;
71 $controllerCounter++;
72 if (strpos($actionsList, ',') !== FALSE) {
73 $hasMultipleActionsCounter++;
74 }
75 }
76
77 $switchableControllerActions = '';
78 if ($controllerCounter > 1 || $hasMultipleActionsCounter > 0) {
79 $switchableControllerActions = '
80 switchableControllerActions {' . $controllers . '
81 }';
82 }
83
84 reset($controllerActions);
85 $defaultController = key($controllerActions);
86 $controller = '
87 controller = ' . $defaultController;
88 $defaultAction = array_shift(t3lib_div::trimExplode(',', current($controllerActions)));
89 $action = '
90 action = ' . $defaultAction;
91
92 $nonCachableActions = array();
93 if (!empty($nonCachableControllerActions[$defaultController])) {
94 $nonCachableActions = t3lib_div::trimExplode(',', $nonCachableControllerActions[$defaultController]);
95 }
96 $cachableActions = array_diff(t3lib_div::trimExplode(',', $controllerActions[$defaultController]), $nonCachableActions);
97
98 $contentObjectType = in_array($defaultAction, $nonCachableActions) ? 'USER_INT' : 'USER';
99
100 $conditions = '';
101 foreach ($controllerActions as $controllerName => $actionsList) {
102 if (!empty($nonCachableControllerActions[$controllerName])) {
103 $nonCachableActions = t3lib_div::trimExplode(',', $nonCachableControllerActions[$controllerName]);
104 $cachableActions = array_diff(t3lib_div::trimExplode(',', $controllerActions[$controllerName]), $nonCachableActions);
105 if (($contentObjectType == 'USER' && count($nonCachableActions) > 0)
106 || ($contentObjectType == 'USER_INT' && count($cachableActions) > 0)) {
107
108 $conditions .= '
109 [globalString = GP:tx_' . $pluginSignature . '|controller = ' . $controllerName . '] && [globalString = GP:tx_' . $pluginSignature . '|action = /' . implode('|', $contentObjectType === 'USER' ? $nonCachableActions : $cachableActions) . '/]
110 tt_content.list.20.' . $pluginSignature . ' = ' . ($contentObjectType === 'USER' ? 'USER_INT' : 'USER') . '
111 [global]
112 ';
113 }
114 }
115 }
116
117 $pluginTemplate = 'plugin.tx_' . strtolower($extensionName) . ' {
118 settings {
119 }
120 persistence {
121 storagePid =
122 classes {
123 }
124 }
125 view {
126 templateRootPath =
127 layoutRootPath =
128 partialRootPath =
129 }
130 }';
131 t3lib_extMgm::addTypoScript($extensionName, 'setup', '
132 # Setting ' . $extensionName . ' plugin TypoScript
133 ' . $pluginTemplate);
134
135 $pluginContent = trim('
136 tt_content.list.20.' . $pluginSignature . ' = ' . $contentObjectType . '
137 tt_content.list.20.' . $pluginSignature . ' {
138 userFunc = tx_extbase_dispatcher->dispatch
139 pluginName = ' . $pluginName . '
140 extensionName = ' . $extensionName . '
141 ' . $controller .
142 $action .
143 $switchableControllerActions . '
144
145 settings =< plugin.tx_' . strtolower($extensionName) . '.settings
146 persistence =< plugin.tx_' . strtolower($extensionName) . '.persistence
147 view =< plugin.tx_' . strtolower($extensionName) . '.view
148 _LOCAL_LANG =< plugin.tx_' . strtolower($extensionName) . '._LOCAL_LANG
149 }
150 ' . $conditions);
151
152 t3lib_extMgm::addTypoScript($extensionName, 'setup', '
153 # Setting ' . $extensionName . ' plugin TypoScript
154 ' . $pluginContent, 43);
155 }
156
157 /**
158 * Register an Extbase PlugIn into backend's list of plugins
159 * FOR USE IN ext_tables.php FILES
160 *
161 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
162 * @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!)
163 * @param string $pluginTitle is a speaking title of the plugin that will be displayed in the drop down menu in the backend
164 * @return void
165 */
166 public static function registerPlugin($extensionName, $pluginName, $pluginTitle) {
167 if (empty($pluginName)) {
168 throw new InvalidArgumentException('The plugin name must not be empty', 1239891987);
169 }
170 if (empty($extensionName)) {
171 throw new InvalidArgumentException('The extension name was invalid (must not be empty and must match /[A-Za-z][_A-Za-z0-9]/)', 1239891989);
172 }
173 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
174 $pluginSignature = strtolower($extensionName) . '_' . strtolower($pluginName);
175
176 t3lib_extMgm::addPlugin(array($pluginTitle, $pluginSignature), 'list_type');
177 }
178
179 /**
180 * This method is called from t3lib_loadModules::checkMod and it replaces old conf.php.
181 *
182 * @param string $key The module name
183 * @param string $fullpath Absolute path to module
184 * @param array $MCONF Reference to the array holding the configuration of the module
185 * @param array $MLANG Reference to the array holding the localized module labels
186 * @return array Configuration of the module
187 */
188 public function configureModule($key, $fullpath, array $MCONF = array(), array $MLANG = array()) {
189 $path = preg_replace('/\/[^\/.]+\/\.\.\//', '/', $fullpath); // because 'path/../path' does not work
190 $config = $GLOBALS['TBE_MODULES'][$key]['config'];
191 define('TYPO3_MOD_PATH', $config['extRelPath']);
192
193 $GLOBALS['BACK_PATH'] = '';
194
195 // Fill $MCONF
196 $MCONF['name'] = $key;
197 $MCONF['access'] = $config['access'];
198 $MCONF['script'] = '_DISPATCH';
199
200 if (substr($config['icon'], 0, 4) === 'EXT:') {
201 list($extKey, $local) = explode('/', substr($config['icon'], 4), 2);
202 $config['icon'] = t3lib_extMgm::extRelPath($extKey) . $local;
203 }
204
205 // Initialize search for alternative icon:
206 $altIconKey = 'MOD:' . $key . '/' . $config['icon']; // Alternative icon key (might have an alternative set in $TBE_STYLES['skinImg']
207 $altIconAbsPath = is_array($GLOBALS['TBE_STYLES']['skinImg'][$altIconKey]) ? t3lib_div::resolveBackPath(PATH_typo3.$GLOBALS['TBE_STYLES']['skinImg'][$altIconKey][0]) : '';
208
209 // Set icon, either default or alternative:
210 if ($altIconAbsPath && @is_file($altIconAbsPath)) {
211 $tabImage = $altIconAbsPath;
212 } else {
213 // Setting default icon:
214 $tabImage = $config['icon'];
215 }
216
217 // Fill $MLANG
218 $MLANG['default']['ll_ref'] = $config['labels'];
219
220 // Finally, set the icon with correct path:
221 if (substr($tabImage, 0 ,3) === '../') {
222 $MLANG['default']['tabs_images']['tab'] = PATH_site . substr($tabImage, 3);
223 } else {
224 $MLANG['default']['tabs_images']['tab'] = PATH_typo3 . $tabImage;
225 }
226
227 // If LOCAL_LANG references are used for labels of the module:
228 if ($MLANG['default']['ll_ref']) {
229 // Now the 'default' key is loaded with the CURRENT language - not the english translation...
230 $MLANG['default']['labels']['tablabel'] = $GLOBALS['LANG']->sL($MLANG['default']['ll_ref'] . ':mlang_labels_tablabel');
231 $MLANG['default']['labels']['tabdescr'] = $GLOBALS['LANG']->sL($MLANG['default']['ll_ref'] . ':mlang_labels_tabdescr');
232 $MLANG['default']['tabs']['tab'] = $GLOBALS['LANG']->sL($MLANG['default']['ll_ref'] . ':mlang_tabs_tab');
233 $GLOBALS['LANG']->addModuleLabels($MLANG['default'], $key . '_');
234 } else { // ... otherwise use the old way:
235 $GLOBALS['LANG']->addModuleLabels($MLANG['default'], $key . '_');
236 $GLOBALS['LANG']->addModuleLabels($MLANG[$GLOBALS['LANG']->lang], $key . '_');
237 }
238
239 // Fill $modconf
240 $modconf['script'] = 'mod.php?M=' . rawurlencode($key);
241 $modconf['name'] = $key;
242
243 // Default tab setting
244 if ($MCONF['defaultMod']) {
245 $modconf['defaultMod'] = $MCONF['defaultMod'];
246 }
247 // Navigation Frame Script (GET params could be added)
248 if ($MCONF['navFrameScript']) {
249 $navFrameScript = explode('?', $MCONF['navFrameScript']);
250 $navFrameScript = $navFrameScript[0];
251 if (file_exists($path . '/' . $navFrameScript)) {
252 $modconf['navFrameScript'] = $this->getRelativePath(PATH_typo3, $fullpath . '/' . $MCONF['navFrameScript']);
253 }
254 }
255
256 // Additional params for Navigation Frame Script: "&anyParam=value&moreParam=1"
257 if ($MCONF['navFrameScriptParam']) {
258 $modconf['navFrameScriptParam'] = $MCONF['navFrameScriptParam'];
259 }
260
261 return $modconf;
262 }
263
264 /**
265 * Registers an Extbase module (main or sub) to the backend interface.
266 * FOR USE IN ext_tables.php FILES
267 *
268 * @param string $extensionName The extension name (in UpperCamelCase) or the extension key (in lower_underscore)
269 * @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
270 * @param string $sub The submodule key. If $sub is not set a blank $main module is created
271 * @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.
272 * @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)
273 * @param array $config The configuration options of the module (icon, locallang.xml file)
274 * @return void
275 */
276 public static function registerModule($extensionName, $main = '', $sub = '', $position = '', array $controllerActions, $config = array()) {
277 if (empty($extensionName)) {
278 throw new InvalidArgumentException('The extension name was invalid (must not be empty and must match /[A-Za-z][_A-Za-z0-9]/)', 1239891989);
279 }
280 $extensionKey = $extensionName; // FIXME This will break if the $extensionName is given as BlogExample
281 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionName)));
282
283 $path = t3lib_extMgm::extPath($extensionKey, 'Classes/');
284 $relPath = t3lib_extMgm::extRelPath($extensionKey) . 'Classes/';
285
286 if (!is_array($config) || count($config) == 0) {
287 $config['access'] = 'admin';
288 $config['icon'] = '';
289 $config['labels'] = '';
290 $config['extRelPath'] = $relPath;
291 }
292
293 if ((strlen($main) > 0) && !array_key_exists($main, $GLOBALS['TBE_MODULES'])) {
294 $main = $extensionName . self::convertLowerUnderscoreToUpperCamelCase($main);
295 } else {
296 $main = (strlen($main) > 0) ? $main : 'web'; // TODO By now, $main must default to 'web'
297 }
298
299 if ((strlen($sub) > 0)) {
300 $sub = $extensionName . self::convertLowerUnderscoreToUpperCamelCase($sub);
301 $key = $main . '_' . $sub;
302 } else {
303 $key = $main;
304 }
305
306 $moduleConfig = array(
307 'name' => $key,
308 'extensionKey' => $extensionKey,
309 'extensionName' => $extensionName,
310 'controllerActions' => $controllerActions,
311 'config' => $config,
312 );
313 $GLOBALS['TBE_MODULES'][$key] = $moduleConfig;
314 $GLOBALS['TBE_MODULES'][$key]['configureModuleFunction'] = array('Tx_Extbase_Utility_Extension', 'configureModule');
315
316 t3lib_extMgm::addModule($main, $sub, $position);
317 }
318
319 // TODO PHPdoc
320 public static function convertCamelCaseToLowerCaseUnderscored($string) {
321 static $conversionMap = array();
322 if (!isset($conversionMap[$string])) {
323 $conversionMap[$string] = strtolower(preg_replace('/(?<=\w)([A-Z])/', '_\\1', $string));
324 }
325 return $conversionMap[$string];
326 }
327
328 public static function convertUnderscoredToLowerCamelCase($string) {
329 $string = str_replace(' ', '', ucwords(str_replace('_', ' ', strtolower($string))));
330 $string[0] = strtolower($string[0]);
331 return $string;
332 }
333
334 public static function convertLowerUnderscoreToUpperCamelCase($camelCasedString) {
335 return t3lib_div::underscoredToUpperCamelCase($camelCasedString);
336 }
337
338 /**
339 * Build the autoload registry for a given extension and place it ext_autoload.php.
340 *
341 * @param string $extensionKey Key of the extension
342 * @param string $extensionPath full path of the extension
343 * @return string HTML string which should be outputted
344 */
345 public function createAutoloadRegistryForExtension($extensionKey, $extensionPath) {
346 $classNameToFileMapping = array();
347 $extensionName = str_replace(' ', '', ucwords(str_replace('_', ' ', $extensionKey)));
348 $errors = $this->buildAutoloadRegistryForSinglePath($classNameToFileMapping, $extensionPath . 'Classes/', '.*tslib.*', '$extensionClassesPath . \'|\'');
349 if ($errors) {
350 return $errors;
351 }
352 $globalPrefix = '$extensionClassesPath = t3lib_extMgm::extPath(\'' . $extensionKey . '\') . \'Classes/\';';
353
354 $errors = array();
355 foreach ($classNameToFileMapping as $className => $fileName) {
356 if (!(strpos($className, 'tx_' . strtolower($extensionName)) === 0)) {
357 $errors[] = $className . ' does not start with Tx_' . $extensionName . ' and was not added to the autoloader registry.';
358 unset($classNameToFileMapping[$className]);
359 }
360 }
361 $autoloadFileString = $this->generateAutoloadPHPFileData($classNameToFileMapping, $globalPrefix);
362 if (!@file_put_contents($extensionPath . 'ext_autoload.php', $autoloadFileString)) {
363 $errors[] = '<b>' . $extensionPath . 'ext_autoload.php could not be written!</b>';
364 }
365 $errors[] = 'Wrote the following data: <pre>' . htmlspecialchars($autoloadFileString) . '</pre>';
366 return implode('<br />', $errors);
367 }
368
369 /**
370 * Generate autoload PHP file data. Takes an associative array with class name to file mapping, and outputs it as PHP.
371 * Does NOT escape the values in the associative array. Includes the <?php ... ?> syntax and an optional global prefix.
372 *
373 * @param array $classNameToFileMapping class name to file mapping
374 * @param string $globalPrefix Global prefix which is prepended to all code.
375 * @return string The full PHP string
376 */
377 protected function generateAutoloadPHPFileData($classNameToFileMapping, $globalPrefix = '') {
378 $output = '<?php' . PHP_EOL;
379 $output .= '// DO NOT CHANGE THIS FILE! It is automatically generated by Tx_Extbase_Utility_Extension::createAutoloadRegistryForExtension.' . PHP_EOL;
380 $output .= '// This file was generated on ' . date('Y-m-d H:i') . PHP_EOL;
381 $output .= PHP_EOL;
382 $output .= $globalPrefix . PHP_EOL;
383 $output .= 'return array(' . PHP_EOL;
384 foreach ($classNameToFileMapping as $className => $quotedFileName) {
385 $output .= ' \'' . $className . '\' => ' . $quotedFileName . ',' . PHP_EOL;
386 }
387 $output .= ');' . PHP_EOL;
388 $output .= '?>';
389 return $output;
390 }
391
392 /**
393 * Generate the $classNameToFileMapping for a given filePath.
394 *
395 * @param array $classNameToFileMapping (Reference to array) All values are appended to this array.
396 * @param string $path Path which should be crawled
397 * @param string $excludeRegularExpression Exclude regular expression, to exclude certain files from being processed
398 * @param string $valueWrap Wrap for the file name
399 * @return void
400 */
401 protected function buildAutoloadRegistryForSinglePath(&$classNameToFileMapping, $path, $excludeRegularExpression = '', $valueWrap = '\'|\'') {
402 // if (file_exists($path . 'Classes/')) {
403 // 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>";
404 // }
405 $extensionFileNames = t3lib_div::removePrefixPathFromList(t3lib_div::getAllFilesAndFoldersInPath(array(), $path, 'php', FALSE, 99, $excludeRegularExpression), $path);
406
407 foreach ($extensionFileNames as $extensionFileName) {
408 $classNamesInFile = $this->extractClassNames($path . $extensionFileName);
409 if (!count($classNamesInFile)) continue;
410 foreach ($classNamesInFile as $className) {
411 $classNameToFileMapping[strtolower($className)] = str_replace('|', $extensionFileName, $valueWrap);
412 }
413 }
414 }
415
416 /**
417 * Extracts class names from the given file.
418 *
419 * @param string $filePath File path (absolute)
420 * @return array Class names
421 */
422 protected function extractClassNames($filePath) {
423 $fileContent = php_strip_whitespace($filePath);
424 $classNames = array();
425 if (function_exists('token_get_all')) {
426 $tokens = token_get_all($fileContent);
427 while(1) {
428 // look for "class" or "interface"
429 $token = $this->findToken($tokens, array(T_ABSTRACT, T_CLASS, T_INTERFACE));
430 // fetch "class" token if "abstract" was found
431 if ($token === 'abstract') {
432 $token = $this->findToken($tokens, array(T_CLASS));
433 }
434 if ($token === false) {
435 // end of file
436 break;
437 }
438 // look for the name (a string) skipping only whitespace and comments
439 $token = $this->findToken($tokens, array(T_STRING), array(T_WHITESPACE, T_COMMENT, T_DOC_COMMENT));
440 if ($token === false) {
441 // unexpected end of file or token: remove found names because of parse error
442 t3lib_div::sysLog('Parse error in "' . $filePath. '".', 'Core', 2);
443 $classNames = array();
444 break;
445 }
446 $token = t3lib_div::strtolower($token);
447 // exclude XLASS classes
448 if (strncmp($token, 'ux_', 3)) {
449 $classNames[] = $token;
450 }
451 }
452 } else {
453 // TODO: parse PHP - skip coments and strings, apply regexp only on the remaining PHP code
454 $matches = array();
455 preg_match_all('/^[ \t]*(?:(?:abstract|final)?[ \t]*(?:class|interface))[ \t\n\r]+([a-zA-Z][a-zA-Z_0-9]*)/mS', $fileContent, $matches);
456 $classNames = array_map('t3lib_div::strtolower', $matches[1]);
457 }
458 return $classNames;
459 }
460
461 /**
462 * Find tokens in the tokenList
463 *
464 * @param array $tokenList list of tokens as returned by token_get_all()
465 * @param array $wantedToken the tokens to be found
466 * @param array $intermediateTokens optional: list of tokens that are allowed to skip when looking for the wanted token
467 * @return mixed
468 */
469 protected function findToken(array &$tokenList, array $wantedTokens, array $intermediateTokens = array()) {
470 $skipAllTokens = count($intermediateTokens) ? false : true;
471
472 $returnValue = false;
473 // Iterate with while since we need the current array position:
474 while (list(,$token) = each($tokenList)) {
475 // parse token (see http://www.php.net/manual/en/function.token-get-all.php for format of token list)
476 if (is_array($token)) {
477 list($id, $text) = $token;
478 } else {
479 $id = $text = $token;
480 }
481 if (in_array($id, $wantedTokens)) {
482 $returnValue = $text;
483 break;
484 }
485 // look for another token
486 if ($skipAllTokens || in_array($id, $intermediateTokens)) {
487 continue;
488 }
489 break;
490 }
491 return $returnValue;
492 }
493
494 }
495 ?>