Sicherheits-/Korrektheits-Fixes und neue Funktionen

Bugfixes aus dem Code-Review:
- XSS: unescaptes $_SERVER['PHP_SELF'] in csvupload.php und
  letzteneintraege.php durch feste Seitennamen ersetzt.
- Stripe-Webhook: Event-Deduplizierung (neue Tabelle stripe_webhook_events)
  gegen doppelte Verarbeitung/Dolibarr-Rechnungen bei Retry-Zustellung.
- Stripe-Webhook: Tarifwechsel aus dem Kundenportal wird lokal nachgezogen
  (plan_code aus dem Preis-lookup_key bei subscription.updated).
- Post-Redirect-Get fuer jahresauswertung, mailversenden und die
  Selbst-Stricheintragung - verhindert Doppelbuchung/Doppelversand per
  Browser-Refresh.
- Jahresbonus: Mails erst nach erfolgreichem Commit; Restcent-Ausgleich
  beim letzten Empfaenger, damit die Summe exakt stimmt.
- Verschachtelte HTML-Dokumente in csvupload/einzahlung/stricheintragen
  entfernt (Layout kommt aus header.php).
- Rate-Limit fuer den Versand von E-Mail-Verifizierungslinks.

Neue Funktionen:
- Mitglieder koennen ihren zuletzt selbst eingetragenen Strich wieder
  stornieren (nur eigene Web-Eintraege, Kassenwart-Eintraege bleiben).
- Monatsuebersicht des eigenen Verbrauchs im Mitglieder-Dashboard.
- Automatische Zahlungserinnerung: opt-in pro Mandant ab der
  Warnschwelle, mit Intervall; Cron-Skript scripts/send-payment-reminders.php.
- CSV-Import fuer Mitgliederlisten inkl. herunterladbarer Vorlage
  (mitglieder-vorlage.php), Semikolon-/Komma- und BOM-Erkennung.
- Logo als PDF-Wasserzeichen pro Mandant (Upload in den Mandant-
  Einstellungen, geschuetzt unter var/tenant_logos/, ersetzt den
  Text-Wasserzeichen im Ausdruck).

Robusterer Teilnehmer-Lookup im Dashboard ueber user_id (Fallback E-Mail).
This commit is contained in:
2026-07-20 21:54:16 +02:00
parent c4cb12b26e
commit ef5d0e1822
23 changed files with 918 additions and 160 deletions
+48
View File
@@ -263,3 +263,51 @@ function imports_commit_batch(PDO $pdo, int $tenantId, int $batchId): array
return ['imported' => $imported];
}
/**
* Parst eine Mitglieder-CSV (Vorlage: Name, E-Mail, PayPal-Name, Aktiv). Erste
* Zeile ist die Kopfzeile und wird uebersprungen. Trennzeichen wird anhand der
* Kopfzeile automatisch erkannt (Komma oder Semikolon - Excel im deutschen
* Gebietsschema exportiert Semikolon).
*
* @return list<array{row_number:int, name:string, email:string, paypal_name:string, active:bool}>
*/
function imports_parse_member_csv(string $filePath): array
{
$handle = fopen($filePath, 'r');
if ($handle === false) {
throw new RuntimeException('Die CSV-Datei konnte nicht gelesen werden.');
}
$firstLine = fgets($handle);
if ($firstLine === false) {
fclose($handle);
return [];
}
// BOM entfernen, Trennzeichen erkennen.
$firstLine = preg_replace('/^\xEF\xBB\xBF/', '', $firstLine);
$delimiter = (substr_count($firstLine, ';') > substr_count($firstLine, ',')) ? ';' : ',';
$rows = [];
$rowNumber = 0;
while (($data = fgetcsv($handle, 2000, $delimiter)) !== false) {
// Komplett leere Zeilen ueberspringen.
if ($data === [null] || (count($data) === 1 && trim((string)$data[0]) === '')) {
continue;
}
$rowNumber++;
$aktivRaw = strtolower(trim((string)($data[3] ?? 'ja')));
$active = !in_array($aktivRaw, ['nein', 'no', '0', 'false', 'inaktiv'], true);
$rows[] = [
'row_number' => $rowNumber,
'name' => trim((string)($data[0] ?? '')),
'email' => trim((string)($data[1] ?? '')),
'paypal_name' => trim((string)($data[2] ?? '')),
'active' => $active,
];
}
fclose($handle);
return $rows;
}