*/
use Psr\Log\LoggerInterface;
+use Symfony\Component\Finder\Finder;
use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher as BackendConditionMatcher;
use TYPO3\CMS\Core\Configuration\TypoScript\ConditionMatching\AbstractConditionMatcher;
use TYPO3\CMS\Core\Log\LogManager;
';
}
+ // Checking for @import syntax imported files
+ $string = self::addImportsFromExternalFiles($string, $cycle_counter, $returnFiles, $includedFiles, $parentFilenameOrPath);
+
// If no tags found, no need to do slower preg_split
if (strpos($string, '<INCLUDE_TYPOSCRIPT:') !== false) {
$splitRegEx = '/\r?\n\s*<INCLUDE_TYPOSCRIPT:\s*(?i)source\s*=\s*"((?i)file|dir):\s*([^"]*)"(.*)>[\ \t]*/';
return $string;
}
+ /**
+ * Splits the unparsed TypoScript content into @import statements
+ *
+ * @param string $typoScript unparsed TypoScript
+ * @param int $cycleCounter counter to stop recursion
+ * @param bool $returnFiles whether to populate the included Files or not
+ * @param array $includedFiles - by reference - if any included files are added, they are added here
+ * @param string $parentFilenameOrPath the current imported file to resolve relative paths - handled by reference
+ * @return string the unparsed TypoScript with included external files
+ */
+ protected static function addImportsFromExternalFiles($typoScript, $cycleCounter, $returnFiles, &$includedFiles, &$parentFilenameOrPath)
+ {
+ // Check for new syntax "@import 'EXT:bennilove/Configuration/TypoScript/*'"
+ if (strpos($typoScript, '@import \'') !== false || strpos($typoScript, '@import "') !== false) {
+ $splitRegEx = '/\r?\n\s*@import\s[\'"]([^\'"]*)[\'"][\ \t]?/';
+ $parts = preg_split($splitRegEx, LF . $typoScript . LF, -1, PREG_SPLIT_DELIM_CAPTURE);
+ // First text part goes through
+ $newString = $parts[0] . LF;
+ $partCount = count($parts);
+ for ($i = 1; $i + 2 <= $partCount; $i += 2) {
+ $filename = $parts[$i];
+ $tsContentsTillNextInclude = $parts[$i + 1];
+ // Resolve a possible relative paths if a parent file is given
+ if ($parentFilenameOrPath !== '' && $filename[0] === '.') {
+ $filename = PathUtility::getAbsolutePathOfRelativeReferencedFileOrPath($parentFilenameOrPath, $filename);
+ }
+ $newString .= self::importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, $includedFiles);
+ // Prepend next normal (not file) part to output string
+ $newString .= $tsContentsTillNextInclude;
+ }
+ // Add a line break before and after the included code in order to make sure that the parser always has a LF.
+ $typoScript = LF . trim($newString) . LF;
+ }
+ return $typoScript;
+ }
+
+ /**
+ * Include file $filename. Contents of the file will be returned, filename is added to &$includedFiles.
+ * Further include/import statements in the contents are processed recursively.
+ *
+ * @param string $filename Full absolute path+filename to the typoscript file to be included
+ * @param int $cycleCounter Counter for detecting endless loops
+ * @param bool $returnFiles When set, filenames of included files will be prepended to the array &$includedFiles
+ * @param array &$includedFiles Array to which the filenames of included files will be prepended (referenced)
+ * @return string the unparsed TypoScript content from external files
+ */
+ protected static function importExternalTypoScriptFile($filename, $cycleCounter, $returnFiles, array &$includedFiles)
+ {
+ if (strpos('..', $filename) !== false) {
+ return self::typoscriptIncludeError('Invalid filepath "' . $filename . '" (containing "..").');
+ }
+
+ $content = '';
+ $absoluteFileName = GeneralUtility::getFileAbsFileName($filename);
+ if ((string)$absoluteFileName === '') {
+ return self::typoscriptIncludeError('Illegal filepath "' . $filename . '".');
+ }
+
+ $finder = new Finder();
+ $finder
+ // no recursive mode on purpose
+ ->depth(0)
+ // no directories should be fetched
+ ->files()
+ ->sortByName();
+
+ // Search all files in the folder
+ if (is_dir($absoluteFileName)) {
+ $finder->in($absoluteFileName);
+ // Used for the TypoScript comments
+ $readableFilePrefix = $filename;
+ } else {
+ // Apparently this is not a folder, so the restriction
+ // is the folder so we restrict into this folder
+ $finder->in(dirname($absoluteFileName));
+ if (!is_file($absoluteFileName)
+ && strpos(basename($absoluteFileName), '*') === false
+ && substr(basename($absoluteFileName), -11) !== '.typoscript') {
+ $absoluteFileName .= '*.typoscript';
+ }
+ $finder->name(basename($absoluteFileName));
+ $readableFilePrefix = dirname($filename);
+ }
+
+ foreach ($finder as $fileObject) {
+ // Clean filename output for comments
+ $readableFileName = rtrim($readableFilePrefix, '/') . '/' . $fileObject->getFilename();
+ $content .= '### @import \'' . $readableFileName . '\' begin ###' . LF;
+ // Check for allowed files
+ if (!GeneralUtility::verifyFilenameAgainstDenyPattern($fileObject->getFilename())) {
+ $content .= self::typoscriptIncludeError('File "' . $readableFileName . '" was not included since it is not allowed due to fileDenyPattern.');
+ } else {
+ $includedFiles[] = $fileObject->getPathname();
+ // check for includes in included text
+ $included_text = self::checkIncludeLines($fileObject->getContents(), $cycleCounter++, $returnFiles, $absoluteFileName);
+ // If the method also has to return all included files, merge currently included
+ // files with files included by recursively calling itself
+ if ($returnFiles && is_array($included_text)) {
+ $includedFiles = array_merge($includedFiles, $included_text['files']);
+ $included_text = $included_text['typoscript'];
+ }
+ $content .= $included_text . LF;
+ }
+ $content .= '### @import \'' . $readableFileName . '\' end ###' . LF;
+
+ // load default TypoScript for content rendering templates like
+ // fluid_styled_content if those have been included through e.g.
+ // @import "fluid_styled_content/Configuration/TypoScript/setup.typoscript"
+ if (strpos(strtoupper($filename), 'EXT:') === 0) {
+ $filePointerPathParts = explode('/', substr($filename, 4));
+ // remove file part, determine whether to load setup or constants
+ list($includeType) = explode('.', array_pop($filePointerPathParts));
+
+ if (in_array($includeType, ['setup', 'constants'], true)) {
+ // adapt extension key to required format (no underscores)
+ $filePointerPathParts[0] = str_replace('_', '', $filePointerPathParts[0]);
+
+ // load default TypoScript
+ $defaultTypoScriptKey = implode('/', $filePointerPathParts) . '/';
+ if (in_array($defaultTypoScriptKey, $GLOBALS['TYPO3_CONF_VARS']['FE']['contentRenderingTemplates'], true)) {
+ $content .= $GLOBALS['TYPO3_CONF_VARS']['FE']['defaultTypoScript_' . $includeType . '.']['defaultContentRendering'];
+ }
+ }
+ }
+ }
+
+ if (empty($content)) {
+ return self::typoscriptIncludeError('No file or folder found for importing TypoScript on "' . $filename . '".');
+ }
+ return $content;
+ }
+
/**
* Include file $filename. Contents of the file will be prepended to &$newstring, filename to &$includedFiles
* Further include_typoscript tags in the contents are processed recursively
--- /dev/null
+.. include:: ../../Includes.txt
+
+===========================================================
+Feature: #82812 - New syntax for importing TypoScript files
+===========================================================
+
+See :issue:`82812`
+
+Description
+===========
+
+A new syntax for importing external TypoScript files has been introduced, which acts as a preprocessor
+before the actual parsing (Condition evaluation) is made.
+
+It's main purpose is ease the use of TypoScript includes and make it easier for integrators and
+frontend developers to work with distributed TypoScript files. The syntax is inspired by SASS
+imports and works as follows:
+
+.. code-block:: typoscript
+
+ # Import a single file
+ @import 'EXT:myproject/TypoScript/Configuration/randomfile.typoscript'
+
+ # Import multiple files in a single directory, sorted by file name
+ @import 'EXT:myproject/TypoScript/Configuration/*.typoscript'
+
+ # Import all files in a directory
+ @import 'EXT:myproject/TypoScript/Configuration/'
+
+ # It's possible to omit the file ending, then "typoscript" is automatically added
+ @import 'EXT:myproject/TypoScript/Configuration/'
+
+The main benefits of `@import` over using `<INCLUDE_TYPOSCRIPT>` are:
+- Less error-prone when adding statements to TypoScript
+- Easier to read what should be included (files, folders - instead of `FILE:` and `DIR:` syntax)
+- @import is more speaking than a pseudo-XML syntax
+
+The following rules apply:
+- If multiple files are found, the file name is important in which order the files (sorted
+alphabetically by filename)
+- Recursive inclusion of files (@import within @import is possible)
+- It is not possible to use a condition as possible with <INCLUDE_TYPOSCRIPT condition=""> as its
+sole purpose is to include files, which happens before the actual real condition matching happens,
+and the INCLUDE_TYPOSCRIPT condition syntax is a conceptual mistake, and should be avoided.
+- Both `<INCLUDE_TYPOSCRIPT>` and `@import` can work side-by-side in the same project
+- If a directory is included, it will not include files recursively
+- Quoting of the filename is necessary, both double quotes (") and single tickst (') can be used
+
+The syntax is designed to stay, and @import is not intended to be extended with more logic in the
+future. However, it may be possible that TypoScript will get more preparsing logic via the @ annotation.
+
+Under the hood, Symfony Finder is used to detect files. This makes globbing (* syntax) possible.
+
+.. index:: TypoScript
\ No newline at end of file
$this->assertNotContains('INCLUDE_TYPOSCRIPT', $resolvedIncludeLines);
}
+ /**
+ * @return array
+ */
+ public function importFilesDataProvider()
+ {
+ return [
+ 'Found include file is imported' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.txt"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.txt\' begin ###
+test.Core.TypoScript = 1
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.txt\' end ###
+'
+ ],
+ 'Not found file is not imported' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/notfoundfile.txt"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+
+###
+### ERROR: No file or folder found for importing TypoScript on "EXT:core/Tests/Unit/TypoScript/Fixtures/notfoundfile.txt".
+###
+'
+ ],
+ 'All files with glob are imported' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/*.txt"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.txt\' begin ###
+test.Core.TypoScript = 1
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.txt\' end ###
+'
+ ],
+ 'Specific file with typoscript ending is imported' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+'
+ ],
+ 'All files in folder are imported, sorted by name' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/badfilename.php\' begin ###
+
+###
+### ERROR: File "EXT:core/Tests/Unit/TypoScript/Fixtures/badfilename.php" was not included since it is not allowed due to fileDenyPattern.
+###
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/badfilename.php\' end ###
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.txt\' begin ###
+test.Core.TypoScript = 1
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/ext_typoscript_setup.txt\' end ###
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+'
+ ],
+ 'All files ending with typoscript in folder are imported' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/*typoscript"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+'
+ ],
+ 'All typoscript files in folder are imported' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/*.typoscript"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' begin ###
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/recursive_includes_setup.typoscript\' end ###
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+'
+ ],
+ 'All typoscript files in folder with glob are not imported due to recursion level=0' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/**/*.typoscript"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+
+###
+### ERROR: No file or folder found for importing TypoScript on "EXT:core/Tests/Unit/**/*.typoscript".
+###
+'
+ ], 'TypoScript file ending is automatically added' => [
+ // Input TypoScript
+ 'bennilove = before
+@import "EXT:core/Tests/Unit/TypoScript/Fixtures/setup"
+'
+ ,
+ // Expected
+ '
+bennilove = before
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' begin ###
+test.TYPO3Forever.TypoScript = 1
+
+### @import \'EXT:core/Tests/Unit/TypoScript/Fixtures/setup.typoscript\' end ###
+'
+ ],
+ ];
+ }
+
+ /**
+ * @test
+ * @dataProvider importFilesDataProvider
+ */
+ public function importFiles($typoScript, $expected)
+ {
+ $resolvedIncludeLines = TypoScriptParser::checkIncludeLines($typoScript);
+ $this->assertEquals($expected, $resolvedIncludeLines);
+ }
+
/**
* @param string $typoScript
* @param array $expected