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