50c72fa8d8
Umsetzung des Thomas-Feedbacks: - Reihenfolge: freie Lizenzen beim Lehrer jetzt ORDER BY id (= Reihenfolge der reinkopierten Liste); stornierte ausgeblendet. - Gratis-Zugabe: Checkbox (Default an) "+10 % aufgerundet" -> geliefert = ceil(bestellt*1.1) (14 -> 16). Batch speichert qty_ordered (verrechnet) und qty (geliefert); Live-Vorschau im Formular. - Einloeselink/QR ENTFERNT (Sicherheitsrisiko: falscher eingeloggter Account). einloesen.html + pages/einloesen.php geloescht, Sammellink aus UI/Mail raus. - CSV: Gueltig-bis + Metadaten nur in der KOPFZEILE, danach reine Code-Zeilen (per Copy-Paste einloesbar). Dateiname: <ExtBestellnr|Datum>_geograsim_<intNr>. - Interne Bestellnummer = batch.id (laufend, nicht editierbar, storno-stabil). - Lehrer-Einloesen: sichere .txt/.csv-Ablage (mehrstufig: Endung+MIME, Groessen- limit, nur Text lesen, Whitelist-Regex extrahiert nur Code-Tokens, Server validiert) — kein Upload, keine Ausfuehrung/Rendering. - Mailsystem: zentrale Config in neuer app_settings (Provider server|aws, Absender-Name/-Adresse, oeffentliche URL), pflegbar in Admin + Test-Versand. Mailer liest Absender aus Config (SMTP-Zugang bleibt in .env). Mailtext nutzt kanonische geograsim.at (nicht v3/localhost). Migrationen: -2 (qty_ordered), -3 (app_settings). Beide additiv, auf v3 angewandt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
275 lines
16 KiB
PHP
275 lines
16 KiB
PHP
<?php
|
|
/**
|
|
* GeoGraSim — E-Mail-Versand via PHPMailer
|
|
* Alle E-Mails im einheitlichen Design (Petrol #1f4e5a, Türkis #4a7c8a, Orange #e8833a)
|
|
*/
|
|
|
|
class Mailer {
|
|
|
|
private static function create(): \PHPMailer\PHPMailer\PHPMailer {
|
|
$mail = new \PHPMailer\PHPMailer\PHPMailer(true);
|
|
$mail->isSMTP();
|
|
$mail->Host = defined('SMTP_HOST') ? SMTP_HOST : 'smtp.world4you.com';
|
|
$mail->SMTPAuth = true;
|
|
$mail->Username = defined('SMTP_USER') ? SMTP_USER : 'support@geograsim.at';
|
|
$mail->Password = defined('SMTP_PASS') ? SMTP_PASS : 'geo8Zeichen!';
|
|
$mail->SMTPSecure = \PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_STARTTLS;
|
|
$mail->Port = defined('SMTP_PORT') ? (int)SMTP_PORT : 587;
|
|
$mail->CharSet = 'UTF-8';
|
|
[$fromEmail, $fromName] = self::configuredFrom();
|
|
$mail->setFrom($fromEmail, $fromName);
|
|
return $mail;
|
|
}
|
|
|
|
/** Absender aus Admin-Config (platform_config_v2), Fallback auf .env-Konstanten.
|
|
* SMTP-Zugangsdaten bleiben bewusst in .env (geheim) — nur die sichtbare
|
|
* Absenderadresse/-name ist zentral in der Admin-Oberflaeche pflegbar. */
|
|
private static function configuredFrom(): array {
|
|
$email = defined('SMTP_FROM') ? SMTP_FROM : 'support@geograsim.at';
|
|
$name = defined('SMTP_FROM_NAME') ? SMTP_FROM_NAME : 'GeoGraSim';
|
|
try {
|
|
if (class_exists('Database')) {
|
|
$rows = Database::get()->fetchAll(
|
|
"SELECT skey, sval FROM app_settings WHERE skey IN ('mail_from_email','mail_from_name')");
|
|
foreach ($rows as $row) {
|
|
if ($row['skey'] === 'mail_from_email' && !empty($row['sval'])) $email = $row['sval'];
|
|
if ($row['skey'] === 'mail_from_name' && !empty($row['sval'])) $name = $row['sval'];
|
|
}
|
|
}
|
|
} catch (\Throwable $e) { /* Config nicht verfuegbar -> Defaults */ }
|
|
return [$email, $name];
|
|
}
|
|
|
|
public static function send(string $to, string $subject, string $htmlBody, string $textBody = ''): bool {
|
|
try {
|
|
$mail = self::create();
|
|
$mail->addAddress($to);
|
|
$mail->isHTML(true);
|
|
$mail->Subject = $subject;
|
|
$mail->Body = self::wrapHtml($htmlBody);
|
|
$mail->AltBody = $textBody ?: strip_tags(str_replace(['<br>','<br/>','</p>','</li>'], "\n", $htmlBody));
|
|
$mail->send();
|
|
return true;
|
|
} catch (\Exception $e) {
|
|
error_log('Mailer error: ' . $e->getMessage());
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Versand mit optionalen Datei-Anhaengen (z. B. Lizenz-Codes als .txt/.csv
|
|
* bei grossen Chargen). $attachments: [ ['content'=>string,'name'=>string,'type'=>string], ... ]
|
|
* Wirft bei Fehler eine Exception (Aufrufer archiviert Status/Fehler selbst).
|
|
*/
|
|
public static function sendWithAttachments(string $to, string $subject, string $htmlBody, string $textBody = '', array $attachments = []): void {
|
|
$mail = self::create();
|
|
$mail->addAddress($to);
|
|
$mail->isHTML(true);
|
|
$mail->Subject = $subject;
|
|
$mail->Body = self::wrapHtml($htmlBody);
|
|
$mail->AltBody = $textBody ?: strip_tags(str_replace(['<br>','<br/>','</p>','</li>'], "\n", $htmlBody));
|
|
foreach ($attachments as $a) {
|
|
if (empty($a['content']) || empty($a['name'])) continue;
|
|
$mail->addStringAttachment($a['content'], $a['name'], \PHPMailer\PHPMailer\PHPMailer::ENCODING_BASE64, $a['type'] ?? 'text/plain');
|
|
}
|
|
$mail->send();
|
|
}
|
|
|
|
/** Freitext-Body (aus Admin-Textbaustein) in die Card-Optik giessen. */
|
|
public static function renderPlainBody(string $text): string {
|
|
$safe = nl2br(htmlspecialchars($text, ENT_QUOTES, 'UTF-8'));
|
|
return '<div style="font-size:14px;color:#333;line-height:1.6">' . $safe . '</div>';
|
|
}
|
|
|
|
// ─── Willkommen (Lehrperson registriert sich) ───
|
|
|
|
public static function sendWelcomeTeacher(string $email, string $name): bool {
|
|
$n = htmlspecialchars($name);
|
|
$html = self::greeting($n)
|
|
. self::text('Willkommen bei <strong>GeoGraSim</strong> — interaktive Simulationen für den Geografie-Unterricht!')
|
|
. self::text('Ihr Konto wurde erfolgreich erstellt. Sie können jetzt:')
|
|
. self::checklist([
|
|
'Klassen anlegen und Schüler*innen-Zugänge generieren',
|
|
'Module freischalten und den Unterricht steuern',
|
|
'Ergebnisse und Fortschritte im Dashboard verfolgen',
|
|
])
|
|
. self::button('Zum Dashboard', 'https://geograsim.at/teacher.html')
|
|
. self::hint('Im <a href="https://geograsim.at/handbuch.html" style="color:#4a7c8a">Didaktischen Handbuch</a> finden Sie zu jedem Modul einen fertigen 45-Minuten-Unterrichtsentwurf.');
|
|
return self::send($email, 'Willkommen bei GeoGraSim', $html);
|
|
}
|
|
|
|
// ─── Zugangsdaten (Schüler*in wurde erstellt) ───
|
|
|
|
public static function sendStudentCredentials(string $email, string $studentName, string $username, string $password, string $classCode, string $className): bool {
|
|
$n = htmlspecialchars($studentName);
|
|
$html = self::greeting($n)
|
|
. self::text('Du hast einen Zugang zu <strong>GeoGraSim</strong> erhalten — interaktive Simulationen für den Geografie-Unterricht.')
|
|
. self::credentialBox($username, $password, $classCode, $className)
|
|
. self::button('Jetzt anmelden', 'https://geograsim.at/login.html')
|
|
. self::hint('Bewahre diese Zugangsdaten gut auf. Bei Problemen wende dich an deine Lehrperson.');
|
|
return self::send($email, 'Dein GeoGraSim-Zugang', $html);
|
|
}
|
|
|
|
// ─── Passwort-Reset ───
|
|
|
|
public static function sendPasswordReset(string $email, string $name, string $token): bool {
|
|
$link = 'https://geograsim.at/login.html?reset=' . urlencode($token);
|
|
$n = htmlspecialchars($name);
|
|
$html = self::greeting($n)
|
|
. self::text('Sie haben ein neues Passwort für Ihr GeoGraSim-Konto angefordert.')
|
|
. self::text('Klicken Sie auf den Button, um ein neues Passwort zu wählen:')
|
|
. self::button('Neues Passwort wählen', $link)
|
|
. self::hint('Dieser Link ist <strong>1 Stunde</strong> gültig und kann nur einmal verwendet werden.')
|
|
. self::hint('Falls Sie kein neues Passwort angefordert haben, können Sie diese E-Mail ignorieren. Ihr bestehendes Passwort bleibt unverändert.')
|
|
. self::hint('Bei Fragen wenden Sie sich an <a href="mailto:support@geograsim.at" style="color:#4a7c8a">support@geograsim.at</a>.');
|
|
return self::send($email, 'GeoGraSim — Passwort zurücksetzen', $html);
|
|
}
|
|
|
|
// ─── Admin 2FA PIN ───
|
|
|
|
public static function sendAdminPin(string $email, string $pin): bool {
|
|
$html = '<p style="font-size:13px;font-weight:700;letter-spacing:.12em;color:#8aa3aa;text-transform:uppercase;margin:0 0 6px">🔐 Admin-Anmeldung</p>'
|
|
. '<h1 style="font-family:\'Arial Black\',\'Helvetica Neue\',Helvetica,Arial,sans-serif;font-size:22px;font-weight:900;color:#143b4b;margin:0 0 14px;line-height:1.25">Bestätige deinen Login</h1>'
|
|
. '<p style="font-size:15px;color:#3a4a4f;line-height:1.6;margin:0">Gib den folgenden PIN im Admin-Login ein, um deine Anmeldung abzuschließen:</p>'
|
|
. self::pinBox($pin)
|
|
. '<p style="font-size:13px;color:#7a8a8e;line-height:1.55;margin:6px 0 0;padding-top:18px;border-top:1px solid #f0eeea">'
|
|
. 'Falls du keinen Login angefordert hast, kannst du diese E-Mail ignorieren — niemand kommt ohne PIN in dein Admin-Konto.'
|
|
. '</p>';
|
|
return self::send($email, 'GeoGraSim — Dein Admin-PIN', $html);
|
|
}
|
|
|
|
// ─── Batch-Zugangsdaten (Lehrperson bekommt Übersicht) ───
|
|
|
|
public static function sendBatchCredentials(string $teacherEmail, string $teacherName, string $className, array $students): bool {
|
|
$n = htmlspecialchars($teacherName);
|
|
$html = self::greeting($n)
|
|
. self::text('Die folgenden Zugangsdaten wurden für Klasse <strong>' . htmlspecialchars($className) . '</strong> erstellt:')
|
|
. self::studentTable($students, $className)
|
|
. self::button('Zum Dashboard', 'https://geograsim.at/teacher.html')
|
|
. self::hint('Sie können diese Zugangsdaten auch im Dashboard unter „Tickets drucken" als PDF herunterladen.');
|
|
return self::send($teacherEmail, 'GeoGraSim — Zugangsdaten für ' . $className, $html);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// Bausteine
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
private static function greeting(string $name): string {
|
|
return '<p style="font-size:16px;color:#1f4e5a;margin:0 0 12px">Hallo ' . $name . ',</p>';
|
|
}
|
|
|
|
private static function text(string $t): string {
|
|
return '<p style="font-size:14px;color:#333;line-height:1.6;margin:0 0 12px">' . $t . '</p>';
|
|
}
|
|
|
|
private static function button(string $label, string $url): string {
|
|
return '<p style="text-align:center;margin:20px 0">'
|
|
. '<a href="' . $url . '" style="display:inline-block;padding:12px 28px;background:#4a7c8a;color:#fff;text-decoration:none;border-radius:8px;font-weight:700;font-size:14px">'
|
|
. $label . '</a></p>';
|
|
}
|
|
|
|
private static function hint(string $t): string {
|
|
return '<p style="font-size:12px;color:#8a8a8a;line-height:1.5;margin:16px 0 0">' . $t . '</p>';
|
|
}
|
|
|
|
private static function checklist(array $items): string {
|
|
$html = '<table cellpadding="0" cellspacing="0" border="0" style="margin:8px 0 12px">';
|
|
foreach ($items as $item) {
|
|
$html .= '<tr><td style="padding:3px 8px 3px 0;font-size:14px;color:#5a8a5e;vertical-align:top">✓</td>'
|
|
. '<td style="padding:3px 0;font-size:14px;color:#333">' . $item . '</td></tr>';
|
|
}
|
|
return $html . '</table>';
|
|
}
|
|
|
|
private static function credentialBox(string $username, string $password, string $classCode, string $className): string {
|
|
return '<div style="background:#f7f6f3;border:1.5px solid #dae8ec;border-radius:10px;padding:16px;margin:16px 0">'
|
|
. '<table cellpadding="0" cellspacing="0" border="0" style="width:100%">'
|
|
. self::credRow('Klassencode', $classCode, '#e8833a')
|
|
. self::credRow('Klasse', htmlspecialchars($className), '#333')
|
|
. self::credRow('Benutzername', $username, '#1f4e5a')
|
|
. self::credRow('Passwort', $password, '#1f4e5a')
|
|
. '</table>'
|
|
. '<p style="font-size:11px;color:#8a8a8a;margin:10px 0 0;text-align:center">geograsim.at</p>'
|
|
. '</div>';
|
|
}
|
|
|
|
private static function credRow(string $label, string $value, string $color): string {
|
|
return '<tr><td style="padding:4px 0;font-size:12px;color:#8a8a8a;width:110px">' . $label . '</td>'
|
|
. '<td style="padding:4px 0;font-size:15px;font-weight:700;font-family:\'Courier New\',monospace;color:' . $color . ';letter-spacing:.04em">' . htmlspecialchars($value) . '</td></tr>';
|
|
}
|
|
|
|
private static function pinBox(string $pin): string {
|
|
$pinSafe = htmlspecialchars($pin);
|
|
return '<table cellpadding="0" cellspacing="0" border="0" align="center" style="margin:28px auto"><tr><td align="center" style="padding:0">'
|
|
. '<div style="font-size:10px;font-weight:700;letter-spacing:.22em;color:#8aa3aa;text-transform:uppercase;margin-bottom:10px">Dein Anmelde-PIN</div>'
|
|
. '<div style="display:inline-block;padding:22px 36px;background:#0d3b47;background-image:linear-gradient(135deg,#1f4e5a 0%,#0d3b47 100%);border-radius:14px;border:1px solid #1f4e5a;box-shadow:0 6px 18px rgba(13,59,71,0.18)">'
|
|
. '<span style="font-size:42px;font-weight:900;letter-spacing:.28em;color:#fff;font-family:\'SFMono-Regular\',\'Menlo\',\'Consolas\',\'Courier New\',monospace;line-height:1">' . $pinSafe . '</span>'
|
|
. '</div>'
|
|
. '<div style="margin-top:12px"><span style="display:inline-block;padding:4px 11px;background:#fff3cf;border:1px solid #fbbf24;border-radius:999px;font-size:10px;font-weight:700;letter-spacing:.08em;color:#7a5a08;text-transform:uppercase">10 Minuten gültig</span></div>'
|
|
. '</td></tr></table>';
|
|
}
|
|
|
|
private static function studentTable(array $students, string $className): string {
|
|
$html = '<table cellpadding="0" cellspacing="0" border="0" style="width:100%;margin:12px 0;border-collapse:collapse">';
|
|
$html .= '<tr><th style="text-align:left;padding:6px 8px;font-size:11px;color:#8a8a8a;border-bottom:2px solid #dae8ec">Benutzer</th>'
|
|
. '<th style="text-align:left;padding:6px 8px;font-size:11px;color:#8a8a8a;border-bottom:2px solid #dae8ec">Passwort</th></tr>';
|
|
foreach ($students as $s) {
|
|
$html .= '<tr><td style="padding:4px 8px;font-size:13px;font-weight:600;border-bottom:1px solid #f0eeea">' . htmlspecialchars($s['username']) . '</td>'
|
|
. '<td style="padding:4px 8px;font-size:13px;font-family:\'Courier New\',monospace;color:#1f4e5a;border-bottom:1px solid #f0eeea">' . htmlspecialchars($s['password']) . '</td></tr>';
|
|
}
|
|
return $html . '</table>';
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════
|
|
// HTML-Wrapper (Briefpapier)
|
|
// ═══════════════════════════════════════════════════════
|
|
|
|
private static function wrapHtml(string $content): string {
|
|
return <<<HTML
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>GeoGraSim</title>
|
|
</head>
|
|
<body style="margin:0;padding:0;background:#f0eee6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Inter',Roboto,Helvetica,Arial,sans-serif;color:#2a2a2a">
|
|
<!-- Pre-header (von vielen Clients in der Liste vorgezogen) -->
|
|
<div style="display:none;font-size:1px;color:#f0eee6;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden">GeoGraSim — Sandbox für Geographie-Unterricht.</div>
|
|
|
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" style="background:#f0eee6">
|
|
<tr><td align="center" style="padding:32px 16px 24px">
|
|
|
|
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="560" style="max-width:560px;width:100%">
|
|
|
|
<!-- Wortmarke (kein Bild!) — Petrol + Orange-Akzent, mail-safe Bold Sans -->
|
|
<tr><td align="center" style="padding:8px 0 22px">
|
|
<div style="display:inline-block;font-family:'Arial Black','Helvetica Neue',Helvetica,Arial,sans-serif;font-weight:900;font-size:34px;letter-spacing:-.02em;line-height:1">
|
|
<span style="color:#143b4b">Geo</span><span style="color:#1d707b">Gra</span><span style="color:#f17f13">Sim</span>
|
|
</div>
|
|
<div style="font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:10px;font-weight:700;letter-spacing:.32em;color:#83a3a0;text-transform:uppercase;margin-top:8px">Sandbox für Geographie</div>
|
|
<div style="margin:14px auto 0;width:52px;height:3px;background:#f17f13;border-radius:2px"></div>
|
|
</td></tr>
|
|
|
|
<!-- Card -->
|
|
<tr><td style="background:#ffffff;border-radius:16px;padding:36px 36px 32px;box-shadow:0 4px 18px rgba(31,78,90,0.07);border:1px solid #e6e0d0">
|
|
{$content}
|
|
</td></tr>
|
|
|
|
<!-- Footer -->
|
|
<tr><td align="center" style="padding:22px 8px 4px">
|
|
<p style="font-size:12px;color:#9aa5a9;margin:0;line-height:1.55">
|
|
Diese Nachricht wurde automatisch von <a href="https://geograsim.at" style="color:#4a7c8a;text-decoration:none;font-weight:600">geograsim.at</a> versendet.
|
|
</p>
|
|
</td></tr>
|
|
|
|
</table>
|
|
|
|
</td></tr>
|
|
</table>
|
|
</body>
|
|
</html>
|
|
HTML;
|
|
}
|
|
}
|