exact download title (spelling variants, missing suffix) */ private const MANUAL_MAP = [ 'isrvideomissioncommander' => 'ISR Mission Commander', 'okadamanila' => 'VITEC Okada Manilla Usecase', ]; protected function configure(): void { $this->addOption('apply', null, InputOption::VALUE_NONE, 'Write changes (default: dry run)'); } protected function execute(InputInterface $input, OutputInterface $output): int { Bootstrap::initializeBackendAuthentication(); $apply = (bool)$input->getOption('apply'); $pool = GeneralUtility::makeInstance(ConnectionPool::class); $content = $pool->getConnectionForTable('tt_content'); $downloads = []; $allNorm = []; $uidByTitle = []; foreach ($pool->getConnectionForTable('tx_vitec_domain_model_download')->fetchAllAssociative( 'SELECT uid, title FROM tx_vitec_domain_model_download WHERE deleted = 0' ) as $download) { $uidByTitle[(string)$download['title']] = (int)$download['uid']; $allNorm[] = ['uid' => (int)$download['uid'], 'title' => (string)$download['title'], 'norm' => $this->norm((string)$download['title'])]; $base = $this->stripSuffix((string)$download['title']); if ($base !== null) { $downloads[] = ['uid' => (int)$download['uid'], 'title' => (string)$download['title'], 'norm' => $this->norm($base)]; } } $elements = $content->fetchAllAssociative( "SELECT c.uid, c.bodytext, c.hidden, u.title AS story_title FROM tt_content c LEFT JOIN tx_vitec_domain_model_usecase u ON u.uid = c.tx_vitec_usecase_content AND u.deleted = 0 WHERE c.deleted = 0 AND c.CType NOT IN ('vitec_downloadcard') AND c.bodytext LIKE '%identifier=download%' ORDER BY u.title, c.uid" ); $output->writeln(($apply ? 'APPLY' : 'DRY-RUN') . ' ยท ' . count($elements) . ' RTE download button(s)'); $datamap = []; $converted = 0; $hiddenCount = 0; $manual = []; foreach ($elements as $element) { $uid = (int)$element['uid']; $story = trim((string)($element['story_title'] ?? '')); $label = sprintf('#%d "%s"', $uid, $story !== '' ? $story : '- kein Usecase -'); if ($story === '') { $manual[] = $label . ': Element haengt an keinem Usecase'; continue; } // safety: the body must be essentially just the button $plain = trim((string)preg_replace('/\s+/', ' ', strip_tags((string)$element['bodytext']))); if (mb_strlen($plain) > 60) { $manual[] = $label . ': Bodytext enthaelt mehr als den Button (' . mb_strlen($plain) . ' Zeichen) - manuell'; continue; } $target = $this->resolveDownload($story, $downloads, $uidByTitle, $allNorm); if ($target === null) { if ((int)$element['hidden'] === 1) { $output->writeln(' = ' . $label . ': kein PDF im Pool, Element bereits versteckt'); continue; } $datamap['tt_content'][(string)$uid] = ['hidden' => 1]; $hiddenCount++; $output->writeln(' - ' . $label . ': kein PDF im Pool -> Element wird VERSTECKT'); continue; } $datamap['tt_content'][(string)$uid] = [ 'CType' => 'vitec_downloadcard', 'header' => 'Downloads', 'bodytext' => '', 'pi_flexform' => [ 'data' => ['sDEF' => ['lDEF' => [ 'settings.download' => ['vDEF' => (string)$target['uid']], 'settings.layout' => ['vDEF' => 'story_download'], ]]], ], ]; $converted++; $output->writeln(sprintf(' ~ %s -> vitec_downloadcard (download #%d "%s")', $label, $target['uid'], $target['title'])); } // follow-up pass: every story downloadcard gets the dedicated // "Success Story Download" layout (DataHandler merges the value // into the existing FlexForm) - covers cards converted before the // variant existed, including the interim `neutral` ones $layoutFixes = 0; $headerFixes = 0; foreach ($content->fetchAllAssociative( "SELECT uid, pi_flexform, header FROM tt_content WHERE deleted = 0 AND CType = 'vitec_downloadcard' AND tx_vitec_usecase_content > 0" ) as $card) { $cardId = (string)(int)$card['uid']; if (!str_contains((string)$card['pi_flexform'], 'story_download')) { $datamap['tt_content'][$cardId]['pi_flexform'] = ['data' => ['sDEF' => ['lDEF' => ['settings.layout' => ['vDEF' => 'story_download']]]]]; $layoutFixes++; } // the front end builds the story anchor nav from element // HEADERS - without one the section is missing from it // (found 2026-10-28, "Downloads" anchor) if (trim((string)$card['header']) === '') { $datamap['tt_content'][$cardId]['header'] = 'Downloads'; $headerFixes++; } } if ($layoutFixes > 0) { $output->writeln(' ~ ' . $layoutFixes . ' bestehende Story-Downloadcard(s): settings.layout -> story_download'); } if ($headerFixes > 0) { $output->writeln(' ~ ' . $headerFixes . ' bestehende Story-Downloadcard(s): header -> "Downloads" (Anker-Nav)'); } $output->writeln(sprintf("\n%d Konvertierung(en), %d versteckt, %d manuell:", $converted, $hiddenCount, count($manual))); foreach ($manual as $line) { $output->writeln(' ! ' . $line); } if (!$apply) { $output->writeln('DRY RUN - nothing written. Add --apply to write.'); return Command::SUCCESS; } if ($datamap === []) { $output->writeln('Nothing to change.'); return Command::SUCCESS; } $dataHandler = GeneralUtility::makeInstance(DataHandler::class); $dataHandler->start($datamap, []); $dataHandler->process_datamap(); if ($dataHandler->errorLog !== []) { $output->writeln('DataHandler: ' . implode(' | ', $dataHandler->errorLog) . ''); return Command::FAILURE; } $output->writeln('Done. Flush the frontend cache, then re-check a story payload.'); return Command::SUCCESS; } /** * @param array $downloads * @param array $uidByTitle * @return array{uid:int,title:string}|null unique match only */ private function resolveDownload(string $story, array $downloads, array $uidByTitle, array $allNorm = []): ?array { $storyNorm = $this->norm($story); $manualTitle = self::MANUAL_MAP[$storyNorm] ?? null; if ($manualTitle !== null && isset($uidByTitle[$manualTitle])) { return ['uid' => $uidByTitle[$manualTitle], 'title' => $manualTitle]; } $candidates = []; foreach ($downloads as $download) { $a = $download['norm']; if ($a !== '' && $storyNorm !== '' && ($a === $storyNorm || str_contains($a, $storyNorm) || str_contains($storyNorm, $a))) { $candidates[] = $download; } } if (count($candidates) === 1) { return ['uid' => $candidates[0]['uid'], 'title' => $candidates[0]['title']]; } // fallback: a download titled EXACTLY like the story, without any // document-type suffix ("Lord's Cricket Ground") - strict equality // only, so brochures and overviews cannot sneak in $exact = array_values(array_filter($allNorm, fn(array $d): bool => $d['norm'] === $storyNorm)); return count($exact) === 1 ? ['uid' => $exact[0]['uid'], 'title' => $exact[0]['title']] : null; } private function stripSuffix(string $title): ?string { foreach (['success story', 'use case', 'usecase'] as $suffix) { if (preg_match('/^(.*?)\s*' . preg_quote($suffix, '/') . '\s*$/i', trim($title), $matches)) { return trim($matches[1]); } } return null; } private function norm(string $value): string { $value = (string)preg_replace('/\s*\([^)]*\)/', ' ', $value); $value = (string)preg_replace('/^\s*(vitec|datapath)\s+/i', '', $value); return (string)preg_replace('/[^a-z0-9]+/', '', str_replace(['+', '&'], ['plus', 'and'], mb_strtolower($value))); } }