2 Commits
1.0.6 ... 1.0.8

2 changed files with 117 additions and 126 deletions

View File

@@ -8,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,96 +29,97 @@
<option value="<?= (int) $id; ?>" <?= in_array((int) $id, ($this->selected['sourcePages'] ?? []), true) ? 'selected' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></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' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></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' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></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="<?= htmlspecialchars((string) $path, ENT_QUOTES, 'UTF-8'); ?>" <?= in_array((string) $path, ($this->selected['sourceDirectories'] ?? []), true) ? 'selected' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></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' : ''; ?>><?= htmlspecialchars((string) $label, ENT_QUOTES, 'UTF-8'); ?></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="<?= htmlspecialchars((string) ($this->selected['targetDirectory'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"></label></p> </label>
<p><label>Praefix fuer Titel/Name/Alias:<br><input type="text" name="namePrefix" placeholder="kunde-x-" value="<?= htmlspecialchars((string) ($this->selected['namePrefix'] ?? ''), ENT_QUOTES, 'UTF-8'); ?>"></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>
@@ -124,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;
@@ -139,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;
}
}); });
}); });
}); });
@@ -155,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;
}); });

View File

@@ -25,39 +25,38 @@ class DummyCopierModule extends BackendModule
$connection = System::getContainer()->get('database_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'),
]; ];
if (Input::post('FORM_SUBMIT') !== 'tl_dummy_copier') { if (!$isPost) {
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'),
@@ -139,6 +138,26 @@ class DummyCopierModule extends BackendModule
return $ids[0] ?? 0; return $ids[0] ?? 0;
} }
private function getCsrfToken(): string
{
$container = System::getContainer();
// 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();
}
// Contao 4 fallback
if (\defined('REQUEST_TOKEN')) {
return REQUEST_TOKEN;
}
return '';
}
/** /**
* @return array<int,string> * @return array<int,string>
*/ */
@@ -205,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) {
@@ -217,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>
*/ */