├── .gitignore ├── Build ├── Scripts │ └── runTests.sh └── phpunit │ ├── FunctionalTests.xml │ ├── FunctionalTestsBootstrap.php │ ├── UnitTests.xml │ └── UnitTestsBootstrap.php ├── Classes ├── Command │ ├── CheckContentEscapingCommand.php │ └── GenerateXsdCommand.php ├── Domain │ └── Model │ │ ├── DateTime.php │ │ ├── FalFile.php │ │ ├── FalImage.php │ │ ├── File.php │ │ ├── Image.php │ │ ├── Labels.php │ │ ├── LanguageNavigationItem.php │ │ ├── Link.php │ │ ├── LocalFile.php │ │ ├── LocalImage.php │ │ ├── Navigation.php │ │ ├── NavigationItem.php │ │ ├── PlaceholderImage.php │ │ ├── RemoteFile.php │ │ ├── RemoteImage.php │ │ ├── RequiredSlotPlaceholder.php │ │ ├── Slot.php │ │ ├── Traits │ │ └── FalFileTrait.php │ │ └── Typolink.php ├── Exception │ ├── FileReferenceNotFoundException.php │ ├── InvalidArgumentException.php │ ├── InvalidFileArrayException.php │ ├── InvalidFilePathException.php │ ├── InvalidRemoteFileException.php │ └── InvalidRemoteImageException.php ├── Fluid │ └── ViewHelper │ │ ├── ComponentRenderer.php │ │ ├── ComponentResolver.php │ │ └── ViewHelperResolverFactory.php ├── Interfaces │ ├── ComponentAware.php │ ├── ConstructibleFromArray.php │ ├── ConstructibleFromClosure.php │ ├── ConstructibleFromDateTime.php │ ├── ConstructibleFromDateTimeImmutable.php │ ├── ConstructibleFromExtbaseFile.php │ ├── ConstructibleFromFileInterface.php │ ├── ConstructibleFromInteger.php │ ├── ConstructibleFromNull.php │ ├── ConstructibleFromString.php │ ├── ConstructibleFromTypolinkParameter.php │ ├── EscapedParameter.php │ ├── ImageWithCropVariants.php │ ├── ImageWithDimensions.php │ ├── ProcessableImage.php │ └── RenderingContextAware.php ├── Service │ ├── PlaceholderImageService.php │ └── XsdGenerator.php ├── ServiceProvider.php ├── Utility │ ├── ComponentArgumentConverter.php │ ├── ComponentLoader.php │ ├── ComponentPrefixer │ │ ├── ComponentPrefixerInterface.php │ │ └── GenericComponentPrefixer.php │ └── ComponentSettings.php └── ViewHelpers │ ├── ComponentViewHelper.php │ ├── ContentViewHelper.php │ ├── Form │ ├── FieldInformationViewHelper.php │ └── TranslatedValidationResultsViewHelper.php │ ├── ParamViewHelper.php │ ├── RendererViewHelper.php │ ├── SlotViewHelper.php │ ├── Translate │ └── LabelsViewHelper.php │ └── Variable │ ├── MapViewHelper.php │ └── PushViewHelper.php ├── Configuration └── Services.yaml ├── Documentation ├── AutoCompletion.md ├── ComponentPrefixers.md ├── ComponentSettings.md ├── DataStructures.md ├── Forms.md ├── UpdateNotes.md ├── ViewHelperReference.md ├── Xsd │ └── fc.xsd └── XssIssue.md ├── LICENSE ├── README.md ├── Resources ├── Private │ └── Templates │ │ └── Placeholder.svg └── Public │ └── Icons │ └── Extension.svg ├── composer.json ├── ext_emconf.php └── ext_localconf.php /.gitignore: -------------------------------------------------------------------------------- 1 | .Build 2 | .cache 3 | /Build/phpunit/.phpunit.cache/ 4 | /var 5 | composer.lock 6 | -------------------------------------------------------------------------------- /Build/phpunit/FunctionalTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | ../../Tests/Functional/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Build/phpunit/FunctionalTestsBootstrap.php: -------------------------------------------------------------------------------- 1 | defineOriginalRootPath(); 18 | $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); 19 | $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); 20 | }); 21 | -------------------------------------------------------------------------------- /Build/phpunit/UnitTests.xml: -------------------------------------------------------------------------------- 1 | 2 | 18 | 19 | 20 | ../../Tests/Unit/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Build/phpunit/UnitTestsBootstrap.php: -------------------------------------------------------------------------------- 1 | getWebRoot(), '/')); 27 | } 28 | if (!getenv('TYPO3_PATH_WEB')) { 29 | putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); 30 | } 31 | 32 | $testbase->defineSitePath(); 33 | 34 | $requestType = TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI; 35 | TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::run(0, $requestType); 36 | 37 | $testbase->createDirectory(TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3conf/ext'); 38 | $testbase->createDirectory(TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/assets'); 39 | $testbase->createDirectory(TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/tests'); 40 | $testbase->createDirectory(TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/transient'); 41 | 42 | // Retrieve an instance of class loader and inject to core bootstrap 43 | $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; 44 | TYPO3\CMS\Core\Core\Bootstrap::initializeClassLoader($classLoader); 45 | 46 | // Initialize default TYPO3_CONF_VARS 47 | $configurationManager = new TYPO3\CMS\Core\Configuration\ConfigurationManager(); 48 | $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); 49 | 50 | $cache = new TYPO3\CMS\Core\Cache\Frontend\PhpFrontend( 51 | 'core', 52 | new TYPO3\CMS\Core\Cache\Backend\NullBackend('production', []) 53 | ); 54 | 55 | // Set all packages to active 56 | if (interface_exists(TYPO3\CMS\Core\Package\Cache\PackageCacheInterface::class)) { 57 | $packageManager = TYPO3\CMS\Core\Core\Bootstrap::createPackageManager( 58 | TYPO3\CMS\Core\Package\UnitTestPackageManager::class, 59 | TYPO3\CMS\Core\Core\Bootstrap::createPackageCache($cache) 60 | ); 61 | } else { 62 | // v10 compatibility layer 63 | $packageManager = TYPO3\CMS\Core\Core\Bootstrap::createPackageManager( 64 | TYPO3\CMS\Core\Package\UnitTestPackageManager::class, 65 | $cache 66 | ); 67 | } 68 | 69 | TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(TYPO3\CMS\Core\Package\PackageManager::class, $packageManager); 70 | TYPO3\CMS\Core\Utility\ExtensionManagementUtility::setPackageManager($packageManager); 71 | 72 | $testbase->dumpClassLoadingInformation(); 73 | 74 | TYPO3\CMS\Core\Utility\GeneralUtility::purgeInstances(); 75 | }); 76 | -------------------------------------------------------------------------------- /Classes/Command/CheckContentEscapingCommand.php: -------------------------------------------------------------------------------- 1 | setDescription( 66 | 'Checks for possible escaping issues with content parameter due to new children escaping behavior' 67 | ); 68 | } 69 | 70 | protected function execute(InputInterface $input, OutputInterface $output): int 71 | { 72 | $componentNamespaces = $this->componentLoader->getNamespaces(); 73 | $templateFiles = $this->discoverTemplateFiles(); 74 | 75 | $progress = new ProgressBar($output, count($componentNamespaces) + count($templateFiles)); 76 | $progress->start(); 77 | 78 | // Determine which components use {content -> f:format.raw()} or similar 79 | foreach ($componentNamespaces as $namespace => $path) { 80 | foreach ($this->componentLoader->findComponentsInNamespace($namespace) as $className => $file) { 81 | try { 82 | $template = $this->parseTemplate($file); 83 | } catch (\TYPO3Fluid\Fluid\Core\Parser\Exception $e) { 84 | $this->addResult($file, $e->getMessage(), true); 85 | continue; 86 | } 87 | 88 | if ($this->detectRawContentVariable($template->getRootNode())) { 89 | $this->affectedComponents[$className] = $file; 90 | } 91 | } 92 | $progress->advance(); 93 | } 94 | 95 | // Check all templates for usage of the content parameter in combination with variables 96 | foreach ($templateFiles as $file) { 97 | $file = (string)$file; 98 | try { 99 | $template = $this->parseTemplate($file); 100 | } catch (\TYPO3Fluid\Fluid\Core\Parser\Exception $e) { 101 | $this->addResult($file, $e->getMessage(), true); 102 | continue; 103 | } 104 | 105 | $results = $this->detectEscapedVariablesPassedAsContent($template->getRootNode()); 106 | foreach ($results as $result) { 107 | $this->addResult($file, sprintf( 108 | 'Component "%s" expects raw html content, but was called with potentially escaped variables: %s', 109 | $this->cleanupPathForOutput($result[0]), 110 | implode(', ', array_map(fn($variableName) => '{' . $variableName . '}', $result[1])) 111 | )); 112 | } 113 | $progress->advance(); 114 | } 115 | 116 | $progress->finish(); 117 | 118 | // Sort results alphabetically 119 | ksort($this->results); 120 | 121 | // Output results 122 | $output->writeln(''); 123 | foreach ($this->results as $file => $messages) { 124 | $output->writeln(''); 125 | $output->writeln(sprintf( 126 | '%s:', 127 | $this->cleanupPathForOutput($file) 128 | )); 129 | $output->writeln(''); 130 | 131 | foreach ($messages as $message) { 132 | $output->writeln($message); 133 | $output->writeln(''); 134 | } 135 | } 136 | 137 | return 0; 138 | } 139 | 140 | public function detectRawContentVariable(NodeInterface $node, array $parents = []): bool 141 | { 142 | $node = $this->resolveEscapingNode($node); 143 | 144 | $lastParent = count($parents) - 1; 145 | foreach ($node->getChildNodes() as $childNode) { 146 | $childNode = $this->resolveEscapingNode($childNode); 147 | 148 | // Check all parent elements of content variable 149 | if ($childNode instanceof ObjectAccessorNode && $childNode->getObjectPath() === ComponentRenderer::DEFAULT_SLOT) { 150 | for ($i = $lastParent; $i >= 0; $i--) { 151 | // Skip all non-viewhelpers 152 | if (!($parents[$i] instanceof ViewHelperNode)) { 153 | continue; 154 | } 155 | 156 | // Check for f:format.raw 157 | if ($parents[$i]->getUninitializedViewHelper() instanceof RawViewHelper) { 158 | return true; 159 | } 160 | } 161 | } 162 | 163 | // Check if the slot ViewHelper is present 164 | if ($childNode instanceof ViewHelperNode) { 165 | $viewHelper = $childNode->getUninitializedViewHelper(); 166 | if ($viewHelper instanceof SlotViewHelper) { 167 | return true; 168 | } 169 | } 170 | 171 | // Search for more occurances of content variable 172 | $result = $this->detectRawContentVariable($childNode, array_merge($parents, [$childNode])); 173 | if ($result) { 174 | return true; 175 | } 176 | } 177 | 178 | return false; 179 | } 180 | 181 | public function detectEscapedVariablesPassedAsContent(NodeInterface $node): array 182 | { 183 | $node = $this->resolveEscapingNode($node); 184 | 185 | $results = []; 186 | foreach ($node->getChildNodes() as $childNode) { 187 | $childNode = $this->resolveEscapingNode($childNode); 188 | 189 | // Check if a component was used 190 | if ($childNode instanceof ViewHelperNode) { 191 | $viewHelper = $childNode->getUninitializedViewHelper(); 192 | if ($viewHelper instanceof ComponentRenderer && 193 | isset($this->affectedComponents[$viewHelper->getComponentNamespace()]) 194 | ) { 195 | // Check if variables were used inside of content parameter 196 | $contentNode = $childNode->getArguments()[ComponentRenderer::DEFAULT_SLOT] ?? $childNode; 197 | $variableNames = $this->checkForVariablesWithoutRaw($contentNode); 198 | if (!empty($variableNames)) { 199 | $results[] = [ 200 | $this->affectedComponents[$viewHelper->getComponentNamespace()], 201 | $variableNames, 202 | ]; 203 | } 204 | continue; 205 | } 206 | } 207 | 208 | $results = array_merge( 209 | $results, 210 | $this->detectEscapedVariablesPassedAsContent($childNode) 211 | ); 212 | } 213 | 214 | return $results; 215 | } 216 | 217 | public function checkForVariablesWithoutRaw(NodeInterface $node, array $parents = []): array 218 | { 219 | $node = $this->resolveEscapingNode($node); 220 | 221 | $variableNames = []; 222 | $lastParent = count($parents) - 1; 223 | foreach ($node->getChildNodes() as $childNode) { 224 | $childNode = $this->resolveEscapingNode($childNode); 225 | 226 | // Check all parent elements of variables 227 | if ($childNode instanceof ObjectAccessorNode && 228 | !in_array($childNode->getObjectPath(), static::IGNORED_VARIABLES) 229 | ) { 230 | for ($i = $lastParent; $i >= 0; $i--) { 231 | // Skip all non-viewhelpers 232 | if (!$parents[$i] instanceof ViewHelperNode) { 233 | continue; 234 | } 235 | 236 | // Check for f:format.raw etc. 237 | $viewHelper = $parents[$i]->getUninitializedViewHelper(); 238 | if (in_array($viewHelper::class, static::RAW_VIEWHELPERS)) { 239 | continue 2; 240 | } 241 | } 242 | 243 | $variableNames[] = $childNode->getObjectPath(); 244 | continue; 245 | } 246 | 247 | // Search for more occurances of variables 248 | $variableNames = array_merge( 249 | $variableNames, 250 | $this->checkForVariablesWithoutRaw($childNode, array_merge($parents, [$childNode])) 251 | ); 252 | } 253 | 254 | return $variableNames; 255 | } 256 | 257 | protected function discoverTemplateFiles(): array 258 | { 259 | // All extensions in local extension directory 260 | $activeExtensions = array_filter($this->packageManager->getActivePackages(), fn($package) => str_starts_with((string) $package->getPackagePath(), Environment::getExtensionsPath()) 261 | || $package->getPackageMetaData()->getPackageType() === 'typo3-cms-extension'); 262 | 263 | // All template paths (Resources/Private/) 264 | $possibleTemplatePaths = array_map(fn($package) => ExtensionManagementUtility::extPath($package->getPackageKey(), 'Resources/Private/'), $activeExtensions); 265 | $possibleTemplatePaths = array_filter($possibleTemplatePaths, 'file_exists'); 266 | 267 | if (empty($possibleTemplatePaths)) { 268 | return []; 269 | } 270 | 271 | // Find all html files 272 | $finder = new Finder(); 273 | $finder 274 | ->in($possibleTemplatePaths) 275 | ->files()->name('*.html'); 276 | return iterator_to_array($finder); 277 | } 278 | 279 | protected function addResult(string $file, string $message, bool $isError = false): void 280 | { 281 | $this->results[$file] ??= []; 282 | $format = ($isError) ? '%s' : '%s'; 283 | $this->results[$file][] = sprintf($format, $message); 284 | } 285 | 286 | protected function cleanupPathForOutput(string $path): string 287 | { 288 | return trim(str_replace(Environment::getProjectPath(), '', $path), '/'); 289 | } 290 | 291 | protected function resolveEscapingNode(NodeInterface $node): NodeInterface 292 | { 293 | return ($node instanceof EscapingNode) ? $this->resolveEscapingNode($node->getNode()) : $node; 294 | } 295 | 296 | protected function parseTemplate(string $file): ParsingState 297 | { 298 | $this->templates[$file] ??= $this->getTemplateParser()->parse( 299 | file_get_contents($file), 300 | $file 301 | ); 302 | return $this->templates[$file]; 303 | } 304 | 305 | protected function getTemplateParser(): TemplateParser 306 | { 307 | if (GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() < 13) { 308 | return (new StandaloneView())->getRenderingContext()->getTemplateParser(); 309 | } 310 | 311 | $view = GeneralUtility::makeInstance(ViewFactoryInterface::class)->create(new ViewFactoryData()); 312 | if ($view instanceof FluidViewAdapter) { 313 | return $view->getRenderingContext()->getTemplateParser(); 314 | } 315 | throw new RuntimeException('view must be an instance of \TYPO3\CMS\Core\View\FluidViewAdapter', 1726643308); 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /Classes/Command/GenerateXsdCommand.php: -------------------------------------------------------------------------------- 1 | setDescription( 23 | 'Generates xsd files for all fluid-components' 24 | ); 25 | $this->setHelp( 26 | <<<'EOH' 27 | Generates Schema documentation (XSD) for your fluid components, preparing the 28 | file to be placed online and used by any XSD-aware editor. 29 | After creating the XSD file, reference it in your IDE and import the namespace 30 | in your Fluid template by adding the xmlns:* attribute(s): 31 | 32 | EOH 33 | ); 34 | $this->addArgument( 35 | 'path', 36 | InputArgument::OPTIONAL, 37 | 'Path where to store the xsd files', 38 | '.' 39 | ); 40 | $this->addOption( 41 | 'namespace', 42 | 'nc', 43 | InputOption::VALUE_OPTIONAL, 44 | 'Namespace to generate xsd for', 45 | null 46 | ); 47 | } 48 | 49 | protected function execute(InputInterface $input, OutputInterface $output): int 50 | { 51 | $path = $input->getArgument('path'); 52 | if (substr((string) $path, 0, 1) !== DIRECTORY_SEPARATOR) { 53 | $path = realpath(getcwd() . DIRECTORY_SEPARATOR . $path); 54 | } 55 | if ($output->isVerbose()) { 56 | $output->writeln('Path: ' . $path); 57 | } 58 | if (!is_dir($path)) { 59 | throw new Exception('Directory \'' . $input->getArgument('path') . '\' does not exist.', 1582535395); 60 | } 61 | $xsdTargetNameSpaces = $this->xsdGenerator->generateXsd($path, $input->getOption('namespace')); 62 | if (count($xsdTargetNameSpaces) === 0) { 63 | $output->writeln('Namespace(s) not found.'); 64 | return 1; 65 | } else { 66 | // add fluid component view helpers (only to complete the namespace xml declaration) 67 | $xsdTargetNameSpaces['fc'][] = 'http://typo3.org/ns/SMS/FluidComponents/ViewHelpers'; 68 | if ($output->isVerbose()) { 69 | $xmlHeader = ' $targetNameSpacesForPrefix) { 71 | foreach ($targetNameSpacesForPrefix as $targetNameSpaceForPrefix) { 72 | $xmlHeader .= 'xmlns:' . $prefix . '="' . $targetNameSpaceForPrefix . '"' . "\n"; 73 | } 74 | } 75 | $xmlHeader .= 'data-namespace-typo3-fluid="true">'; 76 | $output->writeln('Import the namespaces in your Fluid template by adding the xmlns:* attributes:'); 77 | $output->writeln('' . $xmlHeader . ''); 78 | } 79 | return 0; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Classes/Domain/Model/DateTime.php: -------------------------------------------------------------------------------- 1 | format(DateTimeInterface::RFC3339_EXTENDED)); 43 | } 44 | 45 | /** 46 | * Passes immutable datetime object. 47 | * 48 | * @throws Exception 49 | */ 50 | public static function fromDateTimeImmutable(DateTimeImmutable $value): self 51 | { 52 | return new static($value->format(DateTimeInterface::RFC3339_EXTENDED)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Classes/Domain/Model/FalFile.php: -------------------------------------------------------------------------------- 1 | file->getProperty('alternative'); 30 | } 31 | 32 | public function getCopyright(): ?string 33 | { 34 | return parent::getCopyright() ?? $this->file->getProperty('copyright'); 35 | } 36 | 37 | public function getHeight(): int 38 | { 39 | return (int) $this->file->getProperty('height'); 40 | } 41 | 42 | public function getWidth(): int 43 | { 44 | return (int) $this->file->getProperty('width'); 45 | } 46 | 47 | public function getDefaultCrop(): Area 48 | { 49 | $cropVariantCollection = CropVariantCollection::create((string)$this->file->getProperty('crop')); 50 | return $cropVariantCollection->getCropArea(); 51 | } 52 | 53 | public function getCropVariant(string $name): Area 54 | { 55 | $cropVariantCollection = CropVariantCollection::create((string)$this->file->getProperty('crop')); 56 | return $cropVariantCollection->getCropArea($name); 57 | } 58 | 59 | public function process(int $width, int $height, ?string $format, Area $cropArea): FalImage 60 | { 61 | $imageService = GeneralUtility::makeInstance(ImageService::class); 62 | $processedImage = $imageService->applyProcessingInstructions($this->getFile(), [ 63 | 'width' => $width, 64 | 'height' => $height, 65 | 'fileExtension' => $format, 66 | 'crop' => ($cropArea->isEmpty()) ? null : $cropArea->makeAbsoluteBasedOnFile($this->getFile()) 67 | ]); 68 | return new FalImage($processedImage); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Classes/Domain/Model/File.php: -------------------------------------------------------------------------------- 1 | retrieveFileOrFolderObject($value); 71 | return ($file) ? new FalFile($file) : null; 72 | } 73 | } 74 | 75 | /** 76 | * Creates a file object based on a FAL file uid. 77 | */ 78 | public static function fromInteger(int $value): self 79 | { 80 | return static::fromFileUid($value); 81 | } 82 | 83 | /** 84 | * Creates a file object based on data passed by array. There are numerous 85 | * array structures that create a valid file object: 86 | * 87 | * FAL file object: 88 | * [ "fileObject" => $myFileObject ] 89 | * 90 | * FAL file uid: 91 | * 92 | * [ "fileUid" => 123 ] 93 | * 94 | * FAL file reference uid: 95 | * 96 | * [ "fileReferenceUid" => 456 ] 97 | * 98 | * FAL file reference data: 99 | * 100 | * [ 101 | * "fileReference" => [ 102 | * "tableName" => "pages", 103 | * "fieldName" => "media", 104 | * "uid" => 123, 105 | * "counter" => 0 106 | * ] 107 | * ] 108 | * 109 | * Static file path: 110 | * 111 | * [ "file" => "EXT:my_extension/Resources/Public/Files/MyFile.txt" ] 112 | * 113 | * [ 114 | * "resource" => [ 115 | * "extensionName" => "myExtension", 116 | * "path" => "Files/MyFile.txt" 117 | * ] 118 | * ] 119 | * 120 | * [ 121 | * "resource" => [ 122 | * "extensionKey" => "my_extension", 123 | * "path" => "Files/MyFile.txt" 124 | * ] 125 | * ] 126 | * 127 | * Static file uri: 128 | * 129 | * [ "file" => "https://example.com/MyFile.txt" ] 130 | * 131 | * @throws FileReferenceNotFoundException|InvalidArgumentException 132 | */ 133 | public static function fromArray(array $value): ?self 134 | { 135 | // Create an imafe from an existing FAL object 136 | if (isset($value['fileObject'])) { 137 | $file = static::fromFileInterface($value['fileObject']); 138 | // Create a file from file uid 139 | } elseif (isset($value['fileUid'])) { 140 | $file = static::fromFileUid((int) $value['fileUid']); 141 | // Create a file from file reference uid 142 | } elseif (isset($value['fileReferenceUid'])) { 143 | $file = static::fromFileReferenceUid((int) $value['fileReferenceUid']); 144 | // Create a file from file reference data (table, field, uid, counter) 145 | } elseif (isset($value['fileReference']) && is_array($value['fileReference'])) { 146 | $fileReference = $value['fileReference']; 147 | 148 | if (!isset($fileReference['tableName']) 149 | || !isset($fileReference['fieldName']) 150 | || !isset($fileReference['uid']) 151 | ) { 152 | throw new InvalidArgumentException(sprintf( 153 | 'Invalid file reference description: %s', 154 | print_r($fileReference, true) 155 | ), 1562916587); 156 | } 157 | 158 | $file = static::fromFileReference( 159 | (string) $fileReference['tableName'], 160 | (string) $fileReference['fieldName'], 161 | (int) $fileReference['uid'], 162 | (int) ($fileReference['counter'] ?? 0) 163 | ); 164 | // Create a file from a static resource in an extension (Resources/Public/...) 165 | } elseif (isset($value['resource']) && is_array($value['resource'])) { 166 | $resource = $value['resource']; 167 | 168 | if (!isset($resource['path'])) { 169 | throw new InvalidArgumentException(sprintf( 170 | 'Missing path for file resource: %s', 171 | print_r($resource, true) 172 | ), 1564492445); 173 | } 174 | 175 | if (isset($resource['extensionKey'])) { 176 | $extensionKey = $resource['extensionKey']; 177 | } elseif (isset($resource['extensionName'])) { 178 | $extensionKey = GeneralUtility::camelCaseToLowerCaseUnderscored( 179 | $resource['extensionName'] 180 | ); 181 | } else { 182 | throw new InvalidArgumentException(sprintf( 183 | 'Missing extension key or extension name for file resource: %s', 184 | print_r($resource, true) 185 | ), 1564492446); 186 | } 187 | 188 | $file = static::fromExtensionResource($extensionKey, $resource['path']); 189 | // Create a file from a file path or uri 190 | } elseif (isset($value['file'])) { 191 | $file = static::fromString((string) $value['file']); 192 | } else { 193 | throw new InvalidFileArrayException(sprintf( 194 | 'Invalid set of arguments for conversion to file instance: %s', 195 | print_r($value, true) 196 | ), 1562916607); 197 | } 198 | 199 | if (isset($value['title'])) { 200 | $file->setTitle($value['title']); 201 | } 202 | 203 | if (isset($value['description'])) { 204 | $file->setDescription($value['description']); 205 | } 206 | 207 | if (isset($value['properties'])) { 208 | $file->setProperties($value['properties']); 209 | } 210 | 211 | return $file; 212 | } 213 | 214 | /** 215 | * Creates a file object as a wrapper around an existing FAL object. 216 | */ 217 | public static function fromFileInterface(FileInterface $value): self 218 | { 219 | return new FalFile($value); 220 | } 221 | 222 | public static function fromExtbaseFile(FileReference $value): self 223 | { 224 | return static::fromFileInterface($value->getOriginalResource()); 225 | } 226 | 227 | /** 228 | * Creates a file object based on a FAL file uid. 229 | */ 230 | public static function fromFileUid(int $fileUid): self 231 | { 232 | $fileRepository = GeneralUtility::makeInstance(FileRepository::class); 233 | $file = $fileRepository->findByUid($fileUid); 234 | return static::fromFileInterface($file); 235 | } 236 | 237 | /** 238 | * Creates a file object based on a FAL file reference uid. 239 | */ 240 | public static function fromFileReferenceUid(int $fileReferenceUid): self 241 | { 242 | $fileReference = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject($fileReferenceUid); 243 | return static::fromFileInterface($fileReference); 244 | } 245 | 246 | /** 247 | * Creates a file object based on file reference data. 248 | * 249 | * @param string $tableName database table where the file is referenced 250 | * @param string $fieldName database field name in which the file is referenced 251 | * @param int $uid uid of the database record in which the file is referenced 252 | * @param int $counter zero-based index of the file reference to use 253 | * (in case there are multiple) 254 | * 255 | * @throws FileReferenceNotFoundException 256 | * 257 | * @return self 258 | */ 259 | public static function fromFileReference( 260 | string $tableName, 261 | string $fieldName, 262 | int $uid, 263 | int $counter = 0 264 | ): self { 265 | $fileRepository = GeneralUtility::makeInstance(FileRepository::class); 266 | $fileReferences = $fileRepository->findByRelation( 267 | (string) $tableName, 268 | (string) $fieldName, 269 | (int) $uid 270 | ); 271 | 272 | if (!isset($fileReferences[$counter])) { 273 | throw new FileReferenceNotFoundException(sprintf( 274 | 'File reference in %s.%s for uid %d at position %d could not be found.', 275 | $tableName, 276 | $fieldName, 277 | $uid, 278 | $counter 279 | ), 1564495695); 280 | } 281 | 282 | return static::fromFileInterface($fileReferences[$counter]); 283 | } 284 | 285 | /** 286 | * Creates a file object based on a static resource in an extension 287 | * (Resources/Public/...). 288 | * 289 | * @see \TYPO3\CMS\Fluid\ViewHelpers\Uri\ResourceViewHelper 290 | */ 291 | public static function fromExtensionResource(string $extensionKey, string $path): ?self 292 | { 293 | return static::fromString('EXT:' . $extensionKey . '/Resources/Public/' . $path); 294 | } 295 | 296 | public function getType(): string 297 | { 298 | return $this->type; 299 | } 300 | 301 | public function getTitle(): ?string 302 | { 303 | return $this->title; 304 | } 305 | 306 | public function setTitle(?string $title): self 307 | { 308 | $this->title = $title; 309 | return $this; 310 | } 311 | 312 | public function getDescription(): ?string 313 | { 314 | return $this->description; 315 | } 316 | 317 | public function setDescription(?string $description): self 318 | { 319 | $this->description = $description; 320 | return $this; 321 | } 322 | 323 | public function getProperties(): ?array 324 | { 325 | return $this->properties; 326 | } 327 | 328 | public function setProperties(?array $properties): self 329 | { 330 | $this->properties = $properties; 331 | return $this; 332 | } 333 | 334 | /** 335 | * Use public url of file as string representation of file objects. 336 | */ 337 | public function __toString(): string 338 | { 339 | return $this->getPublicUrl(); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Image.php: -------------------------------------------------------------------------------- 1 | retrieveFileOrFolderObject($value); 49 | return ($file) ? new FalImage($file) : null; 50 | } 51 | } 52 | 53 | /** 54 | * Creates an image object based on data passed by array. There are numerous 55 | * array structures that create a valid image object: 56 | * 57 | * FAL file object: 58 | * [ "fileObject" => $myFileObject ] 59 | * 60 | * FAL file uid: 61 | * 62 | * [ "fileUid" => 123 ] 63 | * 64 | * FAL file reference uid: 65 | * 66 | * [ "fileReferenceUid" => 456 ] 67 | * 68 | * FAL file reference data: 69 | * 70 | * [ 71 | * "fileReference" => [ 72 | * "tableName" => "pages", 73 | * "fieldName" => "media", 74 | * "uid" => 123, 75 | * "counter" => 0 76 | * ] 77 | * ] 78 | * 79 | * Static file path: 80 | * 81 | * [ "file" => "EXT:my_extension/Resources/Public/Images/MyImage.png" ] 82 | * 83 | * [ 84 | * "resource" => [ 85 | * "extensionName" => "myExtension", 86 | * "path" => "Images/MyImage.png" 87 | * ] 88 | * ] 89 | * 90 | * [ 91 | * "resource" => [ 92 | * "extensionKey" => "my_extension", 93 | * "path" => "Images/MyImage.png" 94 | * ] 95 | * ] 96 | * 97 | * Static file uri: 98 | * 99 | * [ "file" => "https://example.com/MyImage.png" ] 100 | * 101 | * Placeholder image with dimensions: 102 | * 103 | * [ "width" => 1000, "height": 750 ] 104 | * 105 | * In addition, each variant can specify values for "alternative" as well as "title": 106 | * 107 | * [ 108 | * "fileReferenceUid": 456, 109 | * "title" => "My Image Title", 110 | * "alternative" => "My Alternative Text" 111 | * ] 112 | * 113 | * @throws FileReferenceNotFoundException|InvalidArgumentException 114 | */ 115 | public static function fromArray(array $value): ?self 116 | { 117 | try { 118 | /** @var Image */ 119 | $image = parent::fromArray($value); 120 | } catch (InvalidFileArrayException $e) { 121 | // Create a placeholder image with the specified dimensions 122 | if (isset($value['width']) && isset($value['height'])) { 123 | $image = static::fromDimensions( 124 | (int) $value['width'], 125 | (int) $value['height'] 126 | ); 127 | } else { 128 | throw $e; 129 | } 130 | } 131 | 132 | if (isset($value['title'])) { 133 | $image->setTitle($value['title']); 134 | } 135 | 136 | if (isset($value['description'])) { 137 | $image->setDescription($value['description']); 138 | } 139 | 140 | if (isset($value['properties'])) { 141 | $image->setProperties($value['properties']); 142 | } 143 | 144 | if (isset($value['alternative'])) { 145 | $image->setAlternative($value['alternative']); 146 | } 147 | 148 | if (isset($value['copyright'])) { 149 | $image->setCopyright($value['copyright']); 150 | } 151 | 152 | return $image; 153 | } 154 | 155 | /** 156 | * Creates a file object as a wrapper around an existing FAL object. 157 | */ 158 | public static function fromFileInterface(FileInterface $value): self 159 | { 160 | return new FalImage($value); 161 | } 162 | 163 | /** 164 | * Creates a placeholder image based on the provided image dimensions. 165 | */ 166 | public static function fromDimensions(int $width, int $height): self 167 | { 168 | return new PlaceholderImage($width, $height); 169 | } 170 | 171 | public function getAlternative(): ?string 172 | { 173 | return $this->alternative; 174 | } 175 | 176 | public function setAlternative(?string $alternative): self 177 | { 178 | $this->alternative = $alternative; 179 | return $this; 180 | } 181 | 182 | public function getCopyright(): ?string 183 | { 184 | return $this->copyright; 185 | } 186 | 187 | public function setCopyright(?string $copyright): self 188 | { 189 | $this->copyright = $copyright; 190 | return $this; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Labels.php: -------------------------------------------------------------------------------- 1 | overrideLabels = $overrideLabels; 45 | } 46 | 47 | /** 48 | * Generate object based on an array passed to the component. 49 | */ 50 | public static function fromArray(array $overrideLabels): self 51 | { 52 | return new self($overrideLabels); 53 | } 54 | 55 | /** 56 | * Generate object, even if component parameter is optional and omitted. 57 | */ 58 | public static function fromNull(): self 59 | { 60 | return new self; 61 | } 62 | 63 | /** 64 | * Receive component context to determine language file path. 65 | */ 66 | public function setComponentNamespace(string $componentNamespace): void 67 | { 68 | $this->componentNamespace = $componentNamespace; 69 | } 70 | 71 | /** 72 | * Receive current fluid rendering context. 73 | */ 74 | public function setRenderingContext(RenderingContextInterface $renderingContext): void 75 | { 76 | $this->renderingContext = $renderingContext; 77 | } 78 | 79 | /** 80 | * Check if language label is defined. 81 | */ 82 | public function offsetExists(mixed $identifier): bool 83 | { 84 | return $this->offsetGet($identifier) !== null; 85 | } 86 | 87 | /** 88 | * Return value of language label. 89 | */ 90 | public function offsetGet(mixed $identifier): ?string 91 | { 92 | if (isset($this->overrideLabels[$identifier])) { 93 | return $this->overrideLabels[$identifier]; 94 | } 95 | 96 | // Check if an alternative language was specified for the component 97 | $viewHelperVariableContainer = $this->renderingContext->getViewHelperVariableContainer(); 98 | if ($viewHelperVariableContainer->exists(ComponentRenderer::class, self::OVERRIDE_LANGUAGE_KEY)) { 99 | $languageKey = $viewHelperVariableContainer->get(ComponentRenderer::class, self::OVERRIDE_LANGUAGE_KEY); 100 | $alternativeLanguageKeys = $viewHelperVariableContainer->get(ComponentRenderer::class, self::OVERRIDE_LANGUAGE_ALTERNATIVES); 101 | 102 | $localeFactory = GeneralUtility::makeInstance(Locales::class); 103 | $locale = $localeFactory->createLocale($languageKey, $alternativeLanguageKeys); 104 | } 105 | 106 | return LocalizationUtility::translate( 107 | $this->generateLabelIdentifier($identifier), 108 | null, 109 | null, 110 | $locale ?? null 111 | ); 112 | } 113 | 114 | /** 115 | * Set an override language label. 116 | */ 117 | public function offsetSet(mixed $identifier, mixed $value): void 118 | { 119 | $this->overrideLabels[$identifier] = $value; 120 | } 121 | 122 | /** 123 | * Remove an override language label. 124 | */ 125 | public function offsetUnset(mixed $identifier): void 126 | { 127 | unset($this->overrideLabels[$identifier]); 128 | } 129 | 130 | protected function generateLabelIdentifier(string $identifier): string 131 | { 132 | if (!$this->labelsFile) { 133 | $this->labelsFile = $this->generateLabelFilePath(); 134 | } 135 | return sprintf('LLL:%s:%s', $this->labelsFile, $identifier); 136 | } 137 | 138 | protected function generateLabelFilePath(): string 139 | { 140 | $componentLoader = GeneralUtility::makeInstance(ComponentLoader::class); 141 | $componentFile = $componentLoader->findComponent($this->componentNamespace); 142 | $componentName = basename((string) $componentFile, '.html'); 143 | $componentPath = dirname((string) $componentFile); 144 | return $componentPath . DIRECTORY_SEPARATOR . $componentName . '.labels.xlf'; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Classes/Domain/Model/LanguageNavigationItem.php: -------------------------------------------------------------------------------- 1 | setChildren(new Navigation([])); 44 | if (!$link) { 45 | $link = new Typolink(''); 46 | } 47 | 48 | $this 49 | ->setTitle($title) 50 | ->setLink($link) 51 | ->setCurrent($current) 52 | ->setActive($active || $current) 53 | ->setAvailable($available) 54 | ->setLanguageId($languageId) 55 | ->setLocale($locale) 56 | ->setTwoLetterIsoCode($twoLetterIsoCode) 57 | ->setHreflang($hreflang) 58 | ->setDirection($direction) 59 | ->setFlag($flag) 60 | ->setData($data); 61 | } 62 | 63 | /** 64 | * Creates a navigation item object based on a TYPO3 language navigation item array. 65 | * 66 | * @param array $navigationItem respected properties that will become part of the data structure: 67 | * title, link, target, current, active, available, languageId, locale, 68 | * twoLetterIsoCode, hreflang, direction, flag, data 69 | */ 70 | public static function fromArray(array $navigationItem): self 71 | { 72 | // Convert link to the appropriate data structure 73 | if (isset($navigationItem['link'])) { 74 | if (!$navigationItem['link'] instanceof Typolink) { 75 | $navigationItem['link'] = new Typolink($navigationItem['link']); 76 | } 77 | if (isset($navigationItem['target'])) { 78 | $navigationItem['link']->setTarget($navigationItem['target']); 79 | } 80 | } 81 | 82 | return new static( 83 | $navigationItem['title'] ?? '', 84 | $navigationItem['link'] ?? null, 85 | (bool) ($navigationItem['current'] ?? false), 86 | (bool) ($navigationItem['active'] ?? $navigationItem['current'] ?? false), 87 | (bool) ($navigationItem['available'] ?? false), 88 | $navigationItem['languageId'] ?? 0, 89 | $navigationItem['locale'] ?? '', 90 | $navigationItem['twoLetterIsoCode'] ?? '', 91 | $navigationItem['hreflang'] ?? '', 92 | $navigationItem['direction'] ?? '', 93 | $navigationItem['flag'] ?? '', 94 | $navigationItem['data'] ?? [] 95 | ); 96 | } 97 | 98 | public function getAvailable(): bool 99 | { 100 | return $this->available; 101 | } 102 | 103 | public function setAvailable(bool $available): static 104 | { 105 | $this->available = $available; 106 | return $this; 107 | } 108 | 109 | public function getLanguageId(): int 110 | { 111 | return $this->languageId; 112 | } 113 | 114 | public function setLanguageId(int $languageId): static 115 | { 116 | $this->languageId = $languageId; 117 | return $this; 118 | } 119 | 120 | public function getLocale(): string 121 | { 122 | return $this->locale; 123 | } 124 | 125 | public function setLocale(string $locale): static 126 | { 127 | $this->locale = $locale; 128 | return $this; 129 | } 130 | 131 | public function getTwoLetterIsoCode(): string 132 | { 133 | return $this->twoLetterIsoCode; 134 | } 135 | 136 | public function setTwoLetterIsoCode(string $twoLetterIsoCode): static 137 | { 138 | $this->twoLetterIsoCode = $twoLetterIsoCode; 139 | return $this; 140 | } 141 | 142 | public function getHreflang(): string 143 | { 144 | return $this->hreflang; 145 | } 146 | 147 | public function setHreflang(string $hreflang): static 148 | { 149 | $this->hreflang = $hreflang; 150 | return $this; 151 | } 152 | 153 | public function getDirection(): string 154 | { 155 | return $this->direction; 156 | } 157 | 158 | public function setDirection(string $direction): static 159 | { 160 | $this->direction = $direction; 161 | return $this; 162 | } 163 | 164 | public function getFlag(): string 165 | { 166 | return $this->flag; 167 | } 168 | 169 | public function setFlag(string $flag): static 170 | { 171 | $this->flag = $flag; 172 | return $this; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Link.php: -------------------------------------------------------------------------------- 1 | setUri($uri); 65 | } 66 | 67 | /** 68 | * Creates a link data structure from an URI. 69 | * 70 | * @param string $uri 71 | * 72 | * @return self 73 | */ 74 | public static function fromString(string $uri): ?self 75 | { 76 | if ($uri === '') { 77 | return null; 78 | } 79 | 80 | return new static($uri); 81 | } 82 | 83 | public function setUri(string $uri): static 84 | { 85 | $this->uri = $uri; 86 | 87 | // Extract URI fragments 88 | $parsed = parse_url($uri); 89 | $this->scheme = $parsed['scheme'] ?? null; 90 | $this->host = $parsed['host'] ?? null; 91 | $this->port = $parsed['port'] ?? null; 92 | $this->user = $parsed['user'] ?? null; 93 | $this->pass = $parsed['pass'] ?? null; 94 | $this->path = $parsed['path'] ?? null; 95 | $this->query = $parsed['query'] ?? null; 96 | $this->fragment = $parsed['fragment'] ?? null; 97 | 98 | return $this; 99 | } 100 | 101 | public function getUri(): string 102 | { 103 | return $this->uri; 104 | } 105 | 106 | public function getScheme(): ?string 107 | { 108 | return $this->scheme; 109 | } 110 | 111 | public function getHost(): ?string 112 | { 113 | return $this->host; 114 | } 115 | 116 | public function getPort(): ?int 117 | { 118 | return $this->port; 119 | } 120 | 121 | public function getUser(): ?string 122 | { 123 | return $this->user; 124 | } 125 | 126 | public function getPass(): ?string 127 | { 128 | return $this->pass; 129 | } 130 | 131 | public function getPath(): ?string 132 | { 133 | return $this->path; 134 | } 135 | 136 | public function getQuery(): ?string 137 | { 138 | return $this->query; 139 | } 140 | 141 | public function getFragment(): ?string 142 | { 143 | return $this->fragment; 144 | } 145 | 146 | /** 147 | * Use URI as string representation of the object. 148 | * 149 | * @return string 150 | */ 151 | public function __toString(): string 152 | { 153 | return $this->getUri(); 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Classes/Domain/Model/LocalFile.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 51 | } 52 | 53 | public function getFilePath(): string 54 | { 55 | return $this->filePath; 56 | } 57 | 58 | public function getPublicUrl(): string 59 | { 60 | return PathUtility::getAbsoluteWebPath($this->filePath); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Classes/Domain/Model/LocalImage.php: -------------------------------------------------------------------------------- 1 | filePath = $filePath; 65 | } 66 | 67 | public function getFilePath(): string 68 | { 69 | return $this->filePath; 70 | } 71 | 72 | public function getPublicUrl(): string 73 | { 74 | return PathUtility::getAbsoluteWebPath($this->filePath); 75 | } 76 | 77 | public function getHeight(): int 78 | { 79 | if (!isset($this->height)) { 80 | $this->getImageDimensions(); 81 | } 82 | return $this->height; 83 | } 84 | 85 | public function getWidth(): int 86 | { 87 | if (!isset($this->height)) { 88 | $this->getImageDimensions(); 89 | } 90 | return $this->width; 91 | } 92 | 93 | protected function getImageDimensions(): void 94 | { 95 | $graphicalFunctions = GeneralUtility::makeInstance(GraphicalFunctions::class); 96 | $imageDimensions = $graphicalFunctions->getImageDimensions($this->getFilePath()); 97 | $this->width = (int) $imageDimensions[0]; 98 | $this->height = (int) $imageDimensions[1]; 99 | } 100 | 101 | public function process(int $width, int $height, ?string $format, Area $cropArea): ProcessableImage 102 | { 103 | $imageService = GeneralUtility::makeInstance(ImageService::class); 104 | $file = $imageService->getImage($this->getFilePath(), null, false); 105 | $processedImage = $imageService->applyProcessingInstructions($file, [ 106 | 'width' => $width, 107 | 'height' => $height, 108 | 'fileExtension' => $format, 109 | 'crop' => ($cropArea->isEmpty()) ? null : $cropArea->makeAbsoluteBasedOnFile($file) 110 | ]); 111 | return new FalImage($processedImage); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Navigation.php: -------------------------------------------------------------------------------- 1 | setItems($items); 30 | } 31 | 32 | /** 33 | * Initializes a navigation object from a TYPO3 navigation array. 34 | */ 35 | public static function fromArray(array $items): self 36 | { 37 | return new static($items); 38 | } 39 | 40 | public function getItems(): array 41 | { 42 | return $this->items; 43 | } 44 | 45 | public function setItems(array $items): self 46 | { 47 | // Make sure that navigation items use the appropriate data structure 48 | $this->items = array_filter(array_map([$this, 'sanitizeNavigationItem'], $items)); 49 | return $this; 50 | } 51 | 52 | public function count(): int 53 | { 54 | return count($this->items); 55 | } 56 | 57 | #[ReturnTypeWillChange] 58 | public function current() 59 | { 60 | return current($this->items); 61 | } 62 | 63 | #[ReturnTypeWillChange] 64 | public function key() 65 | { 66 | return key($this->items); 67 | } 68 | 69 | public function next(): void 70 | { 71 | next($this->items); 72 | } 73 | 74 | public function rewind(): void 75 | { 76 | reset($this->items); 77 | } 78 | 79 | public function valid(): bool 80 | { 81 | return $this->current() !== false; 82 | } 83 | 84 | /** 85 | * Makes sure that the provided item is a valid data structure. 86 | */ 87 | protected function sanitizeNavigationItem(mixed $item): ?NavigationItem 88 | { 89 | if ($item instanceof NavigationItem) { 90 | return $item; 91 | } 92 | 93 | if (is_array($item)) { 94 | if (isset($item['languageId'])) { 95 | return LanguageNavigationItem::fromArray($item); 96 | } else { 97 | return NavigationItem::fromArray($item); 98 | } 99 | } 100 | 101 | return null; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Classes/Domain/Model/NavigationItem.php: -------------------------------------------------------------------------------- 1 | setTitle($title) 44 | ->setLink($link) 45 | ->setCurrent($current) 46 | ->setActive($active || $current) 47 | ->setSpacer($spacer) 48 | ->setChildren($children) 49 | ->setData($data); 50 | } 51 | 52 | /** 53 | * Creates a navigation item object based on a TYPO3 navigation item array. 54 | * 55 | * @param array $navigationItem respected properties that will become part of the data structure: 56 | * title, link, target, current, active, spacer, children, data 57 | */ 58 | public static function fromArray(array $navigationItem): self 59 | { 60 | // Convert link and sub navigation to the appropriate data structure 61 | if (isset($navigationItem['link'])) { 62 | if (!$navigationItem['link'] instanceof Typolink) { 63 | $navigationItem['link'] = new Typolink($navigationItem['link']); 64 | } 65 | if (isset($navigationItem['target'])) { 66 | $navigationItem['link']->setTarget($navigationItem['target']); 67 | } 68 | } 69 | if (isset($navigationItem['children']) && !$navigationItem['children'] instanceof Navigation) { 70 | $navigationItem['children'] = new Navigation($navigationItem['children']); 71 | } 72 | 73 | return new static( 74 | $navigationItem['title'] ?? '', 75 | $navigationItem['link'] ?? null, 76 | (bool) ($navigationItem['current'] ?? false), 77 | (bool) ($navigationItem['active'] ?? $navigationItem['current'] ?? false), 78 | (bool) ($navigationItem['spacer'] ?? false), 79 | $navigationItem['children'] ?? null, 80 | $navigationItem['data'] ?? [] 81 | ); 82 | } 83 | 84 | public function getTitle(): string 85 | { 86 | return $this->title; 87 | } 88 | 89 | public function setTitle(string $title): static 90 | { 91 | $this->title = $title; 92 | return $this; 93 | } 94 | 95 | public function getLink(): ?Typolink 96 | { 97 | return $this->link; 98 | } 99 | 100 | public function setLink(Typolink $link): static 101 | { 102 | $this->link = $link; 103 | return $this; 104 | } 105 | 106 | public function getTarget(): string 107 | { 108 | return $this->link->getTarget(); 109 | } 110 | 111 | public function setTarget(string $target): static 112 | { 113 | $this->link->setTarget($target); 114 | return $this; 115 | } 116 | 117 | public function getCurrent(): bool 118 | { 119 | return $this->current; 120 | } 121 | 122 | public function setCurrent(bool $current): static 123 | { 124 | $this->current = $current; 125 | return $this; 126 | } 127 | 128 | public function getActive(): bool 129 | { 130 | return $this->active; 131 | } 132 | 133 | public function setActive(bool $active): static 134 | { 135 | $this->active = $active; 136 | return $this; 137 | } 138 | 139 | public function getSpacer(): bool 140 | { 141 | return $this->spacer; 142 | } 143 | 144 | public function setSpacer(bool $spacer): static 145 | { 146 | $this->spacer = $spacer; 147 | return $this; 148 | } 149 | 150 | public function getChildren(): Navigation 151 | { 152 | return $this->children; 153 | } 154 | 155 | public function hasChildren(): bool 156 | { 157 | return count($this->children) > 0; 158 | } 159 | 160 | public function setChildren(Navigation $children): static 161 | { 162 | $this->children = $children; 163 | return $this; 164 | } 165 | 166 | public function getData(): array 167 | { 168 | return $this->data; 169 | } 170 | 171 | public function setData(array $data): static 172 | { 173 | $this->data = $data; 174 | return $this; 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /Classes/Domain/Model/PlaceholderImage.php: -------------------------------------------------------------------------------- 1 | width = $width; 42 | $this->height = $height; 43 | $this->format = $format ?? $this->format; 44 | } 45 | 46 | public function getWidth(): int 47 | { 48 | return $this->width; 49 | } 50 | 51 | public function getHeight(): int 52 | { 53 | return $this->height; 54 | } 55 | 56 | public function getFormat(): string 57 | { 58 | return $this->format; 59 | } 60 | 61 | public function getPublicUrl(): string 62 | { 63 | return GeneralUtility::makeInstance(PlaceholderImageService::class)->generate( 64 | $this->width, 65 | $this->height, 66 | $this->format, 67 | ); 68 | } 69 | 70 | public function process(int $width, int $height, ?string $format, Area $cropArea): ProcessableImage 71 | { 72 | return new PlaceholderImage( 73 | (int) round($cropArea->getWidth() * $width), 74 | (int) round($cropArea->getHeight() * $height), 75 | $format ?: $this->format 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RemoteFile.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 37 | } 38 | 39 | public function getPublicUrl(): string 40 | { 41 | return $this->uri; 42 | } 43 | 44 | /** 45 | * Checks if the provided uri is a valid remote uri. 46 | */ 47 | protected static function isRemoteUri(string $uri): bool 48 | { 49 | $scheme = parse_url($uri, PHP_URL_SCHEME); 50 | return ($scheme && in_array(strtolower($scheme), ['http', 'https'])); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RemoteImage.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 37 | } 38 | 39 | public function getPublicUrl(): string 40 | { 41 | return $this->uri; 42 | } 43 | 44 | /** 45 | * Checks if the provided uri is a valid remote uri. 46 | */ 47 | protected static function isRemoteUri(string $uri): bool 48 | { 49 | $scheme = parse_url($uri, PHP_URL_SCHEME); 50 | return ($scheme && in_array(strtolower($scheme), ['http', 'https'])); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Classes/Domain/Model/RequiredSlotPlaceholder.php: -------------------------------------------------------------------------------- 1 | html = $html; 21 | } 22 | 23 | public static function fromString(string $html): self 24 | { 25 | return new self($html); 26 | } 27 | 28 | public static function fromClosure(Closure $closure): self 29 | { 30 | return new self($closure()); 31 | } 32 | 33 | public function count(): int 34 | { 35 | return strlen((string) $this->html); 36 | } 37 | 38 | public function __toString(): string 39 | { 40 | return $this->html; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Traits/FalFileTrait.php: -------------------------------------------------------------------------------- 1 | file; 31 | } 32 | 33 | public function getTitle(): ?string 34 | { 35 | return parent::getTitle() ?? $this->file->getProperty('title'); 36 | } 37 | 38 | public function getDescription(): ?string 39 | { 40 | return parent::getDescription() ?? $this->file->getProperty('description'); 41 | } 42 | 43 | public function getProperties(): ?array 44 | { 45 | if (method_exists($this->file, 'getProperties')) { 46 | return $this->file->getProperties(); 47 | } 48 | return null; 49 | } 50 | 51 | public function getPublicUrl(): string 52 | { 53 | return $this->file->getPublicUrl() ?? ''; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Classes/Domain/Model/Typolink.php: -------------------------------------------------------------------------------- 1 | decode($typolink); 55 | 56 | // Analyze structure of provided TYPO3 uri 57 | $linkService = GeneralUtility::makeInstance(LinkService::class); 58 | $uriStructure = $linkService->resolve($typolinkConfiguration['url']); 59 | 60 | // Generate general purpose uri (https://) from TYPO3 uri (t3://) 61 | // Could also be a mailto or tel uri 62 | $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 63 | $uri = $cObj->typoLink_URL([ 64 | 'parameter' => $typolinkConfiguration['url'], 65 | 'additionalParams' => $typolinkConfiguration['additionalParams'], 66 | ]); 67 | 68 | $this 69 | ->setUri($uri) 70 | ->setOriginalLink($uriStructure) 71 | ->setTarget($typolinkConfiguration['target']) 72 | ->setClass($typolinkConfiguration['class']) 73 | ->setTitle($typolinkConfiguration['title']); 74 | } 75 | 76 | /** 77 | * Creates a Typolink data structure from a page uid. 78 | */ 79 | public static function fromInteger(int $pageUid): self 80 | { 81 | return new static((string) $pageUid); 82 | } 83 | 84 | /** 85 | * Creates a Typolink data structure from an array. 86 | * 87 | * Possible array keys are: 88 | * - uri (required) 89 | * - target 90 | * - class 91 | * - title 92 | * 93 | * @throws InvalidArgumentException 94 | */ 95 | public static function fromArray(array $typolinkData): self 96 | { 97 | if (!isset($typolinkData['uri'])) { 98 | throw new InvalidArgumentException( 99 | 'At least an URI has to be provided to be able to create a Typolink object.', 100 | 1564488090 101 | ); 102 | } 103 | 104 | $instance = new static((string) $typolinkData['uri']); 105 | 106 | if (isset($typolinkData['target'])) { 107 | $instance->setTarget((string) $typolinkData['target']); 108 | } 109 | 110 | if (isset($typolinkData['class'])) { 111 | $instance->setClass((string) $typolinkData['class']); 112 | } 113 | 114 | if (isset($typolinkData['title'])) { 115 | $instance->setTitle((string) $typolinkData['title']); 116 | } 117 | 118 | return $instance; 119 | } 120 | 121 | public static function fromTypolinkParameter(TypolinkParameter $parameter): self 122 | { 123 | // Analyze structure of provided TYPO3 uri 124 | $linkService = GeneralUtility::makeInstance(LinkService::class); 125 | $uriStructure = $linkService->resolve($parameter->url); 126 | 127 | // Generate general purpose uri (https://) from TYPO3 uri (t3://) 128 | // Could also be a mailto or tel uri 129 | $cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); 130 | $uri = $cObj->typoLink_URL([ 131 | 'parameter' => $parameter->url, 132 | 'additionalParams' => $parameter->additionalParams, 133 | ]); 134 | 135 | // TODO constructor code should be avoided in the future, but this would require changes 136 | // to the current constructor method signature (such as making the string nullable), 137 | // which would be a breaking change. 138 | return (new static('')) 139 | ->setUri($uri) 140 | ->setOriginalLink($uriStructure) 141 | ->setTarget($parameter->target) 142 | ->setClass($parameter->class) 143 | ->setTitle($parameter->title); 144 | } 145 | 146 | public function setOriginalLink(array $originalLink): self 147 | { 148 | $this->originalLink = $originalLink; 149 | return $this; 150 | } 151 | 152 | public function getOriginalLink(): array 153 | { 154 | return $this->originalLink; 155 | } 156 | 157 | public function setTarget(string $target): self 158 | { 159 | $this->target = $target; 160 | return $this; 161 | } 162 | 163 | public function getTarget(): string 164 | { 165 | return $this->target; 166 | } 167 | 168 | public function setClass(string $class): self 169 | { 170 | $this->class = $class; 171 | return $this; 172 | } 173 | 174 | public function getClass(): string 175 | { 176 | return $this->class; 177 | } 178 | 179 | public function setTitle(string $title): self 180 | { 181 | $this->title = $title; 182 | return $this; 183 | } 184 | 185 | public function getTitle(): string 186 | { 187 | return $this->title; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Classes/Exception/FileReferenceNotFoundException.php: -------------------------------------------------------------------------------- 1 | create()` instead 27 | */ 28 | public function __construct(ContainerInterface $container, array $namespaces) 29 | { 30 | $this->container = $container; 31 | $this->namespaces = $namespaces; 32 | if (($GLOBALS['TYPO3_REQUEST'] ?? null) instanceof ServerRequestInterface 33 | && ApplicationType::fromRequest($GLOBALS['TYPO3_REQUEST'])->isFrontend() 34 | && $this->getBackendUser() instanceof BackendUserAuthentication 35 | ) { 36 | if ($this->getBackendUser()->uc['AdminPanel']['preview_showFluidDebug'] ?? false) { 37 | $this->namespaces['f'][] = 'TYPO3\\CMS\\Fluid\\ViewHelpers\\Debug'; 38 | } 39 | } 40 | } 41 | 42 | /** 43 | * Uses Symfony dependency injection to inject ComponentRenderer into 44 | * Fluid viewhelper processing. 45 | */ 46 | public function createViewHelperInstanceFromClassName($viewHelperClassName): ViewHelperInterface 47 | { 48 | if ($this->container instanceof FailsafeContainer) { 49 | // The install tool creates VH instances using makeInstance to not rely on symfony DI here, 50 | // otherwise we'd have to have all install-tool used ones in ServiceProvider.php. However, 51 | // none of the install tool used VH's use injection. 52 | /** @var ViewHelperInterface $viewHelperInstance */ 53 | $viewHelperInstance = GeneralUtility::makeInstance($viewHelperClassName); 54 | return $viewHelperInstance; 55 | } 56 | 57 | if (class_exists($viewHelperClassName)) { 58 | if ($this->container->has($viewHelperClassName)) { 59 | /** @var ViewHelperInterface $viewHelperInstance */ 60 | $viewHelperInstance = $this->container->get($viewHelperClassName); 61 | } else { 62 | /** @var ViewHelperInterface $viewHelperInstance */ 63 | $viewHelperInstance = new $viewHelperClassName; 64 | } 65 | return $viewHelperInstance; 66 | } else { 67 | // Redirect all components to special ViewHelper ComponentRenderer 68 | $componentRenderer = $this->container->get(ComponentRenderer::class); 69 | $componentRenderer->setComponentNamespace($viewHelperClassName); 70 | return $componentRenderer; 71 | } 72 | } 73 | 74 | /** 75 | * Resolves a ViewHelper class name by namespace alias and 76 | * Fluid-format identity, e.g. "f" and "format.htmlspecialchars". 77 | * 78 | * Looks in all PHP namespaces which have been added for the 79 | * provided alias, starting in the last added PHP namespace. If 80 | * a ViewHelper class exists in multiple PHP namespaces Fluid 81 | * will detect and use whichever one was added last. 82 | * 83 | * If no ViewHelper class can be detected in any of the added 84 | * PHP namespaces a Fluid Parser Exception is thrown. 85 | * 86 | * @throws ParserException 87 | */ 88 | public function resolveViewHelperClassName($namespaceIdentifier, $methodIdentifier): string 89 | { 90 | if (!isset($this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier])) { 91 | $resolvedViewHelperClassName = $this->resolveViewHelperName($namespaceIdentifier, $methodIdentifier); 92 | $actualViewHelperClassName = $this->generateViewHelperClassName($resolvedViewHelperClassName); 93 | if (false === class_exists($actualViewHelperClassName) || $actualViewHelperClassName == '') { 94 | $resolvedViewHelperClassName = $this->resolveComponentName($namespaceIdentifier, $methodIdentifier); 95 | $actualViewHelperClassName = $this->generateViewHelperClassName($resolvedViewHelperClassName); 96 | 97 | $componentLoader = $this->getComponentLoader(); 98 | $componentFile = $componentLoader->findComponent($actualViewHelperClassName); 99 | if (!$componentFile) { 100 | throw new ParserException(sprintf( 101 | 'The ViewHelper "<%s:%s>" could not be resolved.' . chr(10) . 102 | 'Based on your spelling, the system would load the class "%s", ' 103 | . 'however this class does not exist.', 104 | $namespaceIdentifier, 105 | $methodIdentifier, 106 | $resolvedViewHelperClassName 107 | ), 1527779401); 108 | } 109 | } 110 | $this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier] = $actualViewHelperClassName; 111 | } 112 | return $this->resolvedViewHelperClassNames[$namespaceIdentifier][$methodIdentifier]; 113 | } 114 | 115 | /** 116 | * Resolve a viewhelper name. 117 | * 118 | * @param string $namespaceIdentifier namespace identifier for the view helper 119 | * @param string $methodIdentifier Method identifier, might be hierarchical like "link.url" 120 | * 121 | * @return string The fully qualified class name of the viewhelper 122 | */ 123 | protected function resolveComponentName(string $namespaceIdentifier, string $methodIdentifier): string 124 | { 125 | $explodedViewHelperName = explode('.', $methodIdentifier); 126 | if (count($explodedViewHelperName) > 1) { 127 | $className = implode('\\', array_map('ucfirst', $explodedViewHelperName)); 128 | } else { 129 | $className = ucfirst($explodedViewHelperName[0]); 130 | } 131 | 132 | $componentLoader = $this->getComponentLoader(); 133 | $namespaces = (array) $this->getNamespaces()[$namespaceIdentifier]; 134 | 135 | do { 136 | $name = rtrim((string) array_pop($namespaces), '\\') . '\\' . $className; 137 | } while (!$componentLoader->findComponent($name) && count($namespaces)); 138 | 139 | return $name; 140 | } 141 | 142 | /** 143 | * Generates a valid PHP class name from the resolved viewhelper class. 144 | */ 145 | protected function generateViewHelperClassName(string $resolvedViewHelperClassName): string 146 | { 147 | return implode('\\', array_map('ucfirst', explode('.', $resolvedViewHelperClassName))); 148 | } 149 | 150 | protected function getComponentLoader(): ComponentLoader 151 | { 152 | return $this->container->get(ComponentLoader::class); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /Classes/Fluid/ViewHelper/ViewHelperResolverFactory.php: -------------------------------------------------------------------------------- 1 | container, $namespaces); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Classes/Interfaces/ComponentAware.php: -------------------------------------------------------------------------------- 1 | 15 | * 16 | * 17 | * 18 | * 19 | */ 20 | interface EscapedParameter 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /Classes/Interfaces/ImageWithCropVariants.php: -------------------------------------------------------------------------------- 1 | generateBitmap($width, $height, $format, $text); 30 | } 31 | 32 | if (GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() < 13) { 33 | $view = new StandaloneView(); 34 | $view->setTemplatePathAndFilename('EXT:fluid_components/Resources/Private/Templates/Placeholder.svg'); 35 | } else { 36 | $view = $this->viewFactory->create(new ViewFactoryData( 37 | templatePathAndFilename: 'EXT:fluid_components/Resources/Private/Templates/Placeholder.svg', 38 | )); 39 | } 40 | 41 | $view->assignMultiple([ 42 | 'width' => $width, 43 | 'height' => $height, 44 | 'backgroundColor' => self::BACKGROUND_COLOR, 45 | 'color' => self::COLOR, 46 | 'text' => $text, 47 | ]); 48 | return 'data:image/svg+xml;base64,' . base64_encode($view->render()); 49 | } 50 | 51 | private function generateBitmap(int $width, int $height, string $format, string $text): string 52 | { 53 | $configuration = [ 54 | 'XY' => implode(',', [$width, $height]), 55 | 'backColor' => self::BACKGROUND_COLOR, 56 | 'format' => $format, 57 | '10' => 'TEXT', 58 | '10.' => [ 59 | 'text' => $text, 60 | 'fontColor' => self::COLOR, 61 | 'fontSize' => round($width / 9), 62 | 'align' => 'center', 63 | 'offset' => implode(',', [0, $height / 2 + ($width / 9 / 3)]), 64 | ], 65 | ]; 66 | 67 | $strokes = [ 68 | [0 ,0 , self::STROKE_WIDTH, self::STROKE_HEIGHT], 69 | [0 ,0 , self::STROKE_HEIGHT, self::STROKE_WIDTH], 70 | [($width - self::STROKE_WIDTH), 0, $width , self::STROKE_HEIGHT], 71 | [($width - self::STROKE_HEIGHT), 0, $width, self::STROKE_WIDTH], 72 | [0, ($height - self::STROKE_HEIGHT), self::STROKE_WIDTH, $height], 73 | [0, ($height - self::STROKE_WIDTH), self::STROKE_HEIGHT, $height], 74 | [($width - self::STROKE_WIDTH), ($height - self::STROKE_HEIGHT), $width, $height], 75 | [($width - self::STROKE_HEIGHT), ($height - self::STROKE_WIDTH), $width, $height], 76 | ]; 77 | 78 | foreach ($strokes as $key => $dimensions) { 79 | $configuration[10 * ($key + 2)] = 'BOX'; 80 | $configuration[10 * ($key + 2) . '.'] = [ 81 | 'dimensions' => implode(',', $dimensions), 82 | 'color' => self::COLOR, 83 | ]; 84 | } 85 | 86 | $this->gifBuilder->start($configuration, []); 87 | 88 | if (GeneralUtility::makeInstance(Typo3Version::class)->getMajorVersion() < 13 && is_string($imagePath = $this->gifBuilder->gifBuild())) { 89 | return $imagePath; 90 | } 91 | return (string) $this->gifBuilder->gifBuild()->getPublicUrl(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Classes/Service/XsdGenerator.php: -------------------------------------------------------------------------------- 1 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | '; 33 | foreach ($arguments as $argumentName => $argumentDefinition) { 34 | $requiredTag = $argumentDefinition->isRequired() ? ' use="required"' : ''; 35 | try { 36 | $defaultTag = (string)$argumentDefinition->getDefaultValue() !== '' ? ' default="' . $argumentDefinition->getDefaultValue() . '"' : ''; 37 | } catch (Exception) { 38 | $defaultTag = ''; 39 | } 40 | $xsd .= "\n" . ' 41 | 42 | getDescription() . ']]> 43 | 44 | '; 45 | } 46 | $xsd .= ' 47 | '; 48 | return $xsd; 49 | } 50 | 51 | protected function convertNameSpaceToPathSegment($namespace) 52 | { 53 | return str_replace('\\', '/', $namespace); 54 | } 55 | 56 | protected function getTargetXMLNameSpace($namespace) 57 | { 58 | return 'http://typo3.org/ns/' . $this->convertNameSpaceToPathSegment($namespace); 59 | } 60 | 61 | protected function generateXsdForNamespace($namespace, $components) 62 | { 63 | $xsd = '' . "\n"; 65 | foreach ($components as $componentName => $componentFile) { 66 | $componentRenderer = GeneralUtility::makeInstance(ComponentRenderer::class); 67 | $componentRenderer->setComponentNamespace($componentName); 68 | $arguments = $componentRenderer->prepareArguments(); 69 | $componentNameWithoutNameSpace = $this->getTagName($namespace, $componentName); 70 | $xsd .= $this->generateXsdForComponent($componentNameWithoutNameSpace, $arguments); 71 | } 72 | $xsd .= '' . "\n"; 73 | return $xsd; 74 | } 75 | 76 | private function getTagName($nameSpace, $componentName) 77 | { 78 | $tagName = ''; 79 | if (str_starts_with((string) $componentName, (string) $nameSpace)) { 80 | $tagNameWithoutNameSpace = substr((string) $componentName, strlen((string) $nameSpace) + 1); 81 | $tagName = lcfirst(str_replace('\\', '.', $tagNameWithoutNameSpace)); 82 | } 83 | return $tagName; 84 | } 85 | 86 | /** 87 | * returns only the upper chars of a given string. 88 | */ 89 | private function strUpperChars(string $string): string 90 | { 91 | $output = ''; 92 | $strLength = strlen((string) $string); 93 | for ($i = 0; $i < $strLength; $i++) { 94 | if (ctype_upper((string) $string[$i])) { 95 | $output .= $string[$i]; 96 | } 97 | } 98 | return $output; 99 | } 100 | 101 | /** 102 | * returns default prefix for a namespace if defined 103 | * otherwise it builds a prefix from the extension name part of the namespace. 104 | */ 105 | protected function getDefaultPrefixForNamespace(string $namespace): int|string 106 | { 107 | $defaultNamespaceDefinitions = $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']; 108 | foreach ($defaultNamespaceDefinitions as $prefix => $registeredNameSpaces) { 109 | foreach ($registeredNameSpaces as $registeredNameSpace) { 110 | if ($registeredNameSpace === $namespace) { 111 | return $prefix; 112 | } 113 | } 114 | } 115 | // no registered default prefix found, so build one from extension name part of the namespace 116 | // f.e. Vendor\MyExtension\Components => me (converting only the upper chars from 'MyExtension' to lower case 117 | $nameSpaceParts = explode('\\', (string) $namespace); 118 | $lastFragment = $nameSpaceParts[1]; 119 | return strtolower($this->strUpperChars($lastFragment)); 120 | } 121 | 122 | /** 123 | * generate xsd file for each component namespace. 124 | * 125 | * @return array Array of generated XML target namespaces 126 | */ 127 | public function generateXsd(string $path, ?string $namespace = null): array 128 | { 129 | $generatedNameSpaces = []; 130 | $namespaces = $this->componentLoader->getNamespaces(); 131 | foreach ($namespaces as $registeredNamespace => $registeredNamepacePath) { 132 | if ($namespace === null || $registeredNamespace === $namespace) { 133 | $components = $this->componentLoader->findComponentsInNamespace($registeredNamespace); 134 | $filePath = rtrim((string) $path, DIRECTORY_SEPARATOR) . 135 | DIRECTORY_SEPARATOR . 136 | $this->getFileNameForNamespace($registeredNamespace); 137 | file_put_contents($filePath, $this->generateXsdForNamespace($registeredNamespace, $components)); 138 | $generatedNameSpaces[$this->getDefaultPrefixForNamespace($registeredNamespace)][] = $this->getTargetXMLNameSpace($registeredNamespace); 139 | } 140 | } 141 | return $generatedNameSpaces; 142 | } 143 | 144 | /** 145 | * returns a default filename for a given namespace. 146 | */ 147 | protected function getFileNameForNamespace(string $namespace): string 148 | { 149 | return str_replace('\\', '_', $namespace) . '.xsd'; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Classes/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ static::class, 'getViewHelperResolverFactory' ], 26 | ]; 27 | } 28 | 29 | public static function getViewHelperResolverFactory(ContainerInterface $container): Fluid\ViewHelper\ViewHelperResolverFactory 30 | { 31 | return self::new($container, Fluid\ViewHelper\ViewHelperResolverFactory::class, [ 32 | $container, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Classes/Utility/ComponentArgumentConverter.php: -------------------------------------------------------------------------------- 1 | [ 32 | ConstructibleFromString::class, 33 | 'fromString', 34 | ], 35 | 'integer' => [ 36 | ConstructibleFromInteger::class, 37 | 'fromInteger', 38 | ], 39 | 'array' => [ 40 | ConstructibleFromArray::class, 41 | 'fromArray', 42 | ], 43 | 'NULL' => [ 44 | ConstructibleFromNull::class, 45 | 'fromNull', 46 | ], 47 | Closure::class => [ 48 | ConstructibleFromClosure::class, 49 | 'fromClosure', 50 | ], 51 | 'DateTime' => [ 52 | ConstructibleFromDateTime::class, 53 | 'fromDateTime', 54 | ], 55 | 'DateTimeImmutable' => [ 56 | ConstructibleFromDateTimeImmutable::class, 57 | 'fromDateTimeImmutable', 58 | ], 59 | FileInterface::class => [ 60 | ConstructibleFromFileInterface::class, 61 | 'fromFileInterface', 62 | ], 63 | FileReference::class => [ 64 | ConstructibleFromExtbaseFile::class, 65 | 'fromExtbaseFile', 66 | ], 67 | AbstractFile::class => [ 68 | ConstructibleFromFileInterface::class, 69 | 'fromFileInterface', 70 | ], 71 | TypolinkParameter::class => [ 72 | ConstructibleFromTypolinkParameter::class, 73 | 'fromTypolinkParameter', 74 | ], 75 | ]; 76 | 77 | /** 78 | * Registered argument type aliases 79 | * [alias => full php class name]. 80 | */ 81 | protected array $typeAliases = []; 82 | 83 | /** 84 | * Runtime cache to speed up conversion checks. 85 | */ 86 | protected array $conversionCache = []; 87 | 88 | public function __construct() 89 | { 90 | if (isset($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['typeAliases']) 91 | && is_array($GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['typeAliases']) 92 | ) { 93 | $this->typeAliases =& $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['typeAliases']; 94 | } 95 | } 96 | 97 | /** 98 | * Adds an interface to specify argument type conversion to list. 99 | */ 100 | public function addConversionInterface(string $fromType, string $interface, string $constructor): self 101 | { 102 | $this->conversionInterfaces[$fromType] = [$interface, $constructor]; 103 | $this->conversionCache = []; 104 | return $this; 105 | } 106 | 107 | /** 108 | * Removes an interface that specifies argument type conversion from list. 109 | */ 110 | public function removeConversionInterface(string $fromType): self 111 | { 112 | unset($this->conversionInterfaces[$fromType]); 113 | $this->conversionCache = []; 114 | return $this; 115 | } 116 | 117 | /** 118 | * Adds an alias for an argument type. 119 | */ 120 | public function addTypeAlias(string $alias, string $type): self 121 | { 122 | $this->typeAliases[$alias] = $type; 123 | return $this; 124 | } 125 | 126 | /** 127 | * Removes an alias for an argument type. 128 | */ 129 | public function removeTypeAlias(string $alias): self 130 | { 131 | unset($this->typeAliases[$alias]); 132 | return $this; 133 | } 134 | 135 | /** 136 | * Replaces potential argument type alias with real php class name. 137 | * 138 | * @param string $type e. g. MyAlias 139 | * 140 | * @return string e. g. Vendor\MyExtension\MyRealClass 141 | */ 142 | public function resolveTypeAlias(string $type): string 143 | { 144 | if ($this->isCollectionType($type)) { 145 | $subtype = $this->extractCollectionItemType($type); 146 | return $this->resolveTypeAlias($subtype) . '[]'; 147 | } else { 148 | return $this->typeAliases[$type] ?? $type; 149 | } 150 | } 151 | 152 | /** 153 | * Checks if a given variable type can be converted to another 154 | * data type by using alternative constructors in $this->conversionInterfaces. 155 | * 156 | * @return array information about conversion or empty array 157 | */ 158 | public function canTypeBeConvertedToType(string $givenType, string $toType): array 159 | { 160 | // No need to convert equal types 161 | if ($givenType === $toType) { 162 | return []; 163 | } 164 | 165 | // Has this check already been computed? 166 | if (isset($this->conversionCache[$givenType . '|' . $toType])) { 167 | return $this->conversionCache[$givenType . '|' . $toType]; 168 | } 169 | 170 | // Check if a constructor interface exists for the given type 171 | // Check if the target type implements the constructor interface 172 | // required for conversion 173 | $conversionInfo = []; 174 | if (isset($this->conversionInterfaces[$givenType]) && 175 | is_subclass_of($toType, $this->conversionInterfaces[$givenType][0]) 176 | ) { 177 | $conversionInfo = $this->conversionInterfaces[$givenType]; 178 | } elseif ($this->isCollectionType($toType) && $this->isAccessibleArray($givenType)) { 179 | $conversionInfo = $this->conversionInterfaces['array'] ?? []; 180 | } 181 | 182 | if (!$conversionInfo && class_exists($givenType)) { 183 | $parentClasses = array_merge(class_parents($givenType), class_implements($givenType)); 184 | if (is_array($parentClasses)) { 185 | foreach ($parentClasses as $className) { 186 | if ($this->canTypeBeConvertedToType($className, $toType)) { 187 | $conversionInfo = $this->conversionInterfaces[$className]; 188 | break; 189 | } 190 | } 191 | } 192 | } 193 | 194 | // Add to runtime cache 195 | $this->conversionCache[$givenType . '|' . $toType] = $conversionInfo; 196 | 197 | return $conversionInfo; 198 | } 199 | 200 | /** 201 | * Tries to convert the specified value to the specified data type 202 | * by using alternative constructors in $this->conversionInterfaces. 203 | */ 204 | public function convertValueToType(mixed $value, string $toType): mixed 205 | { 206 | $givenType = is_object($value) ? $value::class : gettype($value); 207 | 208 | // Skip if the type can't be converted 209 | $conversionInfo = $this->canTypeBeConvertedToType($givenType, $toType); 210 | if (!$conversionInfo) { 211 | return $value; 212 | } 213 | 214 | // Attempt to convert a collection of objects 215 | if ($this->isCollectionType($toType)) { 216 | $subtype = $this->extractCollectionItemType($toType); 217 | foreach ($value as &$item) { 218 | $item = $this->convertValueToType($item, $subtype); 219 | } 220 | return $value; 221 | } 222 | 223 | // Call alternative constructor provided by interface 224 | $constructor = $conversionInfo[1]; 225 | return $toType::$constructor($value); 226 | } 227 | 228 | /** 229 | * Checks if the provided type describes a collection of values. 230 | */ 231 | protected function isCollectionType(string $type): bool 232 | { 233 | return str_ends_with($type, '[]'); 234 | } 235 | 236 | /** 237 | * Extracts the type of individual items from a collection type. 238 | * 239 | * @param string $type e. g. Vendor\MyExtension\MyClass[] 240 | * 241 | * @return string e. g. Vendor\MyExtension\MyClass 242 | */ 243 | protected function extractCollectionItemType(string $type): string 244 | { 245 | return substr($type, 0, -2); 246 | } 247 | 248 | /** 249 | * Checks if the given type is behaving like an array. 250 | */ 251 | protected function isAccessibleArray(string $typeOrClassName): bool 252 | { 253 | return $typeOrClassName === 'array' || 254 | (is_subclass_of($typeOrClassName, ArrayAccess::class) && is_subclass_of($typeOrClassName, Traversable::class)); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /Classes/Utility/ComponentLoader.php: -------------------------------------------------------------------------------- 1 | component file associations. 14 | */ 15 | protected array $componentsCache = []; 16 | 17 | public function __construct() 18 | { 19 | $this->setNamespaces( 20 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces'] ?? [] 21 | ); 22 | } 23 | 24 | /** 25 | * Adds a new component namespace. 26 | */ 27 | public function addNamespace(string $namespace, string $path): self 28 | { 29 | // Sanitize namespace data 30 | $namespace = $this->sanitizeNamespace($namespace); 31 | $path = $this->sanitizePath($path); 32 | 33 | $this->namespaces[$namespace] = $path; 34 | return $this; 35 | } 36 | 37 | /** 38 | * Removes a registered component namespace. 39 | */ 40 | public function removeNamespace(string $namespace): self 41 | { 42 | unset($this->namespaces[$namespace]); 43 | return $this; 44 | } 45 | 46 | /** 47 | * Sets the component namespaces. 48 | */ 49 | public function setNamespaces(array $namespaces): self 50 | { 51 | // Make sure that namespaces are sanitized 52 | $this->namespaces = []; 53 | foreach ($namespaces as $namespace => $path) { 54 | $this->addNamespace($namespace, $path); 55 | } 56 | 57 | // Order by namespace specificity 58 | krsort($this->namespaces); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * Returns all registered component namespaces. 65 | */ 66 | public function getNamespaces(): array 67 | { 68 | return $this->namespaces; 69 | } 70 | 71 | /** 72 | * Finds a component file based on its class namespace. 73 | */ 74 | public function findComponent(string $class, string $ext = '.html'): string|null 75 | { 76 | // Try cache first 77 | $cacheIdentifier = $class . '|' . $ext; 78 | if (isset($this->componentsCache[$cacheIdentifier])) { 79 | return $this->componentsCache[$cacheIdentifier]; 80 | } 81 | 82 | // Walk through available namespaces, ordered from specific to unspecific 83 | $class = ltrim($class, '\\'); 84 | foreach ($this->namespaces as $namespace => $path) { 85 | // No match, skip to next 86 | if (!str_starts_with($class, $namespace . '\\')) { 87 | continue; 88 | } 89 | 90 | $componentParts = explode('\\', trim(substr($class, strlen($namespace)), '\\')); 91 | 92 | $componentPath = $path . DIRECTORY_SEPARATOR . implode(DIRECTORY_SEPARATOR, $componentParts); 93 | $componentFile = $componentPath . DIRECTORY_SEPARATOR . end($componentParts) . $ext; 94 | 95 | // Check if component file exists 96 | if (file_exists($componentFile)) { 97 | $this->componentsCache[$cacheIdentifier] = $componentFile; 98 | return $componentFile; 99 | } 100 | } 101 | 102 | return null; 103 | } 104 | 105 | /** 106 | * Provides a list of all components that are available in the specified component namespace. 107 | * 108 | * @return array Array of components where the keys contain the component identifier (FQCN) 109 | * and the values contain the path to the component 110 | */ 111 | public function findComponentsInNamespace(string $namespace, string $ext = '.html'): array 112 | { 113 | if (!isset($this->namespaces[$namespace]) || !is_dir($this->namespaces[$namespace])) { 114 | return []; 115 | } 116 | 117 | $scannedPaths = []; 118 | return $this->scanForComponents( 119 | $this->namespaces[$namespace], 120 | $ext, 121 | $namespace, 122 | $scannedPaths 123 | ); 124 | } 125 | 126 | /** 127 | * Searches for component files in a directory and maps them to their namespace. 128 | * 129 | * @param array $scannedPaths Collection of paths that have already been scanned for components; 130 | * this prevents infinite loops caused by circular symlinks 131 | * 132 | * @return array 133 | */ 134 | protected function scanForComponents(string $path, string $ext, string $namespace, array &$scannedPaths): array 135 | { 136 | $components = []; 137 | 138 | $componentCandidates = scandir($path); 139 | foreach ($componentCandidates as $componentName) { 140 | // Skip relative links 141 | if ($componentName === '.' || $componentName === '..') { 142 | continue; 143 | } 144 | 145 | // Only search for directories and prevent infinite loops 146 | $componentPath = realpath($path . DIRECTORY_SEPARATOR . $componentName); 147 | if (!is_dir($componentPath) || isset($scannedPaths[$componentPath])) { 148 | continue; 149 | } 150 | $scannedPaths[$componentPath] = true; 151 | 152 | $componentNamespace = $namespace . '\\' . $componentName; 153 | $componentFile = $componentPath . DIRECTORY_SEPARATOR . $componentName . $ext; 154 | 155 | // Only match folders that contain a component file 156 | if (file_exists($componentFile)) { 157 | $components[$componentNamespace] = $componentFile; 158 | } 159 | 160 | // Continue recursively 161 | $components = array_merge( 162 | $components, 163 | $this->scanForComponents($componentPath, $ext, $componentNamespace, $scannedPaths) 164 | ); 165 | } 166 | 167 | return $components; 168 | } 169 | 170 | /** 171 | * Sanitizes a PHP namespace for use in the component loader. 172 | */ 173 | protected function sanitizeNamespace(string $namespace): string 174 | { 175 | return trim($namespace, '\\'); 176 | } 177 | 178 | /** 179 | * Sanitizes a path for use in the component loader. 180 | */ 181 | protected function sanitizePath(string $path): string 182 | { 183 | return rtrim($path, DIRECTORY_SEPARATOR); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Classes/Utility/ComponentPrefixer/ComponentPrefixerInterface.php: -------------------------------------------------------------------------------- 1 | reset(); 19 | } 20 | 21 | /** 22 | * Resets the settings to the default state (settings from ext_localconf.php and TypoScript). 23 | */ 24 | public function reset(): void 25 | { 26 | $this->settings = array_merge( 27 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['settings'] ?? [], 28 | $this->typoScriptService->convertTypoScriptArrayToPlainArray( 29 | $GLOBALS['TSFE']->tmpl->setup['config.']['tx_fluidcomponents.']['settings.'] ?? [] 30 | ) 31 | ); 32 | } 33 | 34 | /** 35 | * Checks if the specified settings path exists. 36 | */ 37 | public function exists(string $path): bool 38 | { 39 | return $this->get($path) !== null; 40 | } 41 | 42 | /** 43 | * Returns the value of the specified settings path. 44 | */ 45 | public function get(string $path) 46 | { 47 | $path = explode('.', $path); 48 | $value = $this->settings; 49 | foreach ($path as $segment) { 50 | if (!isset($value[$segment])) { 51 | return; 52 | } 53 | $value = $value[$segment]; 54 | } 55 | return $value; 56 | } 57 | 58 | /** 59 | * Sets the value of the specified settings path. 60 | */ 61 | public function set(string $path, mixed $value): self 62 | { 63 | $path = explode('.', $path); 64 | $variable =& $this->settings; 65 | foreach ($path as $segment) { 66 | if (!isset($variable[$segment])) { 67 | $variable[$segment] = []; 68 | } 69 | $variable =& $variable[$segment]; 70 | } 71 | $variable = $value; 72 | return $this; 73 | } 74 | 75 | /** 76 | * Unsets the specified settings path. 77 | */ 78 | public function unset(string $path): self 79 | { 80 | $this->set($path, null); 81 | return $this; 82 | } 83 | 84 | /** 85 | * Checks if a subsetting exists; Part of the ArrayAccess implementation. 86 | */ 87 | #[ReturnTypeWillChange] 88 | public function offsetExists(mixed $offset): bool 89 | { 90 | return isset($this->settings[$offset]); 91 | } 92 | 93 | /** 94 | * Returns the value of a subsetting; Part of the ArrayAccess implementation. 95 | */ 96 | #[ReturnTypeWillChange] 97 | public function offsetGet(mixed $offset): mixed 98 | { 99 | return $this->settings[$offset]; 100 | } 101 | 102 | /** 103 | * Sets the value of a subsetting; Part of the ArrayAccess implementation. 104 | */ 105 | #[ReturnTypeWillChange] 106 | public function offsetSet(mixed $offset, mixed $value): void 107 | { 108 | $this->settings[$offset] = $value; 109 | } 110 | 111 | /** 112 | * Unsets a subsetting; Part of the ArrayAccess implementation. 113 | */ 114 | #[ReturnTypeWillChange] 115 | public function offsetUnset(mixed $offset): void 116 | { 117 | unset($this->settings[$offset]); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/ComponentViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('description', 'string', 'Description of the component'); 14 | } 15 | 16 | public function render(): string 17 | { 18 | return trim((string) $this->renderChildren()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/ContentViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('slot', 'string', 'Slot name', false, ComponentRenderer::DEFAULT_SLOT); 17 | } 18 | 19 | public function render(): void 20 | { 21 | } 22 | 23 | public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler) 24 | { 25 | return ''; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Form/FieldInformationViewHelper.php: -------------------------------------------------------------------------------- 1 | 14 | * 19 | * 25 | * 26 | * 27 | * 28 | * Attributes of variable 'field': 29 | * field = [ 30 | * additionalAttributes => [ 31 | * placeholder => 'First name' 32 | * required => 'required' 33 | * minlength => '1' 34 | * maxlength => '2' 35 | * ] 36 | * data => NULL 37 | * name => 'tx_form_formframework[register-168][text-2]' 38 | * value => 'A' 39 | * property => 'text-2' 40 | * nameWithoutPrefix => 'register-168[text-2]', 41 | * formIdentifier => register-168 42 | * ] 43 | * 44 | * @package SMS\FluidComponents\ViewHelpers\Form 45 | * 46 | * @author Alexander Bohndorf 47 | */ 48 | class FieldInformationViewHelper extends AbstractFormFieldViewHelper 49 | { 50 | protected $escapeOutput = false; 51 | 52 | public function initializeArguments(): void 53 | { 54 | parent::initializeArguments(); 55 | $this->registerArgument( 56 | 'as', 57 | 'string', 58 | 'Name of the variable that should contain information about the current form field', 59 | false, 60 | 'fieldInformation' 61 | ); 62 | } 63 | 64 | /** 65 | * Provides information about the form field to the child elements of this ViewHelper. 66 | */ 67 | public function render(): string 68 | { 69 | // Get form context if available 70 | $formRuntime = $this->renderingContext 71 | ->getViewHelperVariableContainer() 72 | ->get(RenderRenderableViewHelper::class, 'formRuntime'); 73 | 74 | $properties = $this->arguments; 75 | unset($properties['as']); 76 | 77 | // Get raw field properties 78 | $properties['value'] = $this->getValueAttribute(); 79 | $properties['name'] = $this->getName(); 80 | $properties['nameWithoutPrefix'] = $this->getNameWithoutPrefix(); 81 | $properties['formIdentifier'] = ($formRuntime) ? $formRuntime->getFormDefinition()->getIdentifier() : null; 82 | $properties['prefix'] = $this->getPrefix(); 83 | 84 | // Provide form properties to children (e. g. components) 85 | $this->templateVariableContainer->add($this->arguments['as'], $properties); 86 | $output = $this->renderChildren(); 87 | $this->templateVariableContainer->remove($this->arguments['as']); 88 | 89 | return $output; 90 | } 91 | 92 | /** 93 | * @return string prefix/namespace 94 | */ 95 | protected function getPrefix(): string 96 | { 97 | if (!$this->viewHelperVariableContainer->exists(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'fieldNamePrefix')) { 98 | return ''; 99 | } 100 | $fieldNamePrefix = (string)$this->viewHelperVariableContainer->get(\TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper::class, 'fieldNamePrefix'); 101 | return $fieldNamePrefix; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Form/TranslatedValidationResultsViewHelper.php: -------------------------------------------------------------------------------- 1 | 26 | * 27 | * 28 | * {error.message}
29 | *
30 | * 31 | * 32 | *
33 | * 34 | * 35 | * @package SMS\FluidComponents\ViewHelpers\Form 36 | * 37 | * @author Simon Praetorius 38 | */ 39 | class TranslatedValidationResultsViewHelper extends AbstractViewHelper 40 | { 41 | /** 42 | * Stores message objects that have already been translated. 43 | */ 44 | protected static array $translatedMessagesCache = []; 45 | 46 | protected $escapeOutput = false; 47 | 48 | public function initializeArguments(): void 49 | { 50 | $this->registerArgument('for', 'string', 'The name of the error name (e.g. argument name or property name). This can also be a property path (like blog.title), and will then only display the validation errors of that property.', false, ''); 51 | $this->registerArgument('as', 'string', 'The name of the variable to store the current error', false, 'validationResults'); 52 | $this->registerArgument('translatePrefix', 'string', 'String that should be prepended to every language key; Will be ignored if $element is set.', false, 'validation.error.'); 53 | $this->registerArgument('element', RootRenderableInterface::class, 'Form Element to translate'); 54 | $this->registerArgument('extensionName', 'string', 'UpperCamelCased extension key (for example BlogExample)'); 55 | $this->registerArgument('languageKey', 'string', 'Language key ("dk" for example) or "default" to use for this translation. If this argument is empty, we use the current language'); 56 | // @deprecated will be removed in 4.0 57 | $this->registerArgument('alternativeLanguageKeys', 'array', 'Alternative language keys if no translation does exist'); 58 | } 59 | 60 | /** 61 | * Provides and translates validation results for the specified form field. 62 | */ 63 | public function render(): mixed 64 | { 65 | $templateVariableContainer = $this->renderingContext->getVariableProvider(); 66 | 67 | $extensionName = $this->arguments['extensionName'] ?? $this->getRequest()->getControllerExtensionName(); 68 | $for = rtrim((string) $this->arguments['for'], '.'); 69 | $element = $this->arguments['element']; 70 | 71 | $translatedResults = [ 72 | 'errors' => [], 73 | 'warnings' => [], 74 | 'notices' => [], 75 | 'flattenedErrors' => [], 76 | 'flattenedWarnings' => [], 77 | 'flattenedNotices' => [], 78 | 'hasErrors' => false, 79 | 'hasWarnings' => false, 80 | 'hasNotices' => false, 81 | ]; 82 | 83 | if ($element) { 84 | // Generate validation selector based on EXT:form element 85 | $for = $element->getRootForm()->getIdentifier() . '.' . $element->getIdentifier(); 86 | $translatePrefix = ''; 87 | } else { 88 | // Generate static language prefix for validation translations outsite of EXT:form 89 | $translatePrefix = ($this->arguments['translatePrefix']) ? rtrim((string) $this->arguments['translatePrefix'], '.') . '.' : ''; 90 | $translatePrefix .= ($for) ? $for . '.' : ''; 91 | } 92 | 93 | /** @var \TYPO3\CMS\Extbase\Mvc\ExtbaseRequestParameters $extbaseRequestParameters */ 94 | $extbaseRequestParameters = $this->getRequest()->getAttribute('extbase'); 95 | if ($extbaseRequestParameters === null) { 96 | throw new RuntimeException('ViewHelper fc:form.translatedValidationResults needs an extbase context to fetch form validation results', 1727270853); 97 | } 98 | $validationResults = $extbaseRequestParameters->getOriginalRequestMappingResults(); 99 | if ($validationResults !== null && $for !== '') { 100 | $validationResults = $validationResults->forProperty($for); 101 | } 102 | 103 | // Translate validation messages that refer to the current form field 104 | $levels = [ 105 | 'errors' => $validationResults->getErrors(), 106 | 'warnings' => $validationResults->getWarnings(), 107 | 'notices' => $validationResults->getNotices(), 108 | ]; 109 | foreach ($levels as $level => $messages) { 110 | foreach ($messages as $message) { 111 | $translatedResults[$level][] = $this->translateMessage( 112 | $this->renderingContext, 113 | $message, 114 | $translatePrefix, 115 | $element, 116 | $extensionName, 117 | $this->arguments['languageKey'], 118 | $this->arguments['alternativeLanguageKeys'] 119 | ); 120 | } 121 | } 122 | 123 | // Translate validation messages that refer to child fields (flattenedErrors) 124 | $levels = [ 125 | 'flattenedErrors' => $validationResults->getFlattenedErrors(), 126 | 'flattenedWarnings' => $validationResults->getFlattenedWarnings(), 127 | 'flattenedNotices' => $validationResults->getFlattenedNotices(), 128 | ]; 129 | foreach ($levels as $level => $flattened) { 130 | foreach ($flattened as $identifier => $messages) { 131 | $translatedResults[$level][$identifier] = []; 132 | foreach ($messages as $message) { 133 | $translatedResults[$level][$identifier][] = static::translateMessage( 134 | $this->renderingContext, 135 | $message, 136 | $translatePrefix . $identifier . '.', 137 | $element, 138 | $extensionName, 139 | $this->arguments['languageKey'], 140 | $this->arguments['alternativeLanguageKeys'] 141 | ); 142 | } 143 | } 144 | } 145 | 146 | $translatedResults['hasErrors'] = !empty($translatedResults['errors']); 147 | $translatedResults['hasWarnings'] = !empty($translatedResults['warnings']); 148 | $translatedResults['hasNotices'] = !empty($translatedResults['notices']); 149 | 150 | $templateVariableContainer->add($this->arguments['as'], $translatedResults); 151 | $output = $this->renderChildren(); 152 | $templateVariableContainer->remove($this->arguments['as']); 153 | 154 | return $output; 155 | } 156 | 157 | /** 158 | * Translates a validation message, either by using EXT:form's translation chain 159 | * or by the custom implementation of fluid_components for validation translations. 160 | */ 161 | protected static function translateMessage( 162 | RenderingContextInterface $renderingContext, 163 | Message $message, 164 | string $translatePrefix = '', 165 | ?RootRenderableInterface $element = null, 166 | ?string $extensionName = null, 167 | ?string $languageKey = null, 168 | ?array $alternativeLanguageKeys = null 169 | ) { 170 | // Make sure that messages are translated only once 171 | $hash = spl_object_hash($message); 172 | if (isset(static::$translatedMessagesCache[$hash])) { 173 | return static::$translatedMessagesCache[$hash]; 174 | } 175 | 176 | if ($element) { 177 | // Use EXT:form for translation 178 | $translatedMessage = static::translateFormElementError( 179 | $renderingContext, 180 | $element, 181 | $message->getCode(), 182 | $message->getArguments(), 183 | $message->getMessage() 184 | ); 185 | } else { 186 | // Use TYPO3 for translation 187 | $translatedMessage = static::translateValidationError( 188 | [$translatePrefix], 189 | $message->getCode(), 190 | $message->getArguments(), 191 | $message->getMessage(), 192 | $extensionName, 193 | $languageKey, 194 | $alternativeLanguageKeys 195 | ); 196 | } 197 | 198 | // Create new message object from the translated message 199 | $messageClass = $message::class; 200 | $newMessage = new $messageClass( 201 | $translatedMessage, 202 | $message->getCode(), 203 | $message->getArguments(), 204 | $message->getTitle() 205 | ); 206 | 207 | // Prevent double translations 208 | self::$translatedMessagesCache[$hash] = $newMessage; 209 | self::$translatedMessagesCache[spl_object_hash($newMessage)] = $newMessage; 210 | 211 | return $newMessage; 212 | } 213 | 214 | /** 215 | * Translates the provided validation message by using TYPO3's localization utility. 216 | * 217 | * @param array $translationChain Chain of translation keys that should be checked for translations 218 | * @param int $code Validation error code 219 | * @param array $arguments The arguments of the extension, being passed over to vsprintf 220 | * @param string $defaultValue Default validation message used as a fallback 221 | * @param string|null $extensionName The name of the extension 222 | * @param string $languageKey The language key or null for using the current language from the system 223 | * @param string[] $alternativeLanguageKeys The alternative language keys if no translation was found. If null and we are in the frontend, then the language_alt from TypoScript setup will be used. @deprecated will be removed in 4.0 224 | * 225 | * @return string|null the value from LOCAL_LANG or null if no translation was found 226 | */ 227 | public static function translateValidationError( 228 | array $translationChain, 229 | int $code, 230 | array $arguments, 231 | string $defaultValue = '', 232 | ?string $extensionName = null, 233 | ?string $languageKey = null, 234 | ?array $alternativeLanguageKeys = null 235 | ): ?string { 236 | if ($alternativeLanguageKeys) { 237 | trigger_error('Calling translatedValidationResults with the argument $alternativeLanguageKeys will be removed in fluid-components 4.0', E_USER_DEPRECATED); 238 | } 239 | if ($languageKey) { 240 | $localeFactory = GeneralUtility::makeInstance(Locales::class); 241 | $locale = $localeFactory->createLocale($languageKey, $alternativeLanguageKeys); 242 | } 243 | 244 | foreach ($translationChain as $translatePrefix) { 245 | $translatedMessage = LocalizationUtility::translate( 246 | $translatePrefix . $code, 247 | $extensionName, 248 | $arguments, 249 | $locale ?? null, 250 | ); 251 | if ($translatedMessage) { 252 | return $translatedMessage; 253 | } 254 | } 255 | return $defaultValue; 256 | } 257 | 258 | /** 259 | * Translates the provided validation message by using the translation chain by EXT:form. 260 | * 261 | * @throws InvalidArgumentException 262 | */ 263 | public static function translateFormElementError( 264 | RenderingContextInterface $renderingContext, 265 | RootRenderableInterface $element, 266 | int $code, 267 | array $arguments, 268 | string $defaultValue = '' 269 | ): string { 270 | /** @var FormRuntime $formRuntime */ 271 | $formRuntime = $renderingContext 272 | ->getViewHelperVariableContainer() 273 | ->get(RenderRenderableViewHelper::class, 'formRuntime'); 274 | 275 | return GeneralUtility::makeInstance(TranslationService::class)->translateFormElementError( 276 | $element, 277 | $code, 278 | $arguments, 279 | $defaultValue, 280 | $formRuntime 281 | ); 282 | } 283 | 284 | private function getRequest():? RequestInterface 285 | { 286 | if (method_exists($this->renderingContext, 'getAttribute') && 287 | method_exists($this->renderingContext, 'hasAttribute') && 288 | $this->renderingContext->hasAttribute(ServerRequestInterface::class) 289 | ) { 290 | return $this->renderingContext->getAttribute(ServerRequestInterface::class); 291 | } else { 292 | return $this->renderingContext->getRequest(); 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/ParamViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('name', 'string', 'Parameter name', true); 14 | $this->registerArgument('type', 'string', 'Parameter type', true); 15 | $this->registerArgument('optional', 'bool', 'Is parameter optional?', false, false); 16 | $this->registerArgument('default', 'string', 'Default value'); 17 | $this->registerArgument('description', 'string', 'Description of the parameter'); 18 | } 19 | 20 | public function render(): void 21 | { 22 | } 23 | 24 | public function compile($argumentsName, $closureName, &$initializationPhpCode, ViewHelperNode $node, TemplateCompiler $compiler) 25 | { 26 | return ''; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/RendererViewHelper.php: -------------------------------------------------------------------------------- 1 | renderChildren(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/SlotViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('name', 'string', 'Name of the slot that should be rendered', false, 'content'); 16 | $this->registerArgument( 17 | 'default', 18 | 'string', 19 | 'Default content that should be rendered if slot is not defined (falls back to tag content)', 20 | false, 21 | null, 22 | true 23 | ); 24 | } 25 | 26 | public function render(): mixed 27 | { 28 | $slotContent = $this->renderingContext->getVariableProvider()->get($this->arguments['name']); 29 | 30 | if (isset($slotContent) && !$slotContent instanceof Slot) { 31 | throw new InvalidArgumentException( 32 | sprintf('Slot "%s" cannot be rendered because it isn\'t a valid slot object.', $this->arguments['name']), 33 | 1670247849 34 | ); 35 | } 36 | 37 | if ((string)$slotContent === '') { 38 | return $this->arguments['default'] ?: $this->renderChildren(); 39 | } 40 | 41 | return $slotContent; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Translate/LabelsViewHelper.php: -------------------------------------------------------------------------------- 1 | registerArgument('keys', 'array', 'Array of translation keys; Can also contain subarrays, then "key" is key, "arguments" is an array of sprintf arguments, and "default" is a default value', true); 23 | $this->registerArgument('extensionName', 'string', 'UpperCamelCased extension key (for example BlogExample)'); 24 | $this->registerArgument('languageKey', 'string', 'Language key ("dk" for example) or "default" to use for this translation. If this argument is empty, we use the current language'); 25 | // @deprecated will be removed in 4.0 26 | $this->registerArgument('alternativeLanguageKeys', 'array', 'Alternative language keys if no translation does exist'); 27 | } 28 | 29 | public function render(): array 30 | { 31 | $keys = $this->arguments['keys']; 32 | $extensionName = $this->arguments['extensionName'] ?? $this->getRequest()->getControllerExtensionName(); 33 | 34 | $labels = []; 35 | foreach ($keys as $name => $translation) { 36 | if (is_array($translation)) { 37 | $translateArguments = $translation['arguments'] ?? []; 38 | $default = $translation['default'] ?? ''; 39 | $translation = $translation['key'] ?? ''; 40 | } else { 41 | $translateArguments = []; 42 | $default = ''; 43 | } 44 | 45 | if ($this->arguments['alternativeLanguageKeys']) { 46 | trigger_error('Calling labels with the argument alternativeLanguageKeys will be removed in fluid-components 4.0', E_USER_DEPRECATED); 47 | } 48 | if ($this->arguments['languageKey']) { 49 | $localeFactory = GeneralUtility::makeInstance(Locales::class); 50 | $locale = $localeFactory->createLocale($this->arguments['languageKey'], $this->arguments['alternativeLanguageKeys']); 51 | } 52 | 53 | try { 54 | $value = LocalizationUtility::translate($translation, $extensionName, $translateArguments, $locale ?? null); 55 | } catch (InvalidArgumentException) { 56 | $value = null; 57 | } 58 | if ($value === null) { 59 | $value = $default; 60 | if (!empty($translateArguments)) { 61 | $value = vsprintf($value, $translateArguments); 62 | } 63 | } 64 | 65 | $labels[$name] = $value; 66 | } 67 | 68 | return $labels; 69 | } 70 | 71 | private function getRequest():? RequestInterface 72 | { 73 | if (method_exists($this->renderingContext, 'getAttribute') && 74 | method_exists($this->renderingContext, 'hasAttribute') && 75 | $this->renderingContext->hasAttribute(ServerRequestInterface::class) 76 | ) { 77 | return $this->renderingContext->getAttribute(ServerRequestInterface::class); 78 | } else { 79 | return $this->renderingContext->getRequest(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Variable/MapViewHelper.php: -------------------------------------------------------------------------------- 1 | 15 | * items="{data.tx_mask_sliders -> fc:variable.map(fieldMapping: {image: 'tx_mask_slider_image.0', content: 'tx_mask_slider_text'})}" 16 | * 17 | * 18 | * 19 | * {myNavigation -> fc:variable.map(fieldMapping: {url: 'link', title: 'data.page_extend_field'}, keepFields: 'data, target')} 20 | * 21 | * 22 | * @package SMS\FluidComponents\ViewHelpers\Variable 23 | * 24 | * @author Simon Praetorius 25 | */ 26 | class MapViewHelper extends AbstractViewHelper 27 | { 28 | protected $escapeOutput = false; 29 | 30 | public function initializeArguments(): void 31 | { 32 | parent::initializeArguments(); 33 | $this->registerArgument('fieldMapping', 'array', 'Map of fields keys.'); 34 | $this->registerArgument('keepFields', 'mixed', 'Array or comma separated list of fields to keep in array.'); 35 | $this->registerArgument('subject', 'mixed', 'The array of objects/arrays to remap.'); 36 | } 37 | 38 | /** 39 | * @return array remapped array 40 | */ 41 | public function render() 42 | { 43 | $subject = $this->arguments['subject'] ?? $this->renderChildren(); 44 | $mapKeyArray = $this->arguments['fieldMapping'] ?? []; 45 | $keepFields = $this->arguments['keepFields'] ?? []; 46 | if (!is_array($keepFields)) { 47 | $keepFields = array_map('trim', explode(',', (string) $keepFields)); 48 | } 49 | 50 | $newArray = []; 51 | foreach ($subject as $item) { 52 | $variableProvider = new StandardVariableProvider(); 53 | $variableProvider->setSource($item); 54 | 55 | $newItem = []; 56 | foreach ($mapKeyArray as $newKey => $oldKey) { 57 | $newItem[$newKey] = $variableProvider->get($oldKey); 58 | } 59 | 60 | //Add another fields from keepFields list 61 | foreach ($keepFields as $key) { 62 | $newItem[$key] = $variableProvider->get($key); 63 | } 64 | 65 | $newArray[] = $newItem; 66 | } 67 | 68 | return $newArray; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Classes/ViewHelpers/Variable/PushViewHelper.php: -------------------------------------------------------------------------------- 1 | 11 | * 12 | * 13 | * 14 | * 15 | * 16 | * 17 | * @package SMS\FluidComponents\ViewHelpers\Variable 18 | * 19 | * @author Simon Praetorius 20 | */ 21 | class PushViewHelper extends AbstractViewHelper 22 | { 23 | public function initializeArguments(): void 24 | { 25 | $this->registerArgument('item', 'mixed', 'Item to push to specified array variable. If not in arguments then taken from tag content'); 26 | $this->registerArgument('name', 'string', 'Name of variable to extend', true); 27 | $this->registerArgument('key', 'string', 'Key that should be used in the array'); 28 | } 29 | 30 | public function render(): void 31 | { 32 | $value = $this->arguments['item'] ?? $this->renderChildren(); 33 | 34 | $variable = $this->renderingContext->getVariableProvider()->get($this->arguments['name']); 35 | if (!is_array($variable)) { 36 | $variable = []; 37 | } 38 | if ($this->arguments['key']) { 39 | $variable[$this->arguments['key']] = $value; 40 | } else { 41 | $variable[] = $value; 42 | } 43 | 44 | $this->renderingContext->getVariableProvider()->add($this->arguments['name'], $variable); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Configuration/Services.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | _defaults: 3 | autowire: true 4 | autoconfigure: true 5 | public: false 6 | 7 | SMS\FluidComponents\: 8 | resource: '../Classes/*' 9 | exclude: '../Classes/Domain/Model/*' 10 | 11 | SMS\FluidComponents\Service\PlaceholderImageService: 12 | public: true 13 | 14 | SMS\FluidComponents\Command\GenerateXsdCommand: 15 | tags: 16 | - name: 'console.command' 17 | command: 'fluidcomponents:generatexsd' 18 | description: 'Generates the XSD files for autocompletion in the IDE.' 19 | schedulable: true 20 | hidden: false 21 | 22 | SMS\FluidComponents\Command\CheckContentEscapingCommand: 23 | tags: 24 | - name: 'console.command' 25 | command: 'fluidcomponents:checkContentEscaping' 26 | description: 'Checks for possible escaping issues with content parameter due to changed children escaping behavior' 27 | schedulable: false 28 | hidden: false 29 | -------------------------------------------------------------------------------- /Documentation/AutoCompletion.md: -------------------------------------------------------------------------------- 1 | # Fluid Components: Auto Completion 2 | 3 | ## Cli Task 4 | This extension comes with a cli task to generate xsd files for your components. 5 | You can generate these files and reference them in your ide to activate syntax auto-completion for your 6 | components in your fluid templates. 7 | 8 | Generate xsd files of all components in the current directory: 9 | 10 | ``` 11 | $ bin/typo3 fluidcomponents:generatexsd . 12 | ``` 13 | 14 | The parameter -v adds more verbosity and informs about the xmlns attributes you have to add to the `html` tag in your 15 | fluid templates to activate syntax auto-completion. 16 | 17 | ``` 18 | bin/typo3 fluidcomponents:generatexsd . -v 19 | ``` 20 | 21 | ## Generate xsd for a dedicated namespace 22 | 23 | ``` 24 | $ bin/typo3 fluidcomponents:generatexsd . --namespace='Vendor\\MyExtension\\Components' -v 25 | ``` 26 | 27 | The namespace should be quoted with single quotes and each backlash has to be doubled. Don't add a leading backslash 28 | in name space. 29 | 30 | **It is good practice to include a xsd file for all your components in your extension, f.e. in directory `Documentation/Xsd`.** 31 | 32 | ## Header in your Fluid Templates 33 | 34 | You have to add an xmlns declarion inside of the html tag to activate syntax auto-completion in your fluid 35 | template. 36 | 37 | In the following example the namespace `me` is referenced to all of the fluid components defined in the extension 38 | `MyExtension`. 39 | 40 | ```xml 41 | 44 | 45 | ``` 46 | 47 | Inside this you can reference all your components inside your extension with view helpers like ``. 48 | -------------------------------------------------------------------------------- /Documentation/ComponentPrefixers.md: -------------------------------------------------------------------------------- 1 | # Fluid Components: Component Prefixers 2 | 3 | Each component provides a prefixed CSS class derived from the component's name and namespace in the `{component}` 4 | variable. This makes moving or renaming the component easy as the CSS classes change automatically. By default, 5 | Fluid Components uses a generic prefixer class (`SMS\FluidComponents\Utility\ComponentPrefixer\GenericComponentPrefixer`). 6 | For examples, take a look at the [Renderer ViewHelper reference](#renderer-viewhelper). 7 | 8 | However, prefixers can be overwritten per namespace, which makes it easy to customize the generated CSS classes. 9 | Your prefixer class needs to implement the interface `SMS\FluidComponents\Utility\ComponentPrefixer\ComponentPrefixerInterface`, 10 | which requires you to define two methods: 11 | 12 | ```php 13 | /** 14 | * Returns the component prefix for the provided component namespaces 15 | * 16 | * @param string $namespace 17 | * @return string 18 | */ 19 | public function prefix(string $namespace): string; 20 | 21 | /** 22 | * Returns the separator to be used between prefix and the following string 23 | * 24 | * @return string 25 | */ 26 | public function getSeparator(): string; 27 | ``` 28 | 29 | To bind your custom prefixer class to a namespace, simply add the following line to your *ext_localconf.php* file: 30 | 31 | ```php 32 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['prefixer']['VENDOR\\MyExtension'] = 33 | \VENDOR\MyExtension\Utility\ComponentPrefixer\MyComponentPrefixer::class; 34 | ``` 35 | -------------------------------------------------------------------------------- /Documentation/ComponentSettings.md: -------------------------------------------------------------------------------- 1 | # Fluid Components: Component Settings 2 | 3 | Each component provides the `{settings}` variable which contains global settings that can affect multiple components. 4 | These settings can be set in multiple ways: 5 | 6 | ## ext_localconf.php 7 | 8 | In your *ext_localconf.php* you can use an array notation to add global component settings. The following example 9 | fetches the settings from a JSON file: 10 | 11 | ```php 12 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['settings']['styles'] = json_decode(file_get_contents( 13 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('my_extension', 'styles.json') 14 | )); 15 | ``` 16 | 17 | ## Programmatically 18 | 19 | In your own PHP code, you can add settings by injecting the responsible settings class: 20 | 21 | ```php 22 | class MyController 23 | { 24 | /** 25 | * @var \SMS\FluidComponents\Utility\ComponentSettings 26 | */ 27 | public $componentSettings; 28 | 29 | public function myAction() 30 | { 31 | $this->componentSettings->set('mySetting', 'myValue'); 32 | } 33 | 34 | public function injectComponentSettings(\SMS\FluidComponents\Utility\ComponentSettings $componentSettings) 35 | { 36 | $this->componentSettings = $componentSettings; 37 | } 38 | } 39 | ``` 40 | 41 | ## TypoScript 42 | 43 | It is also possible to add settings via TypoScript: 44 | 45 | ``` 46 | config.tx_fluidcomponents.settings.mySetting = myValue 47 | ``` 48 | -------------------------------------------------------------------------------- /Documentation/Forms.md: -------------------------------------------------------------------------------- 1 | # Fluid Components: Usage with Forms 2 | 3 | There are two ViewHelpers that allow us to separate TYPO3's form logic from presentational components: 4 | 5 | * `fc:form.fieldInformation` provides information about the form field to the presentational component 6 | * `fc:form.translatedValidationResults` translates validation results and provides them to the presentational component in a simplified data structure 7 | 8 | Both ViewHelpers are documented in the [ViewHelper Reference](./ViewHelperReference.md). 9 | 10 | ## Example 11 | 12 | ```xml 13 | 14 | 15 | 16 | 20 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | ``` 32 | 33 | Components/Molecule/FieldLabel/FieldLabel.html: 34 | 35 | ```xml 36 | 37 | 38 | 39 | 40 | 41 |
42 | 46 | 47 |
    48 | 49 |
  • {validationMessage.message}
  • 50 |
    51 |
52 |
53 |
54 |
55 |
56 | ``` 57 | -------------------------------------------------------------------------------- /Documentation/UpdateNotes.md: -------------------------------------------------------------------------------- 1 | # Fluid Components: Updating from 1.x 2 | 3 | There is only one breaking change: It isn't possible anymore to use Fluid variables in default values 4 | for component parameters. This will **NOT WORK ANYMORE**: 5 | 6 | ```xml 7 | 8 | ``` 9 | 10 | As this was a pretty esoteric feature, you shouldn't have any problems when updating to 2.x! 11 | -------------------------------------------------------------------------------- /Documentation/Xsd/fc.xsd: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 32 | 38 | 39 | 40 | 41 | Attributes of variable 'field': 42 | field = [ 43 | additionalAttributes => [ 44 | placeholder => 'First name' 45 | required => 'required' 46 | minlength => '1' 47 | maxlength => '2' 48 | ] 49 | data => NULL 50 | name => 'tx_form_formframework[register-168][text-2]' 51 | value => 'A' 52 | property => 'text-2' 53 | nameWithoutPrefix => 'register-168[text-2]', 54 | formIdentifier => register-168 55 | ]]]> 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | , "name" and "value" properties will be ignored.]]> 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 105 | 106 | 107 | {error.message}
108 |
109 | 110 | 111 |
112 | ]]>
113 |
114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 |
161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 |
241 | 242 | -------------------------------------------------------------------------------- /Documentation/XssIssue.md: -------------------------------------------------------------------------------- 1 | # Mitigating XSS Issues with Fluid Components 3.5 2 | 3 | All versions of Fluid Components before 3.5 were susceptible to Cross-Site Scripting. Version 3.5 of the extension 4 | fixes this issue. Due to the nature of the problem, some changes in your project's Fluid templates might be necessary 5 | to prevent unwanted double-escaping of HTML markup. 6 | 7 | ## When do you need to take action? 8 | 9 | In the following edge case, the behavior of the extension changes and you need to adjust your template files: 10 | 11 | * Your component uses the predefined `{content}` parameter AND 12 | * Your component allows HTML markup in the `{content}` parameter by applying `f:format.raw()` before output AND 13 | * You pass a variable containing HTML markup to the component's `{content}` parameter without using `f:format.raw()`, `f:format.html()` or similar. 14 | 15 | ## Finding potential issues 16 | 17 | The extension contains a CLI tool that can help you in finding potential escaping issues in your templates. The tool 18 | picks up template files in all installed extensions (`ext/*/Resources/Private/**/*.html`) automatically, checks all 19 | available components and highlights component calls where changes might be necessary. The tool can be executed via CLI: 20 | 21 | ``` 22 | vendor/bin/typo3 fluidcomponents:checkContentEscaping 23 | ``` 24 | 25 | You get a list of potential issues in your template files, for example: 26 | 27 | > public/typo3conf/ext/sitepackage/Resources/Private/Extensions/GridElements/Templates/Accordion.html: 28 | > 29 | > Component "public/typo3conf/ext/sitepackage/Resources/Private/Components/Organism/Accordion/Accordion.html" expects raw html content, but was called with potentially escaped variables: {data.tx_gridelements_view_column_10} 30 | 31 | In this case, you would need to add `f:format.raw()` to the described variable, as described below. 32 | 33 | Please note that the tool might also show false positives where you don't need to change anything, for example: 34 | 35 | > public/typo3conf/ext/sitepackage/Resources/Private/Components/Organism/NewsArticle/NewsArticle.html: 36 | > 37 | > Component "public/typo3conf/ext/sitepackage/Resources/Private/Components/Molecule/Featurebar/Featurebar.html" expects raw html content, but was called with potentially escaped variables: {datePublished} 38 | 39 | This can be safely ignored because `{datePublished}` shouldn't contain HTML markup and thus can be escaped. 40 | 41 | ## Fixing double escaping in your templates 42 | 43 | Quote Component: 44 | 45 | ```xml 46 | 47 | 48 | 49 |
50 | {content -> f:format.raw()} 51 |
{author}
52 |
53 |
54 |
55 | ``` 56 | 57 | ### Adjustments Necessary 58 | 59 | In the following examples you need to adjust your template to prevent double escaping of the HTML. 60 | 61 | **⚠️ Please make sure to always check if the variable only contains *safe* HTML markup and no direct user input as this could lead to XSS issues!** 62 | 63 | General example: 64 | 65 | ```xml 66 | some text with html markup 67 | 68 | This is a quote that uses a {variableContainingHtmlMarkup}. 69 | 70 | This is a quote that uses a {variableContainingHtmlMarkup -> f:format.raw()}. 71 | ``` 72 | 73 | This is an example of the issue for a template which uses data processed by gridelements: 74 | 75 | ```xml 76 | 77 | 78 | 79 | 80 | ``` 81 | 82 | It can also happen with custom content elements that deal with RTE fields: 83 | 84 | ```xml 85 | 86 | 87 | 88 | 89 | ``` 90 | 91 | ### No Adjustments Necessary 92 | 93 | The following examples are fine and don't need to be changed: 94 | 95 | ```xml 96 | This is a simple quote without any HTML markup 97 | ``` 98 | 99 | ```xml 100 | This is a simple quote with some HTML markup 101 | ``` 102 | 103 | ```xml 104 | This is a quote that uses another component 105 | ``` 106 | 107 | ```xml 108 | This is a quote that uses {rteContent -> f:format.html()}. 109 | ``` 110 | 111 | ```xml 112 | just some text 113 | This is a quote that uses a {variableWithoutHtmlMarkup}. 114 | ``` 115 | 116 | ```xml 117 | some text with html markup 118 | This is a quote that uses a {variableContainingHtmlMarkup -> f:format.raw()}. 119 | ``` 120 | 121 | ## Using the new Slot feature 122 | 123 | This change isn't strictly necessary to update to the new version, but is advisable because it can make the usage of external 124 | markup more visible and explicit in your components. Developers are motivated to think about the source of a variable and 125 | the potential XSS issues that can be triggered by rendering external markup. 126 | 127 | The following quality rules should be set for your components: 128 | 129 | * `f:format.raw()`, `f:format.html()` and similar ViewHelpers should **never** be used inside a component, but rather in the 130 | template calling the component (where you know if the markup is safe) 131 | * `content` that can't contain HTML should be escaped by using the default variable syntax: `{content}` 132 | * `content` that can contain HTML must be rendered using the `fc:slot()` ViewHelper 133 | * To check for the existence of `content`, use the default variable syntax: `...` 134 | * other parameters that can contain HTML must be of type `Slot` and must be rendered with `fc:slot()` as well 135 | 136 | Improved Quote component: 137 | 138 | ```xml 139 | 140 | 141 | 142 | 143 |
144 | 145 |
146 | {author}, 147 | 148 |
149 |
150 |
151 |
152 | ``` 153 | 154 | Examples: 155 | 156 | ```xml 157 | 158 | This is a quote example. 159 | 160 | ``` 161 | 162 | ```xml 163 | Example.com 164 | 165 | This is a quote example. 166 | 167 | ``` 168 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fluid Components 2 | 3 | This TYPO3 extensions puts frontend developers in a position to create encapsulated components 4 | in pure Fluid. By defining a clear interface (API) for the integration, frontend developers can 5 | implement components independent of backend developers. The goal is to create highly reusable 6 | presentational components which have no side effects and aren't responsible for data acquisition. 7 | 8 | ⬇️ **[TL;DR? Get started right away](#getting-started)** ⬇️ 9 | 10 | ## What does it do? 11 | 12 | [Fluid](https://github.com/typo3/fluid) templates usually consist of three ingredients: 13 | 14 | * **Templates**, 15 | * **Layouts**, which structure and wrap the markup defined in the template, and 16 | * **Partials**, which contain markup snippets to be reused in different templates. 17 | 18 | In addition, **ViewHelpers** provide basic control structures and encapsulate advanced rendering and 19 | data manipulation that would otherwise not be possible. They are defined as PHP classes. 20 | 21 | The extension adds another ingredient to Fluid: **Components**. 22 | 23 | ## What are components? 24 | 25 | Fluid components are similar to ViewHelpers. The main difference is that they can be defined solely in 26 | Fluid. In a way, they are quite similar to Fluid's partials, but they have a few advantages: 27 | 28 | * They provide a **clear interface** via predefined parameters. The implementation is encapsulated in 29 | the component. You don't need to know what the component does internally to be able to use it. 30 | * With semantic component names your templates get more **readable**. This gets even better with 31 | [atomic design](http://bradfrost.com/blog/post/atomic-web-design/) or similar approaches. 32 | * They can easily be used across different TYPO3 extensions because they utilize Fluid's 33 | **namespaces**. No *partialRootPath* needed. 34 | 35 | ## How do components look like? 36 | 37 | The following component implements a simple teaser card element: 38 | 39 | *Components/TeaserCard/TeaserCard.html* 40 | 41 | ```xml 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |

{title}

51 | 52 |

53 |
54 | 55 | 56 | 57 | 58 |
59 |
60 |
61 | ``` 62 | 63 | Use the following code in your template to render a teaser card about TYPO3: 64 | 65 | ```xml 66 | {namespace my=VENDOR\MyExtension\Components} 67 | 72 | The professional, flexible Content Management System 73 | 74 | ``` 75 | 76 | The result is the following HTML: 77 | 78 | ```xml 79 | 80 |

TYPO3

81 |

The professional, flexible Content Management System

82 | 83 | 84 |
85 | ``` 86 | *(improved indentation for better readability)* 87 | 88 | ## Getting Started 89 | 90 | 1. Install the extension either [from TER](https://typo3.org/extensions/repository/view/fluid_components) 91 | or [via composer](https://packagist.org/packages/sitegeist/fluid-components): 92 | 93 | ``` 94 | composer require sitegeist/fluid-components 95 | ``` 96 | 97 | 2. Define the component namespace in your *ext_localconf.php*: 98 | 99 | ```php 100 | $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['fluid_components']['namespaces']['VENDOR\\MyExtension\\Components'] = 101 | \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::extPath('my_extension', 'Resources/Private/Components'); 102 | ``` 103 | 104 | Use your own vendor name for `VENDOR`, extension name for `MyExtension`, and extension key for `my_extension`. 105 | 106 | 3. Create your first component in *EXT:my_extension/Resources/Private/Components/* by creating a directory 107 | *MyComponent* containing a file *MyComponent.html* 108 | 109 | 4. Define and apply your component according to [How do components look like?](#how-do-components-look-like). The [Extended Documentation](#extended-documentation) 110 | can be helpful as well. 111 | 112 | 5. Check out [Fluid Styleguide](https://github.com/sitegeist/fluid-styleguide), a living styleguide for Fluid Components, and [Fluid Components Linter](https://github.com/sitegeist/fluid-components-linter) to improve the quality and reusability of your components. 113 | 114 | If you have any questions, need support or want to discuss components in TYPO3, feel free to join [#ext-fluid_components](https://typo3.slack.com/archives/ext-fluid_components). 115 | 116 | ## Why should I use components? 117 | 118 | * Components encourage **markup reusage and refactoring**. Only the component knows about its implementation 119 | details. As long as the interface stays compatible, the implementation can change. 120 | * Components can be a tool to **enforce design guidelines**. If the component's implementation respects the 121 | guidelines, they are respected everywhere the component is used. A helpful tool to accomplish that is the corresponding 122 | living styleguide: [Fluid Styleguide](https://github.com/sitegeist/fluid-styleguide). 123 | * Components **formalize and improve communication**. Frontend developers and integrators agree on a clearly 124 | defined interface instead of debating implementation details. 125 | * Components **reduce dependencies**. Frontend developers can work independent of integrators and backend developers. 126 | 127 | ## Extended Documentation 128 | 129 | Feature References 130 | 131 | * [ViewHelper Reference](Documentation/ViewHelperReference.md) 132 | * [Data Structures](Documentation/DataStructures.md) 133 | * [Links and Typolink](Documentation/DataStructures.md#links-and-typolink) 134 | * [Files and Images](Documentation/DataStructures.md#files-and-images) 135 | * [Translations](Documentation/DataStructures.md#translations) 136 | * [Navigations](Documentation/DataStructures.md#navigations) 137 | * [DateTime](Documentation/DataStructures.md#datetime) 138 | * [Slots](Documentation/DataStructures.md#slots) 139 | * [Component Prefixers](Documentation/ComponentPrefixers.md) 140 | * [Component Settings](Documentation/ComponentSettings.md) 141 | 142 | How-To's 143 | 144 | * [Usage with Forms](Documentation/Forms.md) 145 | * [Add auto-completion in your IDE](Documentation/AutoCompletion.md) 146 | * [Updating from 1.x](Documentation/UpdateNotes.md) 147 | * [Mitigating XSS issues with 3.5](Documentation/XssIssue.md) 148 | 149 | ## Authors & Sponsors 150 | 151 | * Ulrich Mathes - mathes@sitegeist.de 152 | * Simon Praetorius - moin@praetorius.me 153 | * [All contributors](https://github.com/sitegeist/fluid-components/graphs/contributors) 154 | 155 | *The development and the public-releases of this package is generously sponsored 156 | by my employer https://sitegeist.de.* 157 | -------------------------------------------------------------------------------- /Resources/Private/Templates/Placeholder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 |
17 |

{text}

18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 | -------------------------------------------------------------------------------- /Resources/Public/Icons/Extension.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sitegeist/fluid-components", 3 | "description": "Encapsulated frontend components with Fluid's ViewHelper syntax", 4 | "type": "typo3-cms-extension", 5 | "homepage": "https://github.com/sitegeist/fluid-components", 6 | "license": ["GPL-2.0-or-later"], 7 | "keywords": ["typo3", "typo3-extension", "fluid", "typo3-fluid", "components", "html", "template"], 8 | "authors": [ 9 | { 10 | "name": "Ulrich Mathes", 11 | "email": "mathes@sitegeist.de" 12 | }, 13 | { 14 | "name": "Simon Praetorius", 15 | "email": "moin@praetorius.me" 16 | } 17 | ], 18 | "support": { 19 | "issues": "https://github.com/sitegeist/fluid-components/issues" 20 | }, 21 | "require": { 22 | "php": "^8.2", 23 | "typo3/cms-core": "^13.4 || ^12.4" 24 | }, 25 | "require-dev": { 26 | "typo3/testing-framework": "^8.2", 27 | "squizlabs/php_codesniffer": "^3.10", 28 | "editorconfig-checker/editorconfig-checker": "^10.0", 29 | "phpspec/prophecy-phpunit": "^2.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "SMS\\FluidComponents\\": "Classes/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "SMS\\FluidComponents\\Tests\\": "Tests/" 39 | } 40 | }, 41 | "config": { 42 | "vendor-dir": ".Build/vendor", 43 | "bin-dir": ".Build/bin", 44 | "allow-plugins": { 45 | "typo3/class-alias-loader": true, 46 | "typo3/cms-composer-installers": true 47 | } 48 | }, 49 | "extra": { 50 | "typo3/cms": { 51 | "Package": { 52 | "serviceProvider": "SMS\\FluidComponents\\ServiceProvider" 53 | }, 54 | "cms-package-dir": "{$vendor-dir}/typo3/cms", 55 | "app-dir": ".Build", 56 | "web-dir": ".Build/Web", 57 | "extension-key": "fluid_components" 58 | } 59 | }, 60 | "scripts": { 61 | "lint": [ 62 | "@lint:php", 63 | "@lint:editorconfig" 64 | ], 65 | "lint:php": "phpcs --standard=PSR2 --extensions=php --exclude=Generic.Files.LineLength --ignore=.Build,.cache,Tests,ext_emconf.php .", 66 | "lint:editorconfig": "ec -exclude .Build .", 67 | "test": [ 68 | "@test:unit", 69 | "@test:functional" 70 | ], 71 | "test:unit": "Build/Scripts/runTests.sh", 72 | "test:functional": "Build/Scripts/runTests.sh -s functional", 73 | "prepare-release": [ 74 | "sed -i'' -e \"s/'version' => ''/'version' => '$(echo ${GITHUB_REF#refs/tags/} | sed 's/v//')'/\" ext_emconf.php", 75 | "rm -r .github .ecrc .editorconfig .gitattributes Build Tests" 76 | ] 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'Fluid Components', 4 | 'description' => 'Encapsulated frontend components with Fluid\'s ViewHelper syntax', 5 | 'category' => 'fe', 6 | 'author' => 'Ulrich Mathes, Simon Praetorius', 7 | 'author_email' => 'mathes@sitegeist.de, moin@praetorius.me', 8 | 'author_company' => 'sitegeist media solutions GmbH', 9 | 'state' => 'stable', 10 | 'version' => '', 11 | 'constraints' => [ 12 | 'depends' => [ 13 | 'typo3' => '12.4.0-13.9.99', 14 | 'php' => '8.2.0-8.3.99', 15 | ], 16 | 'conflicts' => [ 17 | ], 18 | 'suggests' => [ 19 | ], 20 | ], 21 | 'autoload' => [ 22 | 'psr-4' => [ 23 | 'SMS\\FluidComponents\\' => 'Classes', 24 | ], 25 | ], 26 | ]; 27 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | SMS\FluidComponents\Domain\Model\File::class, 15 | 'Image' => SMS\FluidComponents\Domain\Model\Image::class, 16 | 'Labels' => SMS\FluidComponents\Domain\Model\Labels::class, 17 | 'Link' => SMS\FluidComponents\Domain\Model\Link::class, 18 | 'Navigation' => SMS\FluidComponents\Domain\Model\Navigation::class, 19 | 'NavigationItem' => SMS\FluidComponents\Domain\Model\NavigationItem::class, 20 | 'Slot' => SMS\FluidComponents\Domain\Model\Slot::class, 21 | 'Typolink' => SMS\FluidComponents\Domain\Model\Typolink::class, 22 | ] 23 | ); 24 | 25 | if (!isset($GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['fluidComponents.partialsInComponents'])) { 26 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['features']['fluidComponents.partialsInComponents'] = false; 27 | } 28 | }); 29 | --------------------------------------------------------------------------------