8 Commits
1.0.1 ... 1.0.9

4 changed files with 218 additions and 228 deletions

View File

@@ -1,7 +1,4 @@
<?php $this->extend('be_main'); ?> <form action="<?= $this->action; ?>" method="post" style="max-width:900px;">
<?php $this->block('main'); ?>
<form action="<?= $this->action; ?>" method="post" style="max-width:900px;">
<input type="hidden" name="REQUEST_TOKEN" value="<?= $this->requestToken; ?>"> <input type="hidden" name="REQUEST_TOKEN" value="<?= $this->requestToken; ?>">
<input type="hidden" name="FORM_SUBMIT" value="tl_dummy_copier"> <input type="hidden" name="FORM_SUBMIT" value="tl_dummy_copier">
@@ -11,13 +8,17 @@
.dc-tools { margin: 0.25rem 0 0.5rem; display: flex; gap: 0.5rem; } .dc-tools { margin: 0.25rem 0 0.5rem; display: flex; gap: 0.5rem; }
.dc-filter { width: 100%; margin: 0.25rem 0; } .dc-filter { width: 100%; margin: 0.25rem 0; }
.dc-button { padding: 0.15rem 0.45rem; } .dc-button { padding: 0.15rem 0.45rem; }
.dc-section { border: 1px solid #ccc; padding: 1rem; margin-bottom: 1.5rem; border-radius: 4px; }
.dc-section h3 { margin: 0 0 0.75rem; font-size: 1rem; font-weight: bold; }
.dc-hint { color: #666; font-size: 0.85rem; margin: 0.25rem 0 0.75rem; }
</style> </style>
<p> <!-- Abschnitt 1: Quell-Seiten -->
<label>Quell-Seiten (Mehrfachauswahl):<br> <div class="dc-section">
<?php if (!empty($this->sourcePagesWidget)): ?> <h3>1. Quell-Seiten auswaehlen</h3>
<?= $this->sourcePagesWidget; ?> <p class="dc-hint">Alle Artikel und Inhaltselemente der gewaehlten Seiten werden automatisch mitkopiert (sofern Option "inkl. Content" aktiv ist).</p>
<?php else: ?> <p>
<label>Quell-Seiten (Mehrfachauswahl):<br>
<input class="dc-filter" type="text" data-filter-for="sourcePages" placeholder="Seiten filtern..."> <input class="dc-filter" type="text" data-filter-for="sourcePages" placeholder="Seiten filtern...">
<span class="dc-tools"> <span class="dc-tools">
<button class="dc-button" type="button" data-select-all="sourcePages">Alle</button> <button class="dc-button" type="button" data-select-all="sourcePages">Alle</button>
@@ -25,99 +26,100 @@
</span> </span>
<select id="sourcePages" name="sourcePages[]" multiple size="12" style="width:100%;"> <select id="sourcePages" name="sourcePages[]" multiple size="12" style="width:100%;">
<?php foreach (($this->pageChoices ?? []) as $id => $label): ?> <?php foreach (($this->pageChoices ?? []) as $id => $label): ?>
<option value="<?= (int) $id; ?>" <?= in_array((int) $id, ($this->selected['sourcePages'] ?? []), true) ? 'selected' : ''; ?>><?= $this->specialchars((string) $label); ?></option> <option value="<?= (int) $id; ?>" <?= in_array((int) $id, ($this->selected['sourcePages'] ?? []), true) ? 'selected' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<?php endif; ?> </label>
</label> </p>
</p> </div>
<p> <!-- Abschnitt 2: Quell-Module -->
<label>Quell-Module (Mehrfachauswahl):<br> <div class="dc-section">
<input class="dc-filter" type="text" data-filter-for="sourceModules" placeholder="Module filtern..."> <h3>2. Quell-Module auswaehlen</h3>
<span class="dc-tools"> <p class="dc-hint">Ausgewaehlte Module werden kopiert; Referenzen in den kopierten Seiten werden automatisch auf die neuen Module umgebogen.</p>
<button class="dc-button" type="button" data-select-all="sourceModules">Alle</button> <p>
<button class="dc-button" type="button" data-select-none="sourceModules">Keine</button> <label>Module (Mehrfachauswahl):<br>
</span> <input class="dc-filter" type="text" data-filter-for="sourceModules" placeholder="Module filtern...">
<select id="sourceModules" name="sourceModules[]" multiple size="10" style="width:100%;"> <span class="dc-tools">
<?php foreach (($this->moduleChoices ?? []) as $id => $label): ?> <button class="dc-button" type="button" data-select-all="sourceModules">Alle</button>
<option value="<?= (int) $id; ?>" <?= in_array((int) $id, ($this->selected['sourceModules'] ?? []), true) ? 'selected' : ''; ?>><?= $this->specialchars((string) $label); ?></option> <button class="dc-button" type="button" data-select-none="sourceModules">Keine</button>
<?php endforeach; ?> </span>
</select> <select id="sourceModules" name="sourceModules[]" multiple size="10" style="width:100%;">
</label> <?php foreach (($this->moduleChoices ?? []) as $id => $label): ?>
</p> <option value="<?= (int) $id; ?>" <?= in_array((int) $id, ($this->selected['sourceModules'] ?? []), true) ? 'selected' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?>
</select>
</label>
</p>
</div>
<p> <!-- Abschnitt 3: Quell-Verzeichnisse -->
<label>Quell-Content (optional, Mehrfachauswahl):<br> <div class="dc-section">
<input class="dc-filter" type="text" data-filter-for="sourceContent" placeholder="Content filtern..."> <h3>3. Quell-Verzeichnisse auswaehlen (optional)</h3>
<span class="dc-tools"> <p class="dc-hint">Optionale Dateiverzeichnisse, die gespiegelt werden sollen.</p>
<button class="dc-button" type="button" data-select-all="sourceContent">Alle</button> <p>
<button class="dc-button" type="button" data-select-none="sourceContent">Keine</button> <label>Verzeichnisse (Mehrfachauswahl):<br>
</span>
<select id="sourceContent" name="sourceContent[]" multiple size="10" style="width:100%;">
<?php foreach (($this->contentChoices ?? []) as $id => $label): ?>
<option value="<?= (int) $id; ?>" <?= in_array((int) $id, ($this->selected['sourceContent'] ?? []), true) ? 'selected' : ''; ?>><?= $this->specialchars((string) $label); ?></option>
<?php endforeach; ?>
</select>
</label>
</p>
<p>
<label>Quell-Verzeichnisse (optional, Mehrfachauswahl):<br>
<?php if (!empty($this->sourceDirectoriesWidget)): ?>
<?= $this->sourceDirectoriesWidget; ?>
<?php else: ?>
<input class="dc-filter" type="text" data-filter-for="sourceDirectories" placeholder="Verzeichnisse filtern..."> <input class="dc-filter" type="text" data-filter-for="sourceDirectories" placeholder="Verzeichnisse filtern...">
<span class="dc-tools"> <span class="dc-tools">
<button class="dc-button" type="button" data-select-all="sourceDirectories">Alle</button> <button class="dc-button" type="button" data-select-all="sourceDirectories">Alle</button>
<button class="dc-button" type="button" data-select-none="sourceDirectories">Keine</button> <button class="dc-button" type="button" data-select-none="sourceDirectories">Keine</button>
</span> </span>
<select id="sourceDirectories" name="sourceDirectories[]" multiple size="10" style="width:100%;"> <select id="sourceDirectories" name="sourceDirectories[]" multiple size="8" style="width:100%;">
<?php foreach (($this->directoryChoices ?? []) as $path => $label): ?> <?php foreach (($this->directoryChoices ?? []) as $path => $label): ?>
<option value="<?= $this->specialchars((string) $path); ?>" <?= in_array((string) $path, ($this->selected['sourceDirectories'] ?? []), true) ? 'selected' : ''; ?>><?= $this->specialchars((string) $label); ?></option> <option value="<?= htmlspecialchars((string) $path, ENT_QUOTES, 'UTF-8'); ?>" <?= in_array((string) $path, ($this->selected['sourceDirectories'] ?? []), true) ? 'selected' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<?php endif; ?> </label>
</label> </p>
</p> </div>
<p> <!-- Abschnitt 4: Kopieroptionen -->
<label>Ziel-Elternseite:<br> <div class="dc-section">
<?php if (!empty($this->targetParentPageWidget)): ?> <h3>4. Kopieroptionen</h3>
<?= $this->targetParentPageWidget; ?> <p><label><input type="checkbox" name="includeContent" value="1" <?= ($this->selected['includeContent'] ?? true) ? 'checked' : ''; ?>> Artikel &amp; Inhaltselemente der Seiten mitkopieren</label></p>
<?php else: ?> <p><label><input type="checkbox" name="copyModules" value="1" <?= ($this->selected['copyModules'] ?? true) ? 'checked' : ''; ?>> Module kopieren und in den kopierten Seiten neu verlinken</label></p>
<p><label><input type="checkbox" name="copyDirectories" value="1" <?= ($this->selected['copyDirectories'] ?? false) ? 'checked' : ''; ?>> Ausgewaehlte Verzeichnisse in Ziel-Verzeichnis kopieren</label></p>
<p><label><input type="checkbox" name="dryRun" value="1" <?= ($this->selected['dryRun'] ?? false) ? 'checked' : ''; ?>> Dry-Run (nur Vorschau, keine Schreibzugriffe)</label></p>
</div>
<!-- Abschnitt 5: Ziel -->
<div class="dc-section">
<h3>5. Ziel &amp; Benennung</h3>
<p>
<label>Ziel-Elternseite (Pflichtfeld):<br>
<select name="targetParentPage" required style="width:100%;"> <select name="targetParentPage" required style="width:100%;">
<option value="">Bitte waehlen</option> <option value="">Bitte waehlen</option>
<?php foreach (($this->pageChoices ?? []) as $id => $label): ?> <?php foreach (($this->pageChoices ?? []) as $id => $label): ?>
<option value="<?= (int) $id; ?>" <?= ((int) ($this->selected['targetParentPage'] ?? 0) === (int) $id) ? 'selected' : ''; ?>><?= $this->specialchars((string) $label); ?></option> <option value="<?= (int) $id; ?>" <?= ((int) ($this->selected['targetParentPage'] ?? 0) === (int) $id) ? 'selected' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<?php endif; ?> </label>
</label> </p>
</p> <p>
<label>Ziel-Verzeichnis fuer Dateien (z. B. files/kunden/kunde-x):<br>
<p><label>Ziel-Artikel ID (nur fuer einzelne Content-IDs):<br><input type="number" name="targetArticle" min="0" value="<?= (int) ($this->selected['targetArticle'] ?? 0); ?>"></label></p> <input type="text" name="targetDirectory" style="width:100%" placeholder="files/kunden/mein-kunde" value="<?= htmlspecialchars((string) ($this->selected['targetDirectory'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>">
<p><label>Ziel-Verzeichnis (z. B. files/kunden/kunde-x):<br><input type="text" name="targetDirectory" style="width:100%" value="<?= $this->specialchars((string) ($this->selected['targetDirectory'] ?? '')); ?>"></label></p> </label>
<p><label>Praefix fuer Titel/Name/Alias:<br><input type="text" name="namePrefix" placeholder="kunde-x-" value="<?= $this->specialchars((string) ($this->selected['namePrefix'] ?? '')); ?>"></label></p> </p>
<p>
<p><label><input type="checkbox" name="includeContent" value="1" checked> Seiten inkl. Artikel/Content kopieren</label></p> <label>Praefix fuer Titel / Name / Alias der Kopien:<br>
<p><label><input type="checkbox" name="copyModules" value="1" checked> Module kopieren und neu verlinken</label></p> <input type="text" name="namePrefix" style="width:100%" placeholder="z. B. kunde-x-" value="<?= htmlspecialchars((string) ($this->selected['namePrefix'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>">
<p><label><input type="checkbox" name="copyDirectories" value="1"> Verzeichnisse kopieren</label></p> </label>
<p><label><input type="checkbox" name="dryRun" value="1"> Dry-Run (keine Schreibzugriffe)</label></p> </p>
</div>
<p><button class="tl_submit" type="submit">Ausfuehren</button></p> <p><button class="tl_submit" type="submit">Ausfuehren</button></p>
<?php if (isset($this->result) && \is_object($this->result)): ?> <?php if (isset($this->result) && \is_object($this->result)): ?>
<h3>Ergebnis</h3> <h3>Ergebnis</h3>
<pre><?= json_encode([ <pre><?= htmlspecialchars(json_encode([
'copiedPages' => $this->result->copiedPages, 'copiedPages' => $this->result->copiedPages,
'copiedModules' => $this->result->copiedModules, 'copiedModules' => $this->result->copiedModules,
'copiedContent' => $this->result->copiedContent, 'copiedContent' => $this->result->copiedContent,
'copiedDirectories' => $this->result->copiedDirectories, 'copiedDirectories' => $this->result->copiedDirectories,
'pageMap' => $this->result->pageMap, 'pageMap' => $this->result->pageMap,
'moduleMap' => $this->result->moduleMap, 'moduleMap' => $this->result->moduleMap,
'notes' => $this->result->notes, 'notes' => $this->result->notes,
], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); ?></pre> ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES), ENT_QUOTES, 'UTF-8'); ?></pre>
<?php endif; ?> <?php endif; ?>
<script> <script>
@@ -127,11 +129,7 @@
document.querySelectorAll('[data-filter-for]').forEach(function (input) { document.querySelectorAll('[data-filter-for]').forEach(function (input) {
input.addEventListener('input', function () { input.addEventListener('input', function () {
var select = byId(input.getAttribute('data-filter-for')); var select = byId(input.getAttribute('data-filter-for'));
if (!select) { return; }
if (!select) {
return;
}
var query = (input.value || '').toLowerCase(); var query = (input.value || '').toLowerCase();
Array.prototype.forEach.call(select.options, function (option) { Array.prototype.forEach.call(select.options, function (option) {
option.hidden = query !== '' && option.text.toLowerCase().indexOf(query) === -1; option.hidden = query !== '' && option.text.toLowerCase().indexOf(query) === -1;
@@ -142,15 +140,9 @@
document.querySelectorAll('[data-select-all]').forEach(function (button) { document.querySelectorAll('[data-select-all]').forEach(function (button) {
button.addEventListener('click', function () { button.addEventListener('click', function () {
var select = byId(button.getAttribute('data-select-all')); var select = byId(button.getAttribute('data-select-all'));
if (!select) { return; }
if (!select) {
return;
}
Array.prototype.forEach.call(select.options, function (option) { Array.prototype.forEach.call(select.options, function (option) {
if (!option.hidden) { if (!option.hidden) { option.selected = true; }
option.selected = true;
}
}); });
}); });
}); });
@@ -158,11 +150,7 @@
document.querySelectorAll('[data-select-none]').forEach(function (button) { document.querySelectorAll('[data-select-none]').forEach(function (button) {
button.addEventListener('click', function () { button.addEventListener('click', function () {
var select = byId(button.getAttribute('data-select-none')); var select = byId(button.getAttribute('data-select-none'));
if (!select) { return; }
if (!select) {
return;
}
Array.prototype.forEach.call(select.options, function (option) { Array.prototype.forEach.call(select.options, function (option) {
option.selected = false; option.selected = false;
}); });
@@ -170,5 +158,4 @@
}); });
})(); })();
</script> </script>
</form> </form>
<?php $this->endblock(); ?>

View File

@@ -8,14 +8,12 @@ use Webfarben\DummyCopier\Service\DummyCopier;
use Webfarben\DummyCopier\Service\DummyCopyOptions; use Webfarben\DummyCopier\Service\DummyCopyOptions;
use Contao\BackendModule; use Contao\BackendModule;
use Contao\Environment; use Contao\Environment;
use Contao\FileTree;
use Contao\Input; use Contao\Input;
use Contao\Message; use Contao\Message;
use Contao\PageTree;
use Contao\StringUtil; use Contao\StringUtil;
use Contao\System; use Contao\System;
use Contao\Widget;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use Symfony\Component\Filesystem\Filesystem;
class DummyCopierModule extends BackendModule class DummyCopierModule extends BackendModule
{ {
@@ -23,43 +21,42 @@ class DummyCopierModule extends BackendModule
protected function compile(): void protected function compile(): void
{ {
$connection = System::getContainer()->get(Connection::class); /** @var Connection $connection */
$connection = System::getContainer()->get('database_connection');
$this->Template->action = Environment::get('request'); $this->Template->action = Environment::get('request');
$this->Template->requestToken = \defined('REQUEST_TOKEN') ? REQUEST_TOKEN : ''; $this->Template->requestToken = $this->getCsrfToken();
$this->Template->pageChoices = $this->getPageChoices($connection); $this->Template->pageChoices = $this->getPageChoices($connection);
$this->Template->moduleChoices = $this->getModuleChoices($connection); $this->Template->moduleChoices = $this->getModuleChoices($connection);
$this->Template->contentChoices = $this->getContentChoices($connection);
$this->Template->directoryChoices = $this->getDirectoryChoices(); $this->Template->directoryChoices = $this->getDirectoryChoices();
$this->Template->sourcePagesWidget = '';
$this->Template->targetParentPageWidget = '';
$this->Template->sourceDirectoriesWidget = '';
$targetParentPageId = $this->parseSingleIdInput(Input::postRaw('targetParentPage')); $targetParentPageId = $this->parseSingleIdInput(Input::postRaw('targetParentPage'));
$isPost = Input::post('FORM_SUBMIT') === 'tl_dummy_copier';
$this->Template->selected = [ $this->Template->selected = [
'sourcePages' => $this->parseIdInput(Input::postRaw('sourcePages')), 'sourcePages' => $this->parseIdInput(Input::postRaw('sourcePages')),
'sourceModules' => $this->parseIdInput(Input::postRaw('sourceModules')), 'sourceModules' => $this->parseIdInput(Input::postRaw('sourceModules')),
'sourceContent' => $this->parseIdInput(Input::postRaw('sourceContent')),
'sourceDirectories' => $this->parsePathInput(Input::postRaw('sourceDirectories')), 'sourceDirectories' => $this->parsePathInput(Input::postRaw('sourceDirectories')),
'targetParentPage' => $targetParentPageId, 'targetParentPage' => $targetParentPageId,
'targetArticle' => (int) Input::post('targetArticle'),
'targetDirectory' => trim((string) Input::post('targetDirectory')), 'targetDirectory' => trim((string) Input::post('targetDirectory')),
'namePrefix' => trim((string) Input::post('namePrefix')), 'namePrefix' => trim((string) Input::post('namePrefix')),
'includeContent' => !$isPost || (bool) Input::post('includeContent'),
'copyModules' => !$isPost || (bool) Input::post('copyModules'),
'copyDirectories' => $isPost && (bool) Input::post('copyDirectories'),
'dryRun' => $isPost && (bool) Input::post('dryRun'),
]; ];
$this->prepareTreeWidgets(); if (!$isPost) {
if (Input::post('FORM_SUBMIT') !== 'tl_dummy_copier') {
return; return;
} }
$options = new DummyCopyOptions( $options = new DummyCopyOptions(
$this->parseIdInput(Input::postRaw('sourcePages')), $this->parseIdInput(Input::postRaw('sourcePages')),
$this->parseIdInput(Input::postRaw('sourceModules')), $this->parseIdInput(Input::postRaw('sourceModules')),
$this->parseIdInput(Input::postRaw('sourceContent')), [],
$this->parsePathInput(Input::postRaw('sourceDirectories')), $this->parsePathInput(Input::postRaw('sourceDirectories')),
$targetParentPageId, $targetParentPageId,
(int) Input::post('targetArticle'), 0,
trim((string) Input::post('targetDirectory')), trim((string) Input::post('targetDirectory')),
trim((string) Input::post('namePrefix')), trim((string) Input::post('namePrefix')),
(bool) Input::post('includeContent'), (bool) Input::post('includeContent'),
@@ -69,8 +66,11 @@ class DummyCopierModule extends BackendModule
); );
try { try {
/** @var DummyCopier $copier */ $copier = new DummyCopier(
$copier = System::getContainer()->get(DummyCopier::class); $connection,
new Filesystem(),
(string) System::getContainer()->getParameter('kernel.project_dir')
);
$result = $copier->execute($options); $result = $copier->execute($options);
Message::addConfirmation(sprintf( Message::addConfirmation(sprintf(
@@ -138,85 +138,24 @@ class DummyCopierModule extends BackendModule
return $ids[0] ?? 0; return $ids[0] ?? 0;
} }
private function prepareTreeWidgets(): void private function getCsrfToken(): string
{ {
if (!class_exists(PageTree::class) || !class_exists(FileTree::class) || !class_exists(Widget::class)) { $container = System::getContainer();
return;
// Contao 5: use Symfony CSRF token manager
if ($container->has('contao.csrf.token_manager')) {
return $container
->get('contao.csrf.token_manager')
->getToken((string) $container->getParameter('contao.csrf_token_name'))
->getValue();
} }
try { // Contao 4 fallback
$selectedPages = $this->parseIdInput(Input::postRaw('sourcePages')); if (\defined('REQUEST_TOKEN')) {
$selectedParent = (int) Input::post('targetParentPage'); return REQUEST_TOKEN;
$selectedDirectories = $this->parsePathInput(Input::postRaw('sourceDirectories'));
$this->Template->sourcePagesWidget = $this->renderPageTreeWidget(
'sourcePages',
'Quell-Seiten (pageTree)',
$selectedPages,
true
);
$this->Template->targetParentPageWidget = $this->renderPageTreeWidget(
'targetParentPage',
'Ziel-Elternseite (pageTree)',
$selectedParent > 0 ? [$selectedParent] : [],
false
);
$this->Template->sourceDirectoriesWidget = $this->renderFileTreeWidget(
'sourceDirectories',
'Quell-Verzeichnisse (fileTree)',
$selectedDirectories
);
} catch (\Throwable $exception) {
// If widget rendering differs by Contao version, the module falls back to select boxes.
$this->Template->sourcePagesWidget = '';
$this->Template->targetParentPageWidget = '';
$this->Template->sourceDirectoriesWidget = '';
Message::addInfo('Tree-Widgets konnten nicht initialisiert werden, Fallback-Auswahl wird verwendet.');
} }
}
private function renderPageTreeWidget(string $name, string $label, array $value, bool $multiple): string return '';
{
$attributes = Widget::getAttributesFromDca([
'inputType' => 'pageTree',
'label' => [$label, ''],
'eval' => [
'fieldType' => $multiple ? 'checkbox' : 'radio',
'multiple' => $multiple,
'tl_class' => 'clr',
],
], $name, $value, $name, 'tl_dummy_copier');
$attributes['id'] = $name;
$attributes['name'] = $name;
$widget = new PageTree($attributes);
return $widget->generate();
}
private function renderFileTreeWidget(string $name, string $label, array $value): string
{
$attributes = Widget::getAttributesFromDca([
'inputType' => 'fileTree',
'label' => [$label, ''],
'eval' => [
'fieldType' => 'checkbox',
'filesOnly' => false,
'files' => false,
'multiple' => true,
'tl_class' => 'clr',
],
], $name, $value, $name, 'tl_dummy_copier');
$attributes['id'] = $name;
$attributes['name'] = $name;
$widget = new FileTree($attributes);
return $widget->generate();
} }
/** /**
@@ -285,7 +224,12 @@ class DummyCopierModule extends BackendModule
*/ */
private function getModuleChoices(Connection $connection): array private function getModuleChoices(Connection $connection): array
{ {
$rows = $connection->fetchAllAssociative('SELECT id, name, type FROM tl_module ORDER BY id'); $rows = $connection->fetchAllAssociative(
'SELECT m.id, m.name, m.type, t.name AS theme_name
FROM tl_module m
LEFT JOIN tl_theme t ON t.id = m.pid
ORDER BY t.name, m.type, m.name'
);
$choices = []; $choices = [];
foreach ($rows as $row) { foreach ($rows as $row) {
@@ -297,38 +241,14 @@ class DummyCopierModule extends BackendModule
$name = trim((string) ($row['name'] ?? 'Modul ' . $id)); $name = trim((string) ($row['name'] ?? 'Modul ' . $id));
$type = trim((string) ($row['type'] ?? '')); $type = trim((string) ($row['type'] ?? ''));
$label = $type !== '' ? sprintf('%s (%s)', $name, $type) : $name; $theme = trim((string) ($row['theme_name'] ?? ''));
$label = $theme !== '' ? sprintf('[%s] %s (%s)', $theme, $name, $type) : sprintf('%s (%s)', $name, $type);
$choices[$id] = sprintf('%s [ID %d]', $label, $id); $choices[$id] = sprintf('%s [ID %d]', $label, $id);
} }
return $choices; return $choices;
} }
/**
* @return array<int,string>
*/
private function getContentChoices(Connection $connection): array
{
$rows = $connection->fetchAllAssociative('SELECT id, type, pid, headline FROM tl_content ORDER BY id');
$choices = [];
foreach ($rows as $row) {
$id = (int) ($row['id'] ?? 0);
if ($id < 1) {
continue;
}
$type = trim((string) ($row['type'] ?? 'content'));
$pid = (int) ($row['pid'] ?? 0);
$headline = $this->normalizeHeadline($row['headline'] ?? null);
$label = $headline !== '' ? sprintf('%s: %s', $type, $headline) : $type;
$choices[$id] = sprintf('%s [ID %d, Artikel %d]', $label, $id, $pid);
}
return $choices;
}
/** /**
* @return array<string,string> * @return array<string,string>
*/ */

View File

@@ -8,4 +8,8 @@ use Symfony\Component\HttpKernel\Bundle\Bundle;
class DummyCopierBundle extends Bundle class DummyCopierBundle extends Bundle
{ {
public function getPath(): string
{
return \dirname(__DIR__);
}
} }

View File

@@ -149,11 +149,24 @@ final class DummyCopier
$newArticleId = $this->insertRow('tl_article', $articleRow); $newArticleId = $this->insertRow('tl_article', $articleRow);
$contentIds = $this->connection->fetchFirstColumn('SELECT id FROM tl_content WHERE ptable = ? AND pid = ? ORDER BY sorting', ['tl_article', (int) $articleId]); $this->copyArticleContentTree((int) $articleId, $newArticleId, $moduleMap, $result);
}
}
foreach ($contentIds as $contentId) { /**
$this->copySingleContent((int) $contentId, $newArticleId, $moduleMap, $result); * Copies top-level article content and all nested child elements recursively.
} *
* @param array<int,int> $moduleMap
*/
private function copyArticleContentTree(int $sourceArticleId, int $targetArticleId, array $moduleMap, DummyCopyResult $result): void
{
$contentIds = $this->connection->fetchFirstColumn(
'SELECT id FROM tl_content WHERE ptable = ? AND pid = ? ORDER BY sorting',
['tl_article', $sourceArticleId]
);
foreach ($contentIds as $contentId) {
$this->copyContentRecursive((int) $contentId, 'tl_article', $targetArticleId, $moduleMap, $result);
} }
} }
@@ -161,19 +174,29 @@ final class DummyCopier
* @param array<int,int> $moduleMap * @param array<int,int> $moduleMap
*/ */
private function copySingleContent(int $sourceContentId, int $targetArticleId, array $moduleMap, DummyCopyResult $result): void private function copySingleContent(int $sourceContentId, int $targetArticleId, array $moduleMap, DummyCopyResult $result): void
{
$this->copyContentRecursive($sourceContentId, 'tl_article', $targetArticleId, $moduleMap, $result);
}
/**
* Recursively copies one content element and its children.
*
* @param array<int,int> $moduleMap
*/
private function copyContentRecursive(int $sourceContentId, string $targetPtable, int $targetPid, array $moduleMap, DummyCopyResult $result): int
{ {
$contentRow = $this->fetchRow('tl_content', $sourceContentId); $contentRow = $this->fetchRow('tl_content', $sourceContentId);
if ($contentRow === null) { if ($contentRow === null) {
$result->addNote(sprintf('Content %d nicht gefunden, wurde uebersprungen.', $sourceContentId)); $result->addNote(sprintf('Content %d nicht gefunden, wurde uebersprungen.', $sourceContentId));
return; return 0;
} }
unset($contentRow['id']); unset($contentRow['id']);
$contentRow['pid'] = $targetArticleId; $contentRow['pid'] = $targetPid;
$contentRow['ptable'] = 'tl_article'; $contentRow['ptable'] = $targetPtable;
$contentRow['tstamp'] = time(); $contentRow['tstamp'] = time();
$contentRow['sorting'] = $this->nextSorting('tl_content', 'pid', $targetArticleId, 'ptable', 'tl_article'); $contentRow['sorting'] = $this->nextSorting('tl_content', 'pid', $targetPid, 'ptable', $targetPtable);
if (($contentRow['type'] ?? '') === 'module') { if (($contentRow['type'] ?? '') === 'module') {
$oldModule = (int) ($contentRow['module'] ?? 0); $oldModule = (int) ($contentRow['module'] ?? 0);
@@ -190,6 +213,17 @@ final class DummyCopier
if (isset($contentRow['jumpTo'], $result->pageMap[(int) $contentRow['jumpTo']])) { if (isset($contentRow['jumpTo'], $result->pageMap[(int) $contentRow['jumpTo']])) {
$this->connection->update('tl_content', ['jumpTo' => $result->pageMap[(int) $contentRow['jumpTo']]], ['id' => $newContentId]); $this->connection->update('tl_content', ['jumpTo' => $result->pageMap[(int) $contentRow['jumpTo']]], ['id' => $newContentId]);
} }
$childIds = $this->connection->fetchFirstColumn(
'SELECT id FROM tl_content WHERE ptable = ? AND pid = ? ORDER BY sorting',
['tl_content', $sourceContentId]
);
foreach ($childIds as $childId) {
$this->copyContentRecursive((int) $childId, 'tl_content', $newContentId, $moduleMap, $result);
}
return $newContentId;
} }
private function rewriteReferences(DummyCopyResult $result): void private function rewriteReferences(DummyCopyResult $result): void
@@ -342,10 +376,55 @@ final class DummyCopier
} }
$articlePlaceholders = implode(',', array_fill(0, \count($articleIds), '?')); $articlePlaceholders = implode(',', array_fill(0, \count($articleIds), '?'));
$rootContentIds = $this->connection->fetchFirstColumn(
return $count + (int) $this->connection->fetchOne( sprintf('SELECT id FROM tl_content WHERE ptable = ? AND pid IN (%s)', $articlePlaceholders),
sprintf('SELECT COUNT(*) FROM tl_content WHERE ptable = ? AND pid IN (%s)', $articlePlaceholders),
array_merge(['tl_article'], array_map('intval', $articleIds)) array_merge(['tl_article'], array_map('intval', $articleIds))
); );
if ($rootContentIds === []) {
return $count;
}
return $count + $this->countContentTree(array_map('intval', $rootContentIds));
}
/**
* Counts content roots and all nested descendants.
*
* @param array<int,int> $rootIds
*/
private function countContentTree(array $rootIds): int
{
if ($rootIds === []) {
return 0;
}
$seen = [];
$queue = array_values(array_filter($rootIds, static fn (int $id): bool => $id > 0));
while ($queue !== []) {
$currentBatch = [];
foreach ($queue as $id) {
if (!isset($seen[$id])) {
$seen[$id] = true;
$currentBatch[] = $id;
}
}
if ($currentBatch === []) {
break;
}
$placeholders = implode(',', array_fill(0, \count($currentBatch), '?'));
$children = $this->connection->fetchFirstColumn(
sprintf('SELECT id FROM tl_content WHERE ptable = ? AND pid IN (%s)', $placeholders),
array_merge(['tl_content'], $currentBatch)
);
$queue = array_map('intval', $children);
}
return \count($seen);
} }
} }