Add Turnstile protection and harden export workflow

This commit is contained in:
2026-07-22 08:54:31 +02:00
parent 67c7b5ccff
commit 4b9a5d7e78
17 changed files with 1572 additions and 276 deletions
+161 -69
View File
@@ -1,5 +1,9 @@
<?php
require_once __DIR__ . '/app.php';
app_send_security_headers();
include "zefixAPI.php";
$turnstileSiteKey = app_turnstile_is_configured() ? app_turnstile_site_key() : '';
$formStartedToken = app_create_form_started_token();
?>
<!doctype html>
<html lang="de">
@@ -8,9 +12,12 @@ include "zefixAPI.php";
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap first, then your overrides -->
<link rel="stylesheet" href="css/bootstrap.css">
<link rel="stylesheet" href="css/custom.css?v=1.3">
<link rel="stylesheet" href="css/custom.css?v=2.2">
<?php if ($turnstileSiteKey !== ''): ?>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<?php endif; ?>
<title>Silias Zefix Export</title>
@@ -41,14 +48,28 @@ include "zefixAPI.php";
}
.gemeindeeintrag { margin-bottom: .25rem; }
.bot-trap {
position: absolute !important;
left: -10000px !important;
width: 1px !important;
height: 1px !important;
overflow: hidden !important;
}
</style>
</head>
<body class="bg-light">
<nav class="navbar navbar-light bg-white border-bottom">
<nav class="navbar navbar-light site-header border-bottom py-2">
<div class="container">
<span class="navbar-brand mb-0 h1">Silias • ZEFIX Export</span>
<a class="brand-lockup" href="index.php" aria-label="Silias Zefix Export Startseite">
<span class="brand-logo-crop" aria-hidden="true">
<img src="img/silias-logo.png" alt="">
</span>
<span class="brand-divider" aria-hidden="true"></span>
<span class="brand-product">Zefix Export<small>Ein kostenloses Silias-Tool</small></span>
</a>
</div>
</nav>
@@ -56,53 +77,73 @@ include "zefixAPI.php";
<div class="row justify-content-center">
<div class="col-lg-10 col-xl-9">
<div class="card shadow-sm">
<div class="mb-4 mb-md-5">
<div class="hero-eyebrow mb-2">Zentraler Firmenindex</div>
<h1 class="display-6 fw-bold hero-title mb-3">Firmendaten gezielt auswählen und als CSV exportieren</h1>
<p class="lead hero-copy mb-0">
Filtern Sie Unternehmen nach Firmenname, Ort und Rechtsform. Sobald der Export bereit ist,
erhalten Sie einen sicheren Download-Link per E-Mail.
</p>
</div>
<div class="card export-shell shadow-sm">
<div class="card-body p-4 p-md-5">
<div class="d-flex flex-column flex-md-row align-items-md-start justify-content-between gap-2 mb-3">
<div>
<h1 class="h3 mb-1">Silias Zefix Export</h1>
<p class="text-muted mb-0">
Exportiere Firmen als Excel (csv) aus dem Zentralen Firmenindex (ZEFIX).
</p>
</div>
<span class="badge bg-primary-subtle text-primary border border-primary-subtle align-self-md-center">
Export via E-Mail Link
</span>
</div>
<hr class="my-4">
<form action="submit.php" method="post" class="needs-validation" novalidate>
<!-- Firmenname -->
<div class="mb-4">
<label for="firma" class="form-label">Firmenname (optional)</label>
<div class="form-text mb-2">* kann als Platzhalter verwendet werden</div>
<input type="text" id="firma" name="firma" class="form-control" placeholder="z.B. *solar*">
<input type="hidden" name="form_started" value="<?php echo htmlspecialchars($formStartedToken, ENT_QUOTES); ?>">
<div class="bot-trap" aria-hidden="true" hidden>
<label for="website">Website</label>
<input type="text" id="website" name="website" value="" tabindex="-1" autocomplete="off">
</div>
<!-- Kanton -->
<div class="mb-4">
<label class="form-label">Kanton</label>
<div class="alert alert-warning py-2 mb-2" role="alert" style="font-size: .95rem;">
Die Kantonsauswahl wird vom Export ignoriert und dient nur zum Filtern der Ortschaften.
Für Export nach Kanton: gewünschte Kantone wählen und dann bei Ortschaften auf
<b>Alle Ergebnisse auswählen</b> klicken.
<div class="form-section mb-3">
<div class="section-heading">
<span class="section-number" aria-hidden="true">1</span>
<div>
<h2>Firmenname</h2>
<p>Optional: Schränken Sie den Export auf passende Firmennamen ein.</p>
</div>
</div>
<div id="kantonauswahl" class="scrollWindow" onclick="filterFunction()"></div>
<label for="firma" class="form-label">Firmenname oder Suchmuster</label>
<input type="text" id="firma" name="firma" class="form-control" placeholder="Zum Beispiel *solar*" maxlength="200">
<div class="form-text mt-2">Das Sternchen (*) kann als Platzhalter verwendet werden.</div>
</div>
<!-- Sitz / Ort -->
<div class="mb-4">
<label class="form-label">Sitz (Ort)</label>
<div class="form-section mb-3">
<div class="section-heading">
<span class="section-number" aria-hidden="true">2</span>
<div>
<h2>Ort auswählen</h2>
<p>Filtern Sie zuerst nach Kanton oder Ortsname und übernehmen Sie danach die gewünschten Orte.</p>
</div>
</div>
<div class="alert alert-primary border-0 py-2 mb-3" role="note">
<strong>Hinweis:</strong> Die Kantone filtern nur die Ortsliste. Klicken Sie danach auf
<strong>«Gefilterte Orte auswählen»</strong>, damit die Orte in den Export übernommen werden.
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-2">
<label class="form-label mb-0">Kanton</label>
<span class="selection-meta" id="cantonCount">0 ausgewählt</span>
</div>
<div id="kantonauswahl" class="scrollWindow" onclick="filterFunction()"></div>
</div>
<div>
<div class="d-flex justify-content-between align-items-center mb-2">
<label for="sitzInput" class="form-label mb-0">Sitz (Ort)</label>
<span class="selection-meta" id="seatCount">0 ausgewählt</span>
</div>
<div class="card border-0 bg-white">
<div class="card-body p-0">
<div class="d-flex flex-column flex-lg-row gap-2 mb-2">
<div class="flex-grow-1">
<input type="text"
placeholder="Suchen (Ort)"
placeholder="Ort suchen"
id="sitzInput"
onkeyup="filterFunction()"
onchange="filterFunction()"
@@ -112,7 +153,7 @@ include "zefixAPI.php";
<div class="d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary button-small" onclick="gefundeneGemeindenAuswählen()">
Alle Ergebnisse auswählen
Gefilterte Orte auswählen
</button>
<button type="button" class="btn btn-outline-secondary button-small" onclick="ausgewählteGemeindenLöschen()">
Auswahl löschen
@@ -121,7 +162,7 @@ include "zefixAPI.php";
Alle anzeigen
</button>
<button type="button" class="btn btn-outline-secondary button-small" onclick="nurAusgewählteGemeindenAnzeigen()">
Nur ausgewählte
Nur ausgewählte anzeigen
</button>
</div>
</div>
@@ -130,25 +171,43 @@ include "zefixAPI.php";
</div>
</div>
</div>
</div>
<!-- Rechtsform -->
<div class="mb-4">
<label class="form-label">Rechtsform</label>
<div class="form-section mb-3">
<div class="section-heading">
<span class="section-number" aria-hidden="true">3</span>
<div>
<h2>Rechtsform auswählen</h2>
<p>Wählen Sie mindestens eine Rechtsform für den Export.</p>
</div>
</div>
<div class="mb-2">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
<div>
<button type="button" class="btn btn-outline-primary button-small" onclick="alleRechtsformenAuswählen()">
Alle auswählen
</button>
<button type="button" class="btn btn-outline-secondary button-small" onclick="rechtsformenLöschen()">
Auswahl löschen
</button>
</div>
<span class="selection-meta" id="legalFormCount">0 ausgewählt</span>
</div>
<div id="rechtsformenauswahl" class="scrollWindow"></div>
</div>
<!-- Optionen -->
<div class="mb-4">
<div class="form-section mb-3">
<div class="section-heading">
<span class="section-number" aria-hidden="true">4</span>
<div>
<h2>Export anfordern</h2>
<p>Geben Sie die Zieladresse an und starten Sie den geschützten Export.</p>
</div>
</div>
<div class="mb-4">
<div class="form-label mb-2">Weitere Optionen</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="exakteSuche" name="exakteSuche" disabled>
<label class="form-check-label" for="exakteSuche">Exakte Suche (noch in Entwicklung)</label>
@@ -168,23 +227,43 @@ include "zefixAPI.php";
<input class="form-check-input" type="checkbox" id="phonetischeSuche" name="phonetischeSuche" disabled>
<label class="form-check-label" for="phonetischeSuche">Phonetische Suche (noch in Entwicklung)</label>
</div>
</div>
</div>
<!-- Email -->
<div class="mb-4">
<label for="email" class="form-label">Ihre E-Mail Adresse</label>
<label for="email" class="form-label">Ihre E-Mail-Adresse</label>
<div class="form-text mb-2">
Das Aufbereiten der Daten kann länger dauern. Sie erhalten einen Download-Link per E-Mail sobald die Daten bereit sind.
Die Aufbereitung kann einige Zeit dauern. Sie erhalten den Download-Link per E-Mail, sobald die Daten bereit sind.
</div>
<input type="email" id="email" name="email" class="form-control" required placeholder="name@firma.ch">
<div class="invalid-feedback">Bitte eine gültige E-Mail Adresse eingeben.</div>
<div class="invalid-feedback">Bitte geben Sie eine gültige E-Mail-Adresse ein.</div>
</div>
<?php if ($turnstileSiteKey !== ''): ?>
<div class="privacy-note mb-3">
<div class="cf-turnstile"
data-sitekey="<?php echo htmlspecialchars($turnstileSiteKey, ENT_QUOTES); ?>"
data-action="turnstile-spin-v2"
data-theme="auto"
data-language="de"
data-feedback-enabled="false"></div>
<div class="form-text mt-2">
Der Missbrauchsschutz verarbeitet technisch notwendige Browser- und Verbindungsdaten.
Details finden Sie in der <a href="datenschutz.php">Datenschutzerklärung</a>.
</div>
</div>
<?php else: ?>
<div class="alert alert-warning" role="alert">
Der Export ist vorübergehend deaktiviert, da der Missbrauchsschutz noch nicht konfiguriert ist.
</div>
<?php endif; ?>
<div class="d-grid">
<button type="submit" class="btn btn-primary btn-lg">Export starten</button>
<button type="submit" class="btn btn-primary btn-lg"<?php echo $turnstileSiteKey === '' ? ' disabled' : ''; ?>>Export starten</button>
</div>
<div id="selectionValidationMessage" class="text-danger mt-2" style="display:none;">
Wählen Sie mindestens einen Ort und eine Rechtsform
Wählen Sie mindestens einen Ort und eine Rechtsform aus.
</div>
</div>
</form>
@@ -233,9 +312,11 @@ include "zefixAPI.php";
input.type = "checkbox";
input.name = checkboxName;
input.value = checkboxValue;
input.id = checkboxName.replace(/[^a-z0-9]/gi, '-') + '-' + String(checkboxValue).replace(/[^a-z0-9]/gi, '-');
const label = document.createElement('label');
label.className = "form-check-label";
label.htmlFor = input.id;
label.textContent = labelText;
wrapper.appendChild(input);
@@ -305,9 +386,11 @@ include "zefixAPI.php";
input.type = "checkbox";
input.name = "sitze[]";
input.value = bfsId;
input.id = "sitz-" + bfsId;
const label = document.createElement('label');
label.className = "form-check-label";
label.htmlFor = input.id;
label.textContent = " " + gemeindeName;
wrapper.appendChild(input);
@@ -319,6 +402,22 @@ include "zefixAPI.php";
const sitzInput = document.getElementById('sitzInput');
function updateSelectionCounts() {
const cantonTotal = document.querySelectorAll('input[name="kantone[]"]:checked').length;
const seatTotal = document.querySelectorAll('input[name="sitze[]"]:checked').length;
const legalFormTotal = document.querySelectorAll('input[name="rechtsformen[]"]:checked').length;
document.getElementById('cantonCount').textContent = cantonTotal + ' ausgewählt';
document.getElementById('seatCount').textContent = seatTotal + ' ausgewählt';
document.getElementById('legalFormCount').textContent = legalFormTotal + ' ausgewählt';
}
document.addEventListener('change', function(event) {
if (event.target && event.target.matches('input[type="checkbox"]')) {
updateSelectionCounts();
}
});
function kantoneAusgewählt() {
const checkboxes = document.querySelectorAll('input[name="kantone[]"]');
let countChecked = 0;
@@ -374,6 +473,7 @@ include "zefixAPI.php";
cb.checked = false;
});
filterFunction();
updateSelectionCounts();
}
function gefundeneGemeindenAuswählen() {
@@ -383,19 +483,24 @@ include "zefixAPI.php";
if (entry && entry.style.display !== "none") cb.checked = true;
});
filterFunction();
updateSelectionCounts();
}
function alleRechtsformenAuswählen() {
document.querySelectorAll('input[name="rechtsformen[]"]').forEach(function(cb) {
cb.checked = true;
});
updateSelectionCounts();
}
function rechtsformenLöschen() {
document.querySelectorAll('input[name="rechtsformen[]"]').forEach(function(cb) {
cb.checked = false;
});
updateSelectionCounts();
}
updateSelectionCounts();
</script>
</div>
@@ -405,7 +510,7 @@ include "zefixAPI.php";
</div>
</main>
<footer class="border-top bg-white">
<footer class="site-footer border-top">
<div class="container py-4">
<div class="row g-4">
<div class="col-md-4">
@@ -416,7 +521,7 @@ include "zefixAPI.php";
8245 Feuerthalen<br>
<a href="https://www.silias.ch" target="_blank" rel="noopener">www.silias.ch</a><br>
<a href="mailto:info@silias.ch">info@silias.ch</a><br>
<a href="https://gitea.silias.ch/Silias-Public/Zefix_search">Projekt Repository</a>
<a href="https://gitea.silias.ch/Silias-Public/Zefix_search">Projekt-Repository</a>
</div>
</div>
@@ -435,23 +540,10 @@ include "zefixAPI.php";
</div>
<div class="col-md-4">
<h2 class="h6 mb-2">Warteliste</h2>
<h2 class="h6 mb-2">Datenschutz</h2>
<div class="text-muted" style="font-size:.95rem;">
<script>
const deleteFiles = function () {
let taskDeletePassword = prompt("Passwort eingeben:");
if (taskDeletePassword === null) return;
let formdata = new FormData();
formdata.append('taskDeletePassword', taskDeletePassword);
fetch("deleteFiles.php", { method: "POST", body: formdata })
.then(() => location.reload());
}
</script>
Momentan sind
<b><?php $taskDir = 'tasks'; echo count(scandir($taskDir)) - 2 ?></b>
Aufträge in der Warteliste.<br>
<a href="#" onclick="deleteFiles(); return false;">alle löschen</a>
Wir verwenden Ihre E-Mail-Adresse ausschliesslich zur Bereitstellung des angeforderten Exports.
<br><a href="datenschutz.php">Datenschutzerklärung</a>
</div>
</div>
</div>