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