M5: Mitgliederverwaltung tenant- und rollenbasiert umstellen
- mitarbeiterverwalten.php war rein ueber Legacy-Admin gesperrt und haette neue SaaS-Mandanten ausgeschlossen; jetzt Rollen-/Legacy-Fallback wie in kaffeeliste.php (owner/admin). - Anlegen/Bearbeiten/Aktivieren/Deaktivieren spiegeln transaktional nach participants (ledger_mirror_legacy_participant neu ergaenzt), damit Ledger-Ansichten sofort den aktuellen Mitgliederstand zeigen. - Gespeicherte XSS-Luecke behoben: Name/E-Mail waren in Formular und Liste ungeschuetzt ausgegeben, jetzt ueber saas_html(). - Admin-Rollenvergabe bleibt bewusst Legacy-only (kein automatischer Login-Account ohne Einladungsflow); dokumentiert als offener Punkt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -319,6 +319,36 @@ function ledger_mirror_legacy_consumption(PDO $pdo, int $tenantId, int $legacyCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors a single kl_Mitarbeiter row into participants, so member changes
|
||||||
|
* made through the legacy admin UI are immediately visible to ledger-backed
|
||||||
|
* reads instead of waiting for the next scripts/backfill-default-tenant.php run.
|
||||||
|
*/
|
||||||
|
function ledger_mirror_legacy_participant(PDO $pdo, int $tenantId, int $legacyMitarbeiterId): void
|
||||||
|
{
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"INSERT INTO participants
|
||||||
|
(tenant_id, display_name, email, email_norm, paypal_name, active, legacy_mitarbeiter_id)
|
||||||
|
SELECT
|
||||||
|
?,
|
||||||
|
NULLIF(TRIM(m.Name), ''),
|
||||||
|
NULLIF(TRIM(m.Email), ''),
|
||||||
|
NULLIF(LOWER(TRIM(m.Email)), ''),
|
||||||
|
NULLIF(TRIM(m.paypalname), ''),
|
||||||
|
m.aktiv,
|
||||||
|
m.MitarbeiterID
|
||||||
|
FROM kl_Mitarbeiter m
|
||||||
|
WHERE m.MitarbeiterID = ?
|
||||||
|
ON DUPLICATE KEY UPDATE
|
||||||
|
display_name = COALESCE(VALUES(display_name), participants.display_name),
|
||||||
|
email = VALUES(email),
|
||||||
|
email_norm = VALUES(email_norm),
|
||||||
|
paypal_name = VALUES(paypal_name),
|
||||||
|
active = VALUES(active)"
|
||||||
|
);
|
||||||
|
$stmt->execute([$tenantId, $legacyMitarbeiterId]);
|
||||||
|
}
|
||||||
|
|
||||||
function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymentId): void
|
function ledger_mirror_legacy_payment(PDO $pdo, int $tenantId, int $legacyPaymentId): void
|
||||||
{
|
{
|
||||||
$stmt = $pdo->prepare(
|
$stmt = $pdo->prepare(
|
||||||
|
|||||||
+45
-2
@@ -127,12 +127,52 @@ Alle drei Flows wurden gegen die Remote-Dev-Datenbank live per HTTP getestet
|
|||||||
`letzteneintraege.php`) und anschließend wieder auf den Ausgangsstand
|
`letzteneintraege.php`) und anschließend wieder auf den Ausgangsstand
|
||||||
zurückgesetzt.
|
zurückgesetzt.
|
||||||
|
|
||||||
|
## Fortsetzung: Mitgliederverwaltung
|
||||||
|
|
||||||
|
Umgesetzte Dateien:
|
||||||
|
|
||||||
|
```text
|
||||||
|
mitarbeiterverwalten.php
|
||||||
|
app/ledger.php
|
||||||
|
```
|
||||||
|
|
||||||
|
Umfang:
|
||||||
|
|
||||||
|
- Zugriff nutzte bisher ausschließlich `checkKaffeelisteAdmin` und hätte neue
|
||||||
|
SaaS-Mandanten ohne Legacy-Mitarbeiterzeile ausgeschlossen. Jetzt gilt die
|
||||||
|
gleiche Rollen-/Fallback-Logik wie in `kaffeeliste.php`, mit den Rollen
|
||||||
|
`owner` und `admin` (nicht `treasurer`, da Mitgliederpflege sensibler ist
|
||||||
|
als reine Zahlungsvorgänge).
|
||||||
|
- Anlegen, Bearbeiten, Aktivieren und Deaktivieren schreiben weiterhin zuerst
|
||||||
|
in `kl_Mitarbeiter` und spiegeln danach in derselben Transaktion über die
|
||||||
|
neue Funktion `ledger_mirror_legacy_participant()` nach `participants`.
|
||||||
|
Ledger-Ansichten (Dashboard, Kaffeeliste, Teilnehmerauswertung) sehen neue
|
||||||
|
oder geänderte Mitglieder damit sofort, ohne auf einen manuellen Lauf von
|
||||||
|
`scripts/backfill-default-tenant.php` zu warten.
|
||||||
|
- Eine gespeicherte-XSS-Lücke wurde behoben: Name und E-Mail wurden beim
|
||||||
|
Bearbeiten-Formular und in der Mitgliederliste bisher ungeschützt
|
||||||
|
ausgegeben (nur `paypalname` war escaped). Beide Stellen nutzen jetzt
|
||||||
|
`saas_html()`.
|
||||||
|
- Live gegen die Dev-Datenbank getestet: Anlegen mit einem
|
||||||
|
HTML/Skript-Payload im Namen (korrekt escaped in der Ausgabe, korrekt
|
||||||
|
gespiegelt in `participants`), Deaktivieren (spiegelt `active = 0`).
|
||||||
|
|
||||||
|
Bewusst nicht umgesetzt:
|
||||||
|
|
||||||
|
- Der Haken "Administrator" bleibt ein reines Legacy-Feld auf
|
||||||
|
`kl_Mitarbeiter.admin` und wird nicht automatisch in eine
|
||||||
|
`tenant_memberships`-Rolle übersetzt. Das würde einen Login-Account ohne
|
||||||
|
Einladung/Passwort-Setzung anlegen, was ein eigenes, sauber zu
|
||||||
|
bauendes Einladungs-Flow braucht (E-Mail-Versand, Token, Passwortsetzung).
|
||||||
|
Admin-Rollen für neue SaaS-Nutzer laufen bis dahin weiter über
|
||||||
|
`scripts/backfill-default-tenant.php` oder die Registrierung.
|
||||||
|
|
||||||
## Noch offen
|
## Noch offen
|
||||||
|
|
||||||
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
|
- Eigene PayPal-/Zahlungsbereich als eigenständiger App-Screen (aktuell nur im
|
||||||
Dashboard integriert).
|
Dashboard integriert).
|
||||||
- Mitgliederverwaltung tenant- und rollenbasiert umsetzen
|
- Einladungs-Flow, um bestehenden Teilnehmern nachträglich einen
|
||||||
(`mitarbeiterverwalten.php` ist noch nicht angefasst).
|
Login-Account mit `tenant_memberships`-Rolle zuzuweisen.
|
||||||
- Hinweise als tenant-spezifische Notices umsetzen.
|
- Hinweise als tenant-spezifische Notices umsetzen.
|
||||||
- Export, Mail und Jahresprozesse bleiben M6-Themen.
|
- Export, Mail und Jahresprozesse bleiben M6-Themen.
|
||||||
|
|
||||||
@@ -144,3 +184,6 @@ zurückgesetzt.
|
|||||||
- HTTP-Smoke: grün mit 23 geprüften Seiten.
|
- HTTP-Smoke: grün mit 23 geprüften Seiten.
|
||||||
- Live-Test der Schreibflows gegen die Dev-DB: Sammelstriche, Sammeleinzahlung
|
- Live-Test der Schreibflows gegen die Dev-DB: Sammelstriche, Sammeleinzahlung
|
||||||
und Storno beider Buchungsarten erfolgreich geprüft.
|
und Storno beider Buchungsarten erfolgreich geprüft.
|
||||||
|
- Live-Test der Mitgliederverwaltung gegen die Dev-DB: Anlegen (inklusive
|
||||||
|
XSS-Payload-Check), Deaktivieren, participants-Spiegelung erfolgreich
|
||||||
|
geprüft.
|
||||||
|
|||||||
+63
-39
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
include "functions.php";
|
include "functions.php";
|
||||||
|
require_once __DIR__ . "/app/ledger.php";
|
||||||
app_require_csrf();
|
app_require_csrf();
|
||||||
include "header.php";
|
include "header.php";
|
||||||
include "headerline.php";
|
include "headerline.php";
|
||||||
@@ -17,66 +18,89 @@ include "nav.php";
|
|||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
if(checkKaffeelisteAdmin($conn, $mailadress)){
|
$pdo = app_db_pdo();
|
||||||
|
$saasUser = saas_current_user($pdo);
|
||||||
|
$tenantId = 0;
|
||||||
|
$hasAccess = false;
|
||||||
|
|
||||||
|
if ($saasUser !== null && saas_user_has_role(['owner', 'admin'], $saasUser)) {
|
||||||
|
$tenantId = (int)$saasUser['tenant_id'];
|
||||||
|
$hasAccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$hasAccess && $saasUser === null && checkKaffeelisteAdmin($conn, $mailadress)) {
|
||||||
|
$tenant = ledger_fetch_default_tenant($pdo);
|
||||||
|
if ($tenant !== null) {
|
||||||
|
$tenantId = (int)$tenant['id'];
|
||||||
|
$hasAccess = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($hasAccess){
|
||||||
|
|
||||||
|
|
||||||
// Funktion zum Anlegen, Bearbeiten und Deaktivieren von Mitgliedern
|
// Funktion zum Anlegen, Bearbeiten und Deaktivieren von Mitgliedern. Der
|
||||||
function bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, $conn) {
|
// gespiegelte participants-Datensatz wird in derselben Transaktion
|
||||||
|
// nachgezogen, damit Ledger-Ansichten sofort den neuen Stand zeigen.
|
||||||
|
function bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, PDO $pdo, int $tenantId) {
|
||||||
try {
|
try {
|
||||||
if ($aktion === 'anlegen') {
|
if ($aktion === 'anlegen') {
|
||||||
$sql = "INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin) VALUES (?, ?, ?, ?, ?)";
|
$sql = "INSERT INTO kl_Mitarbeiter (Name, Email, paypalname, aktiv, admin) VALUES (?, ?, ?, ?, ?)";
|
||||||
|
$params = array($name, $email, $paypalname, $aktiv, $admin);
|
||||||
} elseif ($aktion === 'bearbeitenspeichern') {
|
} elseif ($aktion === 'bearbeitenspeichern') {
|
||||||
$sql = "UPDATE kl_Mitarbeiter SET Name = ?, Email = ?, paypalname = ?, aktiv = ?, admin = ? WHERE MitarbeiterID = ?";
|
$sql = "UPDATE kl_Mitarbeiter SET Name = ?, Email = ?, paypalname = ?, aktiv = ?, admin = ? WHERE MitarbeiterID = ?";
|
||||||
|
$params = array($name, $email, $paypalname, $aktiv, $admin, $mitgliedID);
|
||||||
} elseif ($aktion === 'aktivieren') {
|
} elseif ($aktion === 'aktivieren') {
|
||||||
$sql = "UPDATE kl_Mitarbeiter SET aktiv = 1 WHERE MitarbeiterID = ?";
|
$sql = "UPDATE kl_Mitarbeiter SET aktiv = 1 WHERE MitarbeiterID = ?";
|
||||||
|
$params = array($mitgliedID);
|
||||||
} elseif ($aktion === 'deaktivieren') {
|
} elseif ($aktion === 'deaktivieren') {
|
||||||
$sql = "UPDATE kl_Mitarbeiter SET aktiv = 0 WHERE MitarbeiterID = ?";
|
$sql = "UPDATE kl_Mitarbeiter SET aktiv = 0 WHERE MitarbeiterID = ?";
|
||||||
|
$params = array($mitgliedID);
|
||||||
} else {
|
} else {
|
||||||
throw new Exception("Ungültige Aktion.");
|
throw new Exception("Ungültige Aktion.");
|
||||||
}
|
}
|
||||||
|
|
||||||
$params = array($name, $email, $paypalname, $aktiv, $admin);
|
$pdo->beginTransaction();
|
||||||
|
|
||||||
// Füge die MitarbeiterID nur bei Bearbeitung hinzu
|
$stmt = $pdo->prepare($sql);
|
||||||
if ($aktion === 'bearbeitenspeichern' ) {
|
$stmt->execute($params);
|
||||||
array_push($params, $mitgliedID);
|
|
||||||
}elseif($aktion === 'aktivieren' || $aktion === 'deaktivieren'){
|
|
||||||
$params = array($mitgliedID);
|
|
||||||
}
|
|
||||||
|
|
||||||
$stmt = sqlsrv_query($conn, $sql, $params);
|
$legacyMitarbeiterId = $aktion === 'anlegen' ? (int)$pdo->lastInsertId() : (int)$mitgliedID;
|
||||||
|
ledger_mirror_legacy_participant($pdo, $tenantId, $legacyMitarbeiterId);
|
||||||
|
|
||||||
if ($stmt === false) {
|
$pdo->commit();
|
||||||
throw new Exception(print_r(sqlsrv_errors(), true));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // Erfolgreich
|
return true; // Erfolgreich
|
||||||
} catch (Exception $e) {
|
} catch (Throwable $e) {
|
||||||
return $e->getMessage(); // Fehlermeldung zurückgeben
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Die Aktion konnte nicht gespeichert werden.';
|
||||||
}
|
}
|
||||||
}?>
|
}?>
|
||||||
|
|
||||||
<!-- Formular für das Bearbeiten von Mitgliedern -->
|
<!-- Formular für das Bearbeiten von Mitgliedern -->
|
||||||
<?php
|
<?php
|
||||||
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] === 'bearbeiten') {
|
if ($_SERVER["REQUEST_METHOD"] == "POST" && isset($_POST["aktion"]) && $_POST["aktion"] === 'bearbeiten') {
|
||||||
$mitgliedID = $_POST["mitgliedID"];
|
$mitgliedID = (int)$_POST["mitgliedID"];
|
||||||
|
|
||||||
// Informationen des ausgewählten Mitglieds abrufen
|
// Informationen des ausgewählten Mitglieds abrufen
|
||||||
$sqlEinzelmitglied = "SELECT * FROM kl_Mitarbeiter WHERE MitarbeiterID = ?";
|
$stmtEinzelmitglied = $pdo->prepare("SELECT * FROM kl_Mitarbeiter WHERE MitarbeiterID = ?");
|
||||||
$stmtEinzelmitglied = sqlsrv_query($conn, $sqlEinzelmitglied, array($mitgliedID));
|
$stmtEinzelmitglied->execute([$mitgliedID]);
|
||||||
$einzelmitglied = sqlsrv_fetch_array($stmtEinzelmitglied, SQLSRV_FETCH_ASSOC);
|
$einzelmitglied = $stmtEinzelmitglied->fetch();
|
||||||
?>
|
?>
|
||||||
<h3>Bearbeiten von <?php echo $einzelmitglied['Name']; ?></h3>
|
<h3>Bearbeiten von <?php echo saas_html($einzelmitglied['Name']); ?></h3>
|
||||||
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
<form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]); ?>">
|
||||||
<input type="hidden" name="aktion" value="bearbeitenspeichern">
|
<input type="hidden" name="aktion" value="bearbeitenspeichern">
|
||||||
<input type="hidden" name="mitgliedID" value="<?php echo $mitgliedID; ?>">
|
<input type="hidden" name="mitgliedID" value="<?php echo $mitgliedID; ?>">
|
||||||
<?php echo app_csrf_field(); ?>
|
<?php echo app_csrf_field(); ?>
|
||||||
|
|
||||||
<label for="name">Name:</label>
|
<label for="name">Name:</label>
|
||||||
<input type="text" name="name" id="name" value="<?php echo $einzelmitglied['Name']; ?>" required>
|
<input type="text" name="name" id="name" value="<?php echo saas_html($einzelmitglied['Name']); ?>" required>
|
||||||
|
|
||||||
<label for="email">E-Mail:</label>
|
<label for="email">E-Mail:</label>
|
||||||
<input type="email" name="email" id="email" value="<?php echo $einzelmitglied['Email']; ?>" required>
|
<input type="email" name="email" id="email" value="<?php echo saas_html($einzelmitglied['Email']); ?>" required>
|
||||||
|
|
||||||
<label for="paypalname">PayPal-Name:</label>
|
<label for="paypalname">PayPal-Name:</label>
|
||||||
<input type="text" name="paypalname" id="paypalname" value="<?php echo htmlspecialchars($einzelmitglied['paypalname'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
<input type="text" name="paypalname" id="paypalname" value="<?php echo htmlspecialchars($einzelmitglied['paypalname'] ?? '', ENT_QUOTES, 'UTF-8'); ?>">
|
||||||
@@ -101,7 +125,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
$aktion = $_POST["aktion"];
|
$aktion = $_POST["aktion"];
|
||||||
|
|
||||||
if ($aktion === 'anlegen' || $aktion === 'bearbeitenspeichern') {
|
if ($aktion === 'anlegen' || $aktion === 'bearbeitenspeichern') {
|
||||||
$mitgliedID = isset($_POST["mitgliedID"]) ? $_POST["mitgliedID"] : null;
|
$mitgliedID = isset($_POST["mitgliedID"]) ? (int)$_POST["mitgliedID"] : null;
|
||||||
$name = $_POST["name"];
|
$name = $_POST["name"];
|
||||||
$email = $_POST["email"];
|
$email = $_POST["email"];
|
||||||
$paypalname = trim($_POST["paypalname"] ?? '');
|
$paypalname = trim($_POST["paypalname"] ?? '');
|
||||||
@@ -109,7 +133,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
$aktiv = isset($_POST["aktiv"]) ? 1 : 0;
|
$aktiv = isset($_POST["aktiv"]) ? 1 : 0;
|
||||||
$admin = isset($_POST["admin"]) ? 1 : 0;
|
$admin = isset($_POST["admin"]) ? 1 : 0;
|
||||||
|
|
||||||
$ergebnis = bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, $conn);
|
$ergebnis = bearbeiteMitglied($aktion, $mitgliedID, $name, $email, $paypalname, $aktiv, $admin, $pdo, $tenantId);
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
if ($ergebnis === true) {
|
||||||
echo "Aktion erfolgreich durchgeführt.";
|
echo "Aktion erfolgreich durchgeführt.";
|
||||||
@@ -117,9 +141,9 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
echo "Fehler: $ergebnis";
|
echo "Fehler: $ergebnis";
|
||||||
}
|
}
|
||||||
} elseif ($aktion === 'aktivieren') {
|
} elseif ($aktion === 'aktivieren') {
|
||||||
$mitgliedID = $_POST["mitgliedID"];
|
$mitgliedID = (int)$_POST["mitgliedID"];
|
||||||
|
|
||||||
$ergebnis = bearbeiteMitglied('aktivieren', $mitgliedID, null, null, null, null, null, $conn);
|
$ergebnis = bearbeiteMitglied('aktivieren', $mitgliedID, null, null, null, null, null, $pdo, $tenantId);
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
if ($ergebnis === true) {
|
||||||
echo "Mitglied erfolgreich aktiviert.";
|
echo "Mitglied erfolgreich aktiviert.";
|
||||||
@@ -127,9 +151,9 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
echo "Fehler: $ergebnis";
|
echo "Fehler: $ergebnis";
|
||||||
}
|
}
|
||||||
} elseif ($aktion === 'deaktivieren') {
|
} elseif ($aktion === 'deaktivieren') {
|
||||||
$mitgliedID = $_POST["mitgliedID"];
|
$mitgliedID = (int)$_POST["mitgliedID"];
|
||||||
|
|
||||||
$ergebnis = bearbeiteMitglied('deaktivieren', $mitgliedID, null, null, null, null, null, $conn);
|
$ergebnis = bearbeiteMitglied('deaktivieren', $mitgliedID, null, null, null, null, null, $pdo, $tenantId);
|
||||||
|
|
||||||
if ($ergebnis === true) {
|
if ($ergebnis === true) {
|
||||||
echo "Mitglied erfolgreich deaktiviert.";
|
echo "Mitglied erfolgreich deaktiviert.";
|
||||||
@@ -141,8 +165,7 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
|
|
||||||
|
|
||||||
// Mitglieder aus der Datenbank abrufen
|
// Mitglieder aus der Datenbank abrufen
|
||||||
$sqlMitglieder = "SELECT MitarbeiterID, Name, Email, paypalname, aktiv, admin FROM kl_Mitarbeiter ORDER BY Name";
|
$stmtMitglieder = $pdo->query("SELECT MitarbeiterID, Name, Email, paypalname, aktiv, admin FROM kl_Mitarbeiter ORDER BY Name");
|
||||||
$stmtMitglieder = sqlsrv_query($conn, $sqlMitglieder);
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@@ -197,13 +220,14 @@ if(checkKaffeelisteAdmin($conn, $mailadress)){
|
|||||||
</tr>
|
</tr>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
while ($row = sqlsrv_fetch_array($stmtMitglieder, SQLSRV_FETCH_ASSOC)) {
|
while ($row = $stmtMitglieder->fetch()) {
|
||||||
|
$mitgliedIdRow = (int)$row['MitarbeiterID'];
|
||||||
|
|
||||||
echo '<tr >';
|
echo '<tr >';
|
||||||
echo "<td>{$row['MitarbeiterID']}</td>";
|
echo "<td>{$mitgliedIdRow}</td>";
|
||||||
echo "<td>{$row['Name']}</td>";
|
echo "<td>" . saas_html($row['Name']) . "</td>";
|
||||||
echo "<td>{$row['Email']}</td>";
|
echo "<td>" . saas_html($row['Email']) . "</td>";
|
||||||
echo "<td>" . htmlspecialchars($row['paypalname'] ?? '', ENT_QUOTES, 'UTF-8') . "</td>";
|
echo "<td>" . saas_html($row['paypalname'] ?? '') . "</td>";
|
||||||
echo "<td>{$row['aktiv']}</td>";
|
echo "<td>{$row['aktiv']}</td>";
|
||||||
echo "<td>{$row['admin']}</td>";
|
echo "<td>{$row['admin']}</td>";
|
||||||
echo "<td>";
|
echo "<td>";
|
||||||
|
|||||||
Reference in New Issue
Block a user