Alle Frontend-Assets lokal ausliefern statt über CDNs
Inter, Font Awesome, Chart.js, qrcodejs und TinyMCE liegen jetzt unter assets/vendor/ und werden vom eigenen Server ausgeliefert. Warum: - Datenschutz: bisher ging bei jedem Seitenaufruf die IP der Nutzer an Google Fonts, cdnjs, jsDelivr und Tiny Cloud - Funktion: UniFi-Installationen stehen oft in abgeschotteten Netzen – dort fehlten bisher Schrift, Icons, Diagramme und Editor Neu: includes/Ui.php - Ui::head()/Ui::script() binden die Assets ein und hängen einen Versionsstempel an (?v=filemtime), damit Browser nach einem Update nicht das alte CSS aus dem Cache nehmen - Ui::themeScript() setzt das Theme aus der gespeicherten Auswahl oder – wenn keine vorliegt – aus prefers-color-scheme; global.js folgt Systemwechseln live, solange nichts manuell gewählt wurde - <meta name="color-scheme"> ergänzt, damit Formularelemente passen Der TinyMCE-API-Key entfällt: der Editor läuft immer lokal (GPL-Variante), inklusive deutscher Oberfläche, wenn die App auf Deutsch steht. Neu: tools/demo/build.py – baut aus dem Projekt eine Demo-Instanz ohne Datenbank (Stubs für Database/Auth, feste Beispieldaten). Damit lassen sich Screenshots reproduzierbar erzeugen und alle Seiten einmal rendern (Smoke-Test), ohne eine MySQL-Instanz aufzusetzen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ba90ffef03
commit
6da46f040a
57 changed files with 1479 additions and 267 deletions
|
|
@ -6,6 +6,7 @@ ini_set('log_errors', 1);
|
|||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Ui.php';
|
||||
require_once __DIR__ . '/../includes/UniFiController.php';
|
||||
require_once __DIR__ . '/../includes/I18n.php';
|
||||
|
||||
|
|
@ -90,7 +91,7 @@ $currentPage = 'dashboard';
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('dashboard_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<?= Ui::script('assets/vendor/chartjs/chart.umd.min.js', '../') ?>
|
||||
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||
|
||||
<div class="page-header">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ ini_set('log_errors', 1);
|
|||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Ui.php';
|
||||
require_once __DIR__ . '/../includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
|
|
@ -96,7 +97,7 @@ $adminBase = '';
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reporting – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||
<?= Ui::script('assets/vendor/chartjs/chart.umd.min.js', '../') ?>
|
||||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ ini_set('log_errors', 1);
|
|||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../includes/Database.php';
|
||||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Ui.php';
|
||||
|
||||
$auth = new Auth();
|
||||
$auth->requireLogin();
|
||||
|
|
@ -93,14 +94,9 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Zwei-Faktor-Authentifizierung – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<?php if (!$totpEnabled && $hasPassword): ?>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js', '../') ?>
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="../assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<?= Ui::head($db, '../') ?>
|
||||
</head>
|
||||
<body class="app-body focus-page">
|
||||
<div class="focus-card card">
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ require_once __DIR__ . '/../includes/Database.php';
|
|||
require_once __DIR__ . '/../includes/Auth.php';
|
||||
require_once __DIR__ . '/../includes/Mailer.php';
|
||||
require_once __DIR__ . '/../includes/I18n.php';
|
||||
require_once __DIR__ . '/../includes/Ui.php';
|
||||
|
||||
$auth = new Auth();
|
||||
$auth->requireAdmin();
|
||||
|
|
@ -113,7 +114,6 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
|||
}
|
||||
|
||||
if ($formType === 'system') {
|
||||
$settings['tinymce_api_key'] = trim($_POST['tinymce_api_key'] ?? '');
|
||||
$settings['print_template'] = $_POST['print_template'] ?? '';
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +203,6 @@ $cs = [
|
|||
'email_voucher_body' => $db->getSetting('email_voucher_body', "Hallo,\n\nIhr Code: {VOUCHER_CODE}\n\nGültigkeit: 8h\nGeräte: {MAX_USES}\nSite: {SITE_NAME}"),
|
||||
'email_user_notification_subject' => $db->getSetting('email_user_notification_subject', '{APP_TITLE} - Berechtigungen geändert'),
|
||||
'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"),
|
||||
'tinymce_api_key' => $db->getSetting('tinymce_api_key', ''),
|
||||
'print_template' => $db->getSetting('print_template', '<div style="text-align:center;padding:40px"><h1>{APP_TITLE}</h1><h2>WLAN Code</h2><div style="font-size:48px;font-weight:bold;margin:30px 0;font-family:monospace">{VOUCHER_CODE}</div><p><strong>Gültig bis:</strong> {EXPIRY_DATE} {EXPIRY_TIME}</p><p><strong>Site:</strong> {SITE_NAME}</p><p><strong>Geräte:</strong> {MAX_USES}</p><hr style="margin:30px 0"><div>{INSTRUCTIONS}</div></div>'),
|
||||
'login_panel_enabled' => $db->getSetting('login_panel_enabled', '1'),
|
||||
'login_brand_name' => $db->getSetting('login_brand_name', ''),
|
||||
|
|
@ -230,13 +229,9 @@ $adminBase = '';
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('settings_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
|
||||
|
||||
<?php if (!empty($cs['tinymce_api_key'])): ?>
|
||||
<script src="https://cdn.tiny.cloud/1/<?= htmlspecialchars($cs['tinymce_api_key']) ?>/tinymce/6/tinymce.min.js"></script>
|
||||
<?php else: ?>
|
||||
<!-- Ohne API-Key die GPL-Variante von cdnjs laden: gleiche Funktionen, kein Hinweis-Banner. -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.3/tinymce.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script>window.TINYMCE_BASE_URL = 'https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.3';</script>
|
||||
<?php endif; ?>
|
||||
<!-- TinyMCE wird lokal ausgeliefert (GPL-Variante) – keine externen Aufrufe. -->
|
||||
<?= Ui::script('assets/vendor/tinymce/tinymce.min.js', '../') ?>
|
||||
<script>window.TINYMCE_BASE_URL = '../assets/vendor/tinymce';</script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/admin_nav.php'; ?>
|
||||
|
||||
|
|
@ -531,10 +526,10 @@ $adminBase = '';
|
|||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="system">
|
||||
<div class="info-box">
|
||||
<h4><i class="fas fa-info-circle"></i> TinyMCE API Key</h4>
|
||||
<p>Kostenlosen API Key: <a href="https://www.tiny.cloud/auth/signup/" target="_blank" rel="noopener" style="color:#0066cc;">tiny.cloud/signup</a></p>
|
||||
<h4><i class="fas fa-info-circle"></i> WYSIWYG-Editor</h4>
|
||||
<p>Der Editor (TinyMCE, GPL-Variante) wird lokal aus <code>assets/vendor/</code> geladen – es werden keine externen Dienste aufgerufen.</p>
|
||||
</div>
|
||||
<div class="form-group"><label>TinyMCE API Key</label><input type="text" name="tinymce_api_key" value="<?= htmlspecialchars($cs['tinymce_api_key']) ?>" placeholder="your-api-key-here"><div class="help-text">Für WYSIWYG-Editor in Anleitungen</div></div>
|
||||
|
||||
<hr class="section-divider">
|
||||
<h3 style="margin-bottom:15px; color: var(--text-primary);">Druck-Template</h3>
|
||||
<div class="placeholder-info"><h4>Platzhalter:</h4><div class="placeholder-list"><code>{VOUCHER_CODE}</code><code>{EXPIRY_DATE}</code><code>{EXPIRY_TIME}</code><code>{SITE_NAME}</code><code>{MAX_USES}</code><code>{APP_TITLE}</code><code>{INSTRUCTIONS}</code></div></div>
|
||||
|
|
@ -655,6 +650,10 @@ function initTinyMCE() {
|
|||
tinymce.baseURL = window.TINYMCE_BASE_URL;
|
||||
config.base_url = window.TINYMCE_BASE_URL;
|
||||
config.suffix = '.min';
|
||||
if (document.documentElement.lang === 'de') {
|
||||
config.language = 'de';
|
||||
config.language_url = window.TINYMCE_BASE_URL + '/langs/de.js';
|
||||
}
|
||||
}
|
||||
tinymce.init(config);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,18 @@
|
|||
/* === DARK MODE === */
|
||||
/* === DARK MODE ===
|
||||
Reihenfolge: ausdrueckliche Auswahl des Nutzers > Systemeinstellung. */
|
||||
(function() {
|
||||
const saved = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', saved);
|
||||
const saved = localStorage.getItem('theme');
|
||||
const system = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
document.documentElement.setAttribute('data-theme', saved || system);
|
||||
|
||||
// Solange nichts ausgewaehlt wurde, folgt die Oberflaeche dem System.
|
||||
if (!saved && window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
|
||||
if (localStorage.getItem('theme')) return;
|
||||
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||
if (typeof updateDarkModeBtn === 'function') updateDarkModeBtn();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
function toggleDarkMode() {
|
||||
|
|
|
|||
30
assets/vendor/README.md
vendored
Normal file
30
assets/vendor/README.md
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Lokale Drittanbieter-Assets
|
||||
|
||||
Alle Frontend-Bibliotheken werden aus diesem Ordner ausgeliefert. Damit gibt es
|
||||
im Betrieb **keine Verbindungen zu externen CDNs** – wichtig für den Datenschutz
|
||||
(keine IP-Übertragung an Dritte) und für abgeschottete Netze ohne Internetzugang.
|
||||
|
||||
| Ordner | Inhalt | Version | Lizenz |
|
||||
|---|---|---|---|
|
||||
| `inter/` | Schriftschnitte 400/500/600/700 als woff2 | Inter 4.0 | SIL OFL 1.1 |
|
||||
| `fontawesome/` | Solid- und Brands-Icons, gekürzte CSS (nur woff2) | Font Awesome Free 6.4.0 | Icons CC BY 4.0, Fonts SIL OFL 1.1, Code MIT |
|
||||
| `chartjs/` | Diagramme für Dashboard und Reporting | Chart.js 4.4.0 | MIT |
|
||||
| `qrcodejs/` | QR-Code-Erzeugung im Browser | qrcodejs 1.0.0 | MIT |
|
||||
| `tinymce/` | WYSIWYG-Editor (GPL-Variante), auf die genutzten Plugins gekürzt | TinyMCE 6.8.3 | GPL-2.0-or-later |
|
||||
|
||||
Eingebunden werden sie über `Ui::head()` bzw. `Ui::script()` aus
|
||||
`includes/Ui.php` – inklusive Versionsstempel (`?v=<filemtime>`), damit Browser
|
||||
nach einem Update nicht die alten Dateien aus dem Cache verwenden.
|
||||
|
||||
## Aktualisieren
|
||||
|
||||
```bash
|
||||
# Beispiel Chart.js
|
||||
curl -L -o assets/vendor/chartjs/chart.umd.min.js \
|
||||
https://cdn.jsdelivr.net/npm/chart.js@<version>/dist/chart.umd.min.js
|
||||
```
|
||||
|
||||
Bei TinyMCE werden nur `tinymce.min.js`, `themes/silver`, `models/dom`,
|
||||
`icons/default`, die Skins `oxide`/`oxide-dark`, die deutsche Sprachdatei und
|
||||
die tatsächlich genutzten Plugins übernommen (siehe `initTinyMCE()` in
|
||||
`admin/settings.php`).
|
||||
20
assets/vendor/chartjs/chart.umd.min.js
vendored
Normal file
20
assets/vendor/chartjs/chart.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
11
assets/vendor/fontawesome/fontawesome.css
vendored
Normal file
11
assets/vendor/fontawesome/fontawesome.css
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
assets/vendor/fontawesome/webfonts/fa-brands-400.woff2
vendored
Normal file
BIN
assets/vendor/fontawesome/webfonts/fa-brands-400.woff2
vendored
Normal file
Binary file not shown.
BIN
assets/vendor/fontawesome/webfonts/fa-solid-900.woff2
vendored
Normal file
BIN
assets/vendor/fontawesome/webfonts/fa-solid-900.woff2
vendored
Normal file
Binary file not shown.
BIN
assets/vendor/inter/Inter-Bold.woff2
vendored
Normal file
BIN
assets/vendor/inter/Inter-Bold.woff2
vendored
Normal file
Binary file not shown.
BIN
assets/vendor/inter/Inter-Medium.woff2
vendored
Normal file
BIN
assets/vendor/inter/Inter-Medium.woff2
vendored
Normal file
Binary file not shown.
BIN
assets/vendor/inter/Inter-Regular.woff2
vendored
Normal file
BIN
assets/vendor/inter/Inter-Regular.woff2
vendored
Normal file
Binary file not shown.
BIN
assets/vendor/inter/Inter-SemiBold.woff2
vendored
Normal file
BIN
assets/vendor/inter/Inter-SemiBold.woff2
vendored
Normal file
Binary file not shown.
30
assets/vendor/inter/inter.css
vendored
Normal file
30
assets/vendor/inter/inter.css
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* Inter v4 – lokal ausgeliefert (SIL Open Font License 1.1).
|
||||
Quelle: https://github.com/rsms/inter/releases – nur die vier genutzten Schnitte. */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url('Inter-Regular.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
src: url('Inter-Medium.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
src: url('Inter-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
src: url('Inter-Bold.woff2') format('woff2');
|
||||
}
|
||||
1
assets/vendor/qrcodejs/qrcode.min.js
vendored
Normal file
1
assets/vendor/qrcodejs/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/vendor/tinymce/icons/default/icons.min.js
vendored
Normal file
1
assets/vendor/tinymce/icons/default/icons.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
406
assets/vendor/tinymce/langs/de.js
vendored
Normal file
406
assets/vendor/tinymce/langs/de.js
vendored
Normal file
|
|
@ -0,0 +1,406 @@
|
|||
tinymce.addI18n("de", {
|
||||
"Redo": "Wiederholen",
|
||||
"Undo": "R\xfcckg\xe4ngig machen",
|
||||
"Cut": "Ausschneiden",
|
||||
"Copy": "Kopieren",
|
||||
"Paste": "Einf\xfcgen",
|
||||
"Select all": "Alles ausw\xe4hlen",
|
||||
"New document": "Neues Dokument",
|
||||
"Ok": "Ok",
|
||||
"Cancel": "Abbrechen",
|
||||
"Visual aids": "Visuelle Hilfen",
|
||||
"Bold": "Fett",
|
||||
"Italic": "Kursiv",
|
||||
"Underline": "Unterstrichen",
|
||||
"Strikethrough": "Durchgestrichen",
|
||||
"Superscript": "Hochgestellt",
|
||||
"Subscript": "Tiefgestellt",
|
||||
"Clear formatting": "Formatierung entfernen",
|
||||
"Remove": "Entfernen",
|
||||
"Align left": "Linksb\xfcndig ausrichten",
|
||||
"Align center": "Zentrieren",
|
||||
"Align right": "Rechtsb\xfcndig ausrichten",
|
||||
"No alignment": "Keine Ausrichtung",
|
||||
"Justify": "Blocksatz",
|
||||
"Bullet list": "Aufz\xe4hlung",
|
||||
"Numbered list": "Nummerierte Liste",
|
||||
"Decrease indent": "Einzug verkleinern",
|
||||
"Increase indent": "Einzug vergr\xf6\xdfern",
|
||||
"Close": "Schlie\xdfen",
|
||||
"Formats": "Formate",
|
||||
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.": "Ihr Browser unterst\xfctzt leider keinen direkten Zugriff auf die Zwischenablage. Bitte benutzen Sie die Tastenkombinationen Strg+X/C/V.",
|
||||
"Headings": "\xdcberschriften",
|
||||
"Heading 1": "\xdcberschrift 1",
|
||||
"Heading 2": "\xdcberschrift 2",
|
||||
"Heading 3": "\xdcberschrift 3",
|
||||
"Heading 4": "\xdcberschrift 4",
|
||||
"Heading 5": "\xdcberschrift 5",
|
||||
"Heading 6": "\xdcberschrift 6",
|
||||
"Preformatted": "Vorformatiert",
|
||||
"Div": "Div",
|
||||
"Pre": "Pre",
|
||||
"Code": "Code",
|
||||
"Paragraph": "Absatz",
|
||||
"Blockquote": "Blockzitat",
|
||||
"Inline": "Zeichenformate",
|
||||
"Blocks": "Bl\xf6cke",
|
||||
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "Einf\xfcgen ist nun im unformatierten Textmodus. Inhalte werden ab jetzt als unformatierter Text eingef\xfcgt, bis Sie diese Einstellung wieder deaktivieren.",
|
||||
"Fonts": "Schriftarten",
|
||||
"Font sizes": "Schriftgr\xf6\xdfen",
|
||||
"Class": "Klasse",
|
||||
"Browse for an image": "Bild...",
|
||||
"OR": "ODER",
|
||||
"Drop an image here": "Bild hier ablegen",
|
||||
"Upload": "Hochladen",
|
||||
"Uploading image": "Bild wird hochgeladen",
|
||||
"Block": "Blocksatz",
|
||||
"Align": "Ausrichtung",
|
||||
"Default": "Standard",
|
||||
"Circle": "Kreis",
|
||||
"Disc": "Scheibe",
|
||||
"Square": "Rechteck",
|
||||
"Lower Alpha": "Lateinisches Alphabet in Kleinbuchstaben",
|
||||
"Lower Greek": "Griechische Kleinbuchstaben",
|
||||
"Lower Roman": "Kleiner r\xf6mischer Buchstabe",
|
||||
"Upper Alpha": "Lateinisches Alphabet in Gro\xdfbuchstaben",
|
||||
"Upper Roman": "Gro\xdfer r\xf6mischer Buchstabe",
|
||||
"Anchor...": "Textmarke",
|
||||
"Anchor": "Anker",
|
||||
"Name": "Name",
|
||||
"ID": "ID",
|
||||
"ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "Die ID muss mit einem Buchstaben beginnen gefolgt von Buchstaben, Zahlen, Bindestrichen, Punkten, Doppelpunkten oder Unterstrichen.",
|
||||
"You have unsaved changes are you sure you want to navigate away?": "Die \xc4nderungen wurden noch nicht gespeichert. Sind Sie sicher, dass Sie diese Seite verlassen wollen?",
|
||||
"Restore last draft": "Letzten Entwurf wiederherstellen",
|
||||
"Special character...": "Sonderzeichen...",
|
||||
"Special Character": "Sonderzeichen",
|
||||
"Source code": "Quellcode",
|
||||
"Insert/Edit code sample": "Codebeispiel einf\xfcgen/bearbeiten",
|
||||
"Language": "Sprache",
|
||||
"Code sample...": "Codebeispiel...",
|
||||
"Left to right": "Von links nach rechts",
|
||||
"Right to left": "Von rechts nach links",
|
||||
"Title": "Titel",
|
||||
"Fullscreen": "Vollbild",
|
||||
"Action": "Aktion",
|
||||
"Shortcut": "Tastenkombination",
|
||||
"Help": "Hilfe",
|
||||
"Address": "Adresse",
|
||||
"Focus to menubar": "Fokus auf Men\xfcleiste",
|
||||
"Focus to toolbar": "Fokus auf Symbolleiste",
|
||||
"Focus to element path": "Fokus auf Elementpfad",
|
||||
"Focus to contextual toolbar": "Fokus auf kontextbezogene Symbolleiste",
|
||||
"Insert link (if link plugin activated)": "Link einf\xfcgen (wenn Link-Plugin aktiviert ist)",
|
||||
"Save (if save plugin activated)": "Speichern (wenn Save-Plugin aktiviert ist)",
|
||||
"Find (if searchreplace plugin activated)": "Suchen (wenn Suchen/Ersetzen-Plugin aktiviert ist)",
|
||||
"Plugins installed ({0}):": "Installierte Plugins ({0}):",
|
||||
"Premium plugins:": "Premium-Plugins:",
|
||||
"Learn more...": "Erfahren Sie mehr dazu...",
|
||||
"You are using {0}": "Sie verwenden {0}",
|
||||
"Plugins": "Plugins",
|
||||
"Handy Shortcuts": "Praktische Tastenkombinationen",
|
||||
"Horizontal line": "Horizontale Linie",
|
||||
"Insert/edit image": "Bild einf\xfcgen/bearbeiten",
|
||||
"Alternative description": "Alternative Beschreibung",
|
||||
"Accessibility": "Barrierefreiheit",
|
||||
"Image is decorative": "Bild ist dekorativ",
|
||||
"Source": "Quelle",
|
||||
"Dimensions": "Abmessungen",
|
||||
"Constrain proportions": "Seitenverh\xe4ltnis beibehalten",
|
||||
"General": "Allgemein",
|
||||
"Advanced": "Erweitert",
|
||||
"Style": "Formatvorlage",
|
||||
"Vertical space": "Vertikaler Raum",
|
||||
"Horizontal space": "Horizontaler Raum",
|
||||
"Border": "Rahmen",
|
||||
"Insert image": "Bild einf\xfcgen",
|
||||
"Image...": "Bild...",
|
||||
"Image list": "Bildliste",
|
||||
"Resize": "Skalieren",
|
||||
"Insert date/time": "Datum/Uhrzeit einf\xfcgen",
|
||||
"Date/time": "Datum/Uhrzeit",
|
||||
"Insert/edit link": "Link einf\xfcgen/bearbeiten",
|
||||
"Text to display": "Anzuzeigender Text",
|
||||
"Url": "URL",
|
||||
"Open link in...": "Link \xf6ffnen in...",
|
||||
"Current window": "Aktuelles Fenster",
|
||||
"None": "Keine",
|
||||
"New window": "Neues Fenster",
|
||||
"Open link": "Link \xf6ffnen",
|
||||
"Remove link": "Link entfernen",
|
||||
"Anchors": "Anker",
|
||||
"Link...": "Link...",
|
||||
"Paste or type a link": "Link einf\xfcgen oder eingeben",
|
||||
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "Diese URL scheint eine E-Mail-Adresse zu sein. M\xf6chten Sie das dazu ben\xf6tigte mailto: voranstellen?",
|
||||
"The URL you entered seems to be an external link. Do you want to add the required http:// prefix?": "Diese URL scheint ein externer Link zu sein. M\xf6chten Sie das dazu ben\xf6tigte http:// voranstellen?",
|
||||
"The URL you entered seems to be an external link. Do you want to add the required https:// prefix?": "Die eingegebene URL scheint ein externer Link zu sein. Soll das fehlende https:// davor erg\xe4nzt werden?",
|
||||
"Link list": "Linkliste",
|
||||
"Insert video": "Video einf\xfcgen",
|
||||
"Insert/edit video": "Video einf\xfcgen/bearbeiten",
|
||||
"Insert/edit media": "Medien einf\xfcgen/bearbeiten",
|
||||
"Alternative source": "Alternative Quelle",
|
||||
"Alternative source URL": "URL der alternativen Quelle",
|
||||
"Media poster (Image URL)": "Medienposter (Bild-URL)",
|
||||
"Paste your embed code below:": "F\xfcgen Sie Ihren Einbettungscode unten ein:",
|
||||
"Embed": "Einbettung",
|
||||
"Media...": "Medien...",
|
||||
"Nonbreaking space": "Gesch\xfctztes Leerzeichen",
|
||||
"Page break": "Seitenumbruch",
|
||||
"Paste as text": "Als Text einf\xfcgen",
|
||||
"Preview": "Vorschau",
|
||||
"Print": "Drucken",
|
||||
"Print...": "Drucken...",
|
||||
"Save": "Speichern",
|
||||
"Find": "Suchen",
|
||||
"Replace with": "Ersetzen durch",
|
||||
"Replace": "Ersetzen",
|
||||
"Replace all": "Alle ersetzen",
|
||||
"Previous": "Vorherige",
|
||||
"Next": "N\xe4chste",
|
||||
"Find and Replace": "Suchen und Ersetzen",
|
||||
"Find and replace...": "Suchen und ersetzen...",
|
||||
"Could not find the specified string.": "Die angegebene Zeichenfolge wurde nicht gefunden.",
|
||||
"Match case": "Gro\xdf-/Kleinschreibung beachten",
|
||||
"Find whole words only": "Nur ganze W\xf6rter suchen",
|
||||
"Find in selection": "In Auswahl suchen",
|
||||
"Insert table": "Tabelle einf\xfcgen",
|
||||
"Table properties": "Tabelleneigenschaften",
|
||||
"Delete table": "Tabelle l\xf6schen",
|
||||
"Cell": "Zelle",
|
||||
"Row": "Zeile",
|
||||
"Column": "Spalte",
|
||||
"Cell properties": "Zelleigenschaften",
|
||||
"Merge cells": "Zellen verbinden",
|
||||
"Split cell": "Zelle aufteilen",
|
||||
"Insert row before": "Neue Zeile davor einf\xfcgen",
|
||||
"Insert row after": "Neue Zeile danach einf\xfcgen",
|
||||
"Delete row": "Zeile l\xf6schen",
|
||||
"Row properties": "Zeileneigenschaften",
|
||||
"Cut row": "Zeile ausschneiden",
|
||||
"Cut column": "Spalte ausschneiden",
|
||||
"Copy row": "Zeile kopieren",
|
||||
"Copy column": "Spalte kopieren",
|
||||
"Paste row before": "Zeile davor einf\xfcgen",
|
||||
"Paste column before": "Spalte davor einf\xfcgen",
|
||||
"Paste row after": "Zeile danach einf\xfcgen",
|
||||
"Paste column after": "Spalte danach einf\xfcgen",
|
||||
"Insert column before": "Neue Spalte davor einf\xfcgen",
|
||||
"Insert column after": "Neue Spalte danach einf\xfcgen",
|
||||
"Delete column": "Spalte l\xf6schen",
|
||||
"Cols": "Spalten",
|
||||
"Rows": "Zeilen",
|
||||
"Width": "Breite",
|
||||
"Height": "H\xf6he",
|
||||
"Cell spacing": "Zellenabstand",
|
||||
"Cell padding": "Zelleninnenabstand",
|
||||
"Row clipboard actions": "Zeilen-Zwischenablage-Aktionen",
|
||||
"Column clipboard actions": "Spalten-Zwischenablage-Aktionen",
|
||||
"Table styles": "Tabellenstil",
|
||||
"Cell styles": "Zellstil",
|
||||
"Column header": "Spaltenkopf",
|
||||
"Row header": "Zeilenkopf",
|
||||
"Table caption": "Tabellenbeschriftung",
|
||||
"Caption": "Beschriftung",
|
||||
"Show caption": "Beschriftung anzeigen",
|
||||
"Left": "Links",
|
||||
"Center": "Zentriert",
|
||||
"Right": "Rechts",
|
||||
"Cell type": "Zelltyp",
|
||||
"Scope": "Bereich",
|
||||
"Alignment": "Ausrichtung",
|
||||
"Horizontal align": "Horizontal ausrichten",
|
||||
"Vertical align": "Vertikal ausrichten",
|
||||
"Top": "Oben",
|
||||
"Middle": "Mitte",
|
||||
"Bottom": "Unten",
|
||||
"Header cell": "Kopfzelle",
|
||||
"Row group": "Zeilengruppe",
|
||||
"Column group": "Spaltengruppe",
|
||||
"Row type": "Zeilentyp",
|
||||
"Header": "Kopfzeile",
|
||||
"Body": "Inhalt",
|
||||
"Footer": "Fu\xdfzeile",
|
||||
"Border color": "Rahmenfarbe",
|
||||
"Solid": "Durchgezogen",
|
||||
"Dotted": "Gepunktet",
|
||||
"Dashed": "Gestrichelt",
|
||||
"Double": "Doppelt",
|
||||
"Groove": "Gekantet",
|
||||
"Ridge": "Eingeritzt",
|
||||
"Inset": "Eingelassen",
|
||||
"Outset": "Hervorstehend",
|
||||
"Hidden": "Unsichtbar",
|
||||
"Insert template...": "Vorlage einf\xfcgen...",
|
||||
"Templates": "Vorlagen",
|
||||
"Template": "Vorlage",
|
||||
"Insert Template": "Vorlage einf\xfcgen",
|
||||
"Text color": "Textfarbe",
|
||||
"Background color": "Hintergrundfarbe",
|
||||
"Custom...": "Benutzerdefiniert...",
|
||||
"Custom color": "Benutzerdefinierte Farbe",
|
||||
"No color": "Keine Farbe",
|
||||
"Remove color": "Farbauswahl aufheben",
|
||||
"Show blocks": "Bl\xf6cke anzeigen",
|
||||
"Show invisible characters": "Unsichtbare Zeichen anzeigen",
|
||||
"Word count": "Anzahl der W\xf6rter",
|
||||
"Count": "Anzahl",
|
||||
"Document": "Dokument",
|
||||
"Selection": "Auswahl",
|
||||
"Words": "W\xf6rter",
|
||||
"Words: {0}": "Wortzahl: {0}",
|
||||
"{0} words": "{0} W\xf6rter",
|
||||
"File": "Datei",
|
||||
"Edit": "Bearbeiten",
|
||||
"Insert": "Einf\xfcgen",
|
||||
"View": "Ansicht",
|
||||
"Format": "Format",
|
||||
"Table": "Tabelle",
|
||||
"Tools": "Werkzeuge",
|
||||
"Powered by {0}": "Betrieben von {0}",
|
||||
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "Rich-Text-Bereich. Dr\xfccken Sie Alt+F9 f\xfcr das Men\xfc. Dr\xfccken Sie Alt+F10 f\xfcr die Symbolleiste. Dr\xfccken Sie Alt+0 f\xfcr Hilfe.",
|
||||
"Image title": "Bildtitel",
|
||||
"Border width": "Rahmenbreite",
|
||||
"Border style": "Rahmenstil",
|
||||
"Error": "Fehler",
|
||||
"Warn": "Warnung",
|
||||
"Valid": "G\xfcltig",
|
||||
"To open the popup, press Shift+Enter": "Dr\xfccken Sie Umschalt+Eingabe, um das Popup-Fenster zu \xf6ffnen.",
|
||||
"Rich Text Area": "Rich-Text-Area",
|
||||
"Rich Text Area. Press ALT-0 for help.": "Rich-Text-Bereich. Dr\xfccken Sie Alt+0 f\xfcr Hilfe.",
|
||||
"System Font": "Betriebssystemschriftart",
|
||||
"Failed to upload image: {0}": "Bild konnte nicht hochgeladen werden: {0}",
|
||||
"Failed to load plugin: {0} from url {1}": "Plugin konnte nicht geladen werden: {0} von URL {1}",
|
||||
"Failed to load plugin url: {0}": "Plugin-URL konnte nicht geladen werden: {0}",
|
||||
"Failed to initialize plugin: {0}": "Plugin konnte nicht initialisiert werden: {0}",
|
||||
"example": "Beispiel",
|
||||
"Search": "Suchen",
|
||||
"All": "Alle",
|
||||
"Currency": "W\xe4hrung",
|
||||
"Text": "Text",
|
||||
"Quotations": "Anf\xfchrungszeichen",
|
||||
"Mathematical": "Mathematisch",
|
||||
"Extended Latin": "Erweitertes Latein",
|
||||
"Symbols": "Symbole",
|
||||
"Arrows": "Pfeile",
|
||||
"User Defined": "Benutzerdefiniert",
|
||||
"dollar sign": "Dollarzeichen",
|
||||
"currency sign": "W\xe4hrungssymbol",
|
||||
"euro-currency sign": "Eurozeichen",
|
||||
"colon sign": "Doppelpunkt",
|
||||
"cruzeiro sign": "Cruzeirozeichen",
|
||||
"french franc sign": "Franczeichen",
|
||||
"lira sign": "Lirezeichen",
|
||||
"mill sign": "Millzeichen",
|
||||
"naira sign": "Nairazeichen",
|
||||
"peseta sign": "Pesetazeichen",
|
||||
"rupee sign": "Rupiezeichen",
|
||||
"won sign": "Wonzeichen",
|
||||
"new sheqel sign": "Schekelzeichen",
|
||||
"dong sign": "Dongzeichen",
|
||||
"kip sign": "Kipzeichen",
|
||||
"tugrik sign": "Tugrikzeichen",
|
||||
"drachma sign": "Drachmezeichen",
|
||||
"german penny symbol": "Pfennigzeichen",
|
||||
"peso sign": "Pesozeichen",
|
||||
"guarani sign": "Guaranizeichen",
|
||||
"austral sign": "Australzeichen",
|
||||
"hryvnia sign": "Hrywnjazeichen",
|
||||
"cedi sign": "Cedizeichen",
|
||||
"livre tournois sign": "Livrezeichen",
|
||||
"spesmilo sign": "Spesmilozeichen",
|
||||
"tenge sign": "Tengezeichen",
|
||||
"indian rupee sign": "Indisches Rupiezeichen",
|
||||
"turkish lira sign": "T\xfcrkisches Lirazeichen",
|
||||
"nordic mark sign": "Zeichen nordische Mark",
|
||||
"manat sign": "Manatzeichen",
|
||||
"ruble sign": "Rubelzeichen",
|
||||
"yen character": "Yenzeichen",
|
||||
"yuan character": "Yuanzeichen",
|
||||
"yuan character, in hong kong and taiwan": "Yuanzeichen in Hongkong und Taiwan",
|
||||
"yen/yuan character variant one": "Yen-/Yuanzeichen Variante 1",
|
||||
"Emojis": "Emojis",
|
||||
"Emojis...": "Emojis...",
|
||||
"Loading emojis...": "Lade Emojis...",
|
||||
"Could not load emojis": "Emojis konnten nicht geladen werden",
|
||||
"People": "Menschen",
|
||||
"Animals and Nature": "Tiere und Natur",
|
||||
"Food and Drink": "Essen und Trinken",
|
||||
"Activity": "Aktivit\xe4t",
|
||||
"Travel and Places": "Reisen und Orte",
|
||||
"Objects": "Objekte",
|
||||
"Flags": "Flaggen",
|
||||
"Characters": "Zeichen",
|
||||
"Characters (no spaces)": "Zeichen (ohne Leerzeichen)",
|
||||
"{0} characters": "{0}\xa0Zeichen",
|
||||
"Error: Form submit field collision.": "Fehler: Kollision der Formularbest\xe4tigungsfelder.",
|
||||
"Error: No form element found.": "Fehler: Kein Formularelement gefunden.",
|
||||
"Color swatch": "Farbpalette",
|
||||
"Color Picker": "Farbwahl",
|
||||
"Invalid hex color code: {0}": "Ung\xfcltiger Hexadezimal-Farbwert: {0}",
|
||||
"Invalid input": "Ung\xfcltige Eingabe",
|
||||
"R": "R",
|
||||
"Red component": "Rotanteil",
|
||||
"G": "G",
|
||||
"Green component": "Gr\xfcnanteil",
|
||||
"B": "B",
|
||||
"Blue component": "Blauanteil",
|
||||
"#": "#",
|
||||
"Hex color code": "Hexadezimal-Farbwert",
|
||||
"Range 0 to 255": "Spanne 0 bis 255",
|
||||
"Turquoise": "T\xfcrkis",
|
||||
"Green": "Gr\xfcn",
|
||||
"Blue": "Blau",
|
||||
"Purple": "Violett",
|
||||
"Navy Blue": "Marineblau",
|
||||
"Dark Turquoise": "Dunkelt\xfcrkis",
|
||||
"Dark Green": "Dunkelgr\xfcn",
|
||||
"Medium Blue": "Mittleres Blau",
|
||||
"Medium Purple": "Mittelviolett",
|
||||
"Midnight Blue": "Mitternachtsblau",
|
||||
"Yellow": "Gelb",
|
||||
"Orange": "Orange",
|
||||
"Red": "Rot",
|
||||
"Light Gray": "Hellgrau",
|
||||
"Gray": "Grau",
|
||||
"Dark Yellow": "Dunkelgelb",
|
||||
"Dark Orange": "Dunkelorange",
|
||||
"Dark Red": "Dunkelrot",
|
||||
"Medium Gray": "Mittelgrau",
|
||||
"Dark Gray": "Dunkelgrau",
|
||||
"Light Green": "Hellgr\xfcn",
|
||||
"Light Yellow": "Hellgelb",
|
||||
"Light Red": "Hellrot",
|
||||
"Light Purple": "Helllila",
|
||||
"Light Blue": "Hellblau",
|
||||
"Dark Purple": "Dunkellila",
|
||||
"Dark Blue": "Dunkelblau",
|
||||
"Black": "Schwarz",
|
||||
"White": "Wei\xdf",
|
||||
"Switch to or from fullscreen mode": "Vollbildmodus umschalten",
|
||||
"Open help dialog": "Hilfe-Dialog \xf6ffnen",
|
||||
"history": "Historie",
|
||||
"styles": "Stile",
|
||||
"formatting": "Formatierung",
|
||||
"alignment": "Ausrichtung",
|
||||
"indentation": "Einr\xfcckungen",
|
||||
"Font": "Schriftart",
|
||||
"Size": "Schriftgr\xf6\xdfe",
|
||||
"More...": "Mehr...",
|
||||
"Select...": "Auswahl...",
|
||||
"Preferences": "Einstellungen",
|
||||
"Yes": "Ja",
|
||||
"No": "Nein",
|
||||
"Keyboard Navigation": "Tastaturnavigation",
|
||||
"Version": "Version",
|
||||
"Code view": "Code Ansicht",
|
||||
"Open popup menu for split buttons": "\xd6ffne Popup Menge um Buttons zu trennen",
|
||||
"List Properties": "Liste Eigenschaften",
|
||||
"List properties...": "Liste Eigenschaften",
|
||||
"Start list at number": "Beginne Liste mit Nummer",
|
||||
"Line height": "Liniendicke",
|
||||
"Dropped file type is not supported": "Hereingezogener Dateityp wird nicht unterst\xfctzt",
|
||||
"Loading...": "Wird geladen...",
|
||||
"ImageProxy HTTP error: Rejected request": "Image Proxy HTTP Fehler: Abgewiesene Anfrage",
|
||||
"ImageProxy HTTP error: Could not find Image Proxy": "Image Proxy HTTP Fehler: Kann Image Proxy nicht finden",
|
||||
"ImageProxy HTTP error: Incorrect Image Proxy URL": "Image Proxy HTTP Fehler: Falsche Image Proxy URL",
|
||||
"ImageProxy HTTP error: Unknown ImageProxy error": "Image Proxy HTTP Fehler: Unbekannter Image Proxy Fehler"
|
||||
});
|
||||
21
assets/vendor/tinymce/license.txt
vendored
Normal file
21
assets/vendor/tinymce/license.txt
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
assets/vendor/tinymce/models/dom/model.min.js
vendored
Normal file
4
assets/vendor/tinymce/models/dom/model.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/plugins/advlist/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/advlist/plugin.min.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* TinyMCE version 6.8.3 (2024-02-08)
|
||||
*/
|
||||
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const e=(t,e,s)=>{const r="UL"===e?"InsertUnorderedList":"InsertOrderedList";t.execCommand(r,!1,!1===s?null:{"list-style-type":s})},s=t=>e=>e.options.get(t),r=s("advlist_number_styles"),n=s("advlist_bullet_styles"),i=t=>null==t,l=t=>!i(t);var o=tinymce.util.Tools.resolve("tinymce.util.Tools");class a{constructor(t,e){this.tag=t,this.value=e}static some(t){return new a(!0,t)}static none(){return a.singletonNone}fold(t,e){return this.tag?e(this.value):t()}isSome(){return this.tag}isNone(){return!this.tag}map(t){return this.tag?a.some(t(this.value)):a.none()}bind(t){return this.tag?t(this.value):a.none()}exists(t){return this.tag&&t(this.value)}forall(t){return!this.tag||t(this.value)}filter(t){return!this.tag||t(this.value)?this:a.none()}getOr(t){return this.tag?this.value:t}or(t){return this.tag?this:t}getOrThunk(t){return this.tag?this.value:t()}orThunk(t){return this.tag?this:t()}getOrDie(t){if(this.tag)return this.value;throw new Error(null!=t?t:"Called getOrDie on None")}static from(t){return l(t)?a.some(t):a.none()}getOrNull(){return this.tag?this.value:null}getOrUndefined(){return this.value}each(t){this.tag&&t(this.value)}toArray(){return this.tag?[this.value]:[]}toString(){return this.tag?`some(${this.value})`:"none()"}}a.singletonNone=new a(!1);const u=t=>e=>l(e)&&t.test(e.nodeName),d=u(/^(OL|UL|DL)$/),g=u(/^(TH|TD)$/),c=t=>i(t)||"default"===t?"":t,h=(t,e)=>s=>((t,e)=>{const s=t.selection.getNode();return e({parents:t.dom.getParents(s),element:s}),t.on("NodeChange",e),()=>t.off("NodeChange",e)})(t,(r=>((t,r)=>{const n=t.selection.getStart(!0);s.setActive(((t,e,s)=>((t,e,s)=>{for(let e=0,n=t.length;e<n;e++){const n=t[e];if(d(r=n)&&!/\btox\-/.test(r.className))return a.some(n);if(s(n,e))break}var r;return a.none()})(e,0,g).exists((e=>e.nodeName===s&&((t,e)=>t.dom.isChildOf(e,t.getBody()))(t,e))))(t,r,e)),s.setEnabled(!((t,e)=>{const s=t.dom.getParent(e,"ol,ul,dl");return((t,e)=>null!==e&&!t.dom.isEditable(e))(t,s)&&t.selection.isEditable()})(t,n)&&t.selection.isEditable())})(t,r.parents))),m=(t,s,r,n,i,l)=>{l.length>1?((t,s,r,n,i,l)=>{t.ui.registry.addSplitButton(s,{tooltip:r,icon:"OL"===i?"ordered-list":"unordered-list",presets:"listpreview",columns:3,fetch:t=>{t(o.map(l,(t=>{const e="OL"===i?"num":"bull",s="disc"===t||"decimal"===t?"default":t,r=c(t),n=(t=>t.replace(/\-/g," ").replace(/\b\w/g,(t=>t.toUpperCase())))(t);return{type:"choiceitem",value:r,icon:"list-"+e+"-"+s,text:n}})))},onAction:()=>t.execCommand(n),onItemAction:(s,r)=>{e(t,i,r)},select:e=>{const s=(t=>{const e=t.dom.getParent(t.selection.getNode(),"ol,ul"),s=t.dom.getStyle(e,"listStyleType");return a.from(s)})(t);return s.map((t=>e===t)).getOr(!1)},onSetup:h(t,i)})})(t,s,r,n,i,l):((t,s,r,n,i,l)=>{t.ui.registry.addToggleButton(s,{active:!1,tooltip:r,icon:"OL"===i?"ordered-list":"unordered-list",onSetup:h(t,i),onAction:()=>t.queryCommandState(n)||""===l?t.execCommand(n):e(t,i,l)})})(t,s,r,n,i,c(l[0]))};t.add("advlist",(t=>{t.hasPlugin("lists")?((t=>{const e=t.options.register;e("advlist_number_styles",{processor:"string[]",default:"default,lower-alpha,lower-greek,lower-roman,upper-alpha,upper-roman".split(",")}),e("advlist_bullet_styles",{processor:"string[]",default:"default,circle,square".split(",")})})(t),(t=>{m(t,"numlist","Numbered list","InsertOrderedList","OL",r(t)),m(t,"bullist","Bullet list","InsertUnorderedList","UL",n(t))})(t),(t=>{t.addCommand("ApplyUnorderedListStyle",((s,r)=>{e(t,"UL",r["list-style-type"])})),t.addCommand("ApplyOrderedListStyle",((s,r)=>{e(t,"OL",r["list-style-type"])}))})(t)):console.error("Please use the Lists plugin together with the Advanced List plugin.")}))}();
|
||||
4
assets/vendor/tinymce/plugins/autolink/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/autolink/plugin.min.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* TinyMCE version 6.8.3 (2024-02-08)
|
||||
*/
|
||||
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager");const t=e=>t=>t.options.get(e),n=t("autolink_pattern"),o=t("link_default_target"),r=t("link_default_protocol"),a=t("allow_unsafe_link_target"),s=("string",e=>"string"===(e=>{const t=typeof e;return null===e?"null":"object"===t&&Array.isArray(e)?"array":"object"===t&&(n=o=e,(r=String).prototype.isPrototypeOf(n)||(null===(a=o.constructor)||void 0===a?void 0:a.name)===r.name)?"string":t;var n,o,r,a})(e));const l=(void 0,e=>undefined===e);const i=e=>!(e=>null==e)(e),c=Object.hasOwnProperty,d=e=>"\ufeff"===e;var u=tinymce.util.Tools.resolve("tinymce.dom.TextSeeker");const f=e=>/^[(\[{ \u00a0]$/.test(e),g=(e,t,n)=>{for(let o=t-1;o>=0;o--){const t=e.charAt(o);if(!d(t)&&n(t))return o}return-1},m=(e,t)=>{var o;const a=e.schema.getVoidElements(),s=n(e),{dom:i,selection:d}=e;if(null!==i.getParent(d.getNode(),"a[href]"))return null;const m=d.getRng(),k=u(i,(e=>{return i.isBlock(e)||(t=a,n=e.nodeName.toLowerCase(),c.call(t,n))||"false"===i.getContentEditable(e);var t,n})),{container:p,offset:y}=((e,t)=>{let n=e,o=t;for(;1===n.nodeType&&n.childNodes[o];)n=n.childNodes[o],o=3===n.nodeType?n.data.length:n.childNodes.length;return{container:n,offset:o}})(m.endContainer,m.endOffset),w=null!==(o=i.getParent(p,i.isBlock))&&void 0!==o?o:i.getRoot(),h=k.backwards(p,y+t,((e,t)=>{const n=e.data,o=g(n,t,(r=f,e=>!r(e)));var r,a;return-1===o||(a=n[o],/[?!,.;:]/.test(a))?o:o+1}),w);if(!h)return null;let v=h.container;const _=k.backwards(h.container,h.offset,((e,t)=>{v=e;const n=g(e.data,t,f);return-1===n?n:n+1}),w),A=i.createRng();_?A.setStart(_.container,_.offset):A.setStart(v,0),A.setEnd(h.container,h.offset);const C=A.toString().replace(/\uFEFF/g,"").match(s);if(C){let t=C[0];return $="www.",(b=t).length>=4&&b.substr(0,4)===$?t=r(e)+"://"+t:((e,t,n=0,o)=>{const r=e.indexOf(t,n);return-1!==r&&(!!l(o)||r+t.length<=o)})(t,"@")&&!(e=>/^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(e))(t)&&(t="mailto:"+t),{rng:A,url:t}}var b,$;return null},k=(e,t)=>{const{dom:n,selection:r}=e,{rng:l,url:i}=t,c=r.getBookmark();r.setRng(l);const d="createlink",u={command:d,ui:!1,value:i};if(!e.dispatch("BeforeExecCommand",u).isDefaultPrevented()){e.getDoc().execCommand(d,!1,i),e.dispatch("ExecCommand",u);const t=o(e);if(s(t)){const o=r.getNode();n.setAttrib(o,"target",t),"_blank"!==t||a(e)||n.setAttrib(o,"rel","noopener")}}r.moveToBookmark(c),e.nodeChanged()},p=e=>{const t=m(e,-1);i(t)&&k(e,t)},y=p;e.add("autolink",(e=>{(e=>{const t=e.options.register;t("autolink_pattern",{processor:"regexp",default:new RegExp("^"+/(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g.source+"$","i")}),t("link_default_target",{processor:"string"}),t("link_default_protocol",{processor:"string",default:"https"})})(e),(e=>{e.on("keydown",(t=>{13!==t.keyCode||t.isDefaultPrevented()||(e=>{const t=m(e,0);i(t)&&k(e,t)})(e)})),e.on("keyup",(t=>{32===t.keyCode?p(e):(48===t.keyCode&&t.shiftKey||221===t.keyCode)&&y(e)}))})(e)}))}();
|
||||
4
assets/vendor/tinymce/plugins/code/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/code/plugin.min.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* TinyMCE version 6.8.3 (2024-02-08)
|
||||
*/
|
||||
!function(){"use strict";tinymce.util.Tools.resolve("tinymce.PluginManager").add("code",(e=>((e=>{e.addCommand("mceCodeEditor",(()=>{(e=>{const o=(e=>e.getContent({source_view:!0}))(e);e.windowManager.open({title:"Source Code",size:"large",body:{type:"panel",items:[{type:"textarea",name:"code"}]},buttons:[{type:"cancel",name:"cancel",text:"Cancel"},{type:"submit",name:"save",text:"Save",primary:!0}],initialData:{code:o},onSubmit:o=>{((e,o)=>{e.focus(),e.undoManager.transact((()=>{e.setContent(o)})),e.selection.setCursorLocation(),e.nodeChanged()})(e,o.getData().code),o.close()}})})(e)}))})(e),(e=>{const o=()=>e.execCommand("mceCodeEditor");e.ui.registry.addButton("code",{icon:"sourcecode",tooltip:"Source code",onAction:o}),e.ui.registry.addMenuItem("code",{icon:"sourcecode",text:"Source code",onAction:o})})(e),{})))}();
|
||||
4
assets/vendor/tinymce/plugins/fullscreen/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/fullscreen/plugin.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
90
assets/vendor/tinymce/plugins/help/js/i18n/keynav/de.js
vendored
Normal file
90
assets/vendor/tinymce/plugins/help/js/i18n/keynav/de.js
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
tinymce.Resource.add('tinymce.html-i18n.help-keynav.de',
|
||||
'<h1>Grundlagen der Tastaturnavigation</h1>\n' +
|
||||
'\n' +
|
||||
'<dl>\n' +
|
||||
' <dt>Fokus auf Menüleiste</dt>\n' +
|
||||
' <dd>Windows oder Linux: ALT+F9</dd>\n' +
|
||||
' <dd>macOS: ⌥F9</dd>\n' +
|
||||
' <dt>Fokus auf Symbolleiste</dt>\n' +
|
||||
' <dd>Windows oder Linux: ALT+F10</dd>\n' +
|
||||
' <dd>macOS: ⌥F10</dd>\n' +
|
||||
' <dt>Fokus auf Fußzeile</dt>\n' +
|
||||
' <dd>Windows oder Linux: ALT+F11</dd>\n' +
|
||||
' <dd>macOS: ⌥F11</dd>\n' +
|
||||
' <dt>Fokus auf kontextbezogene Symbolleiste</dt>\n' +
|
||||
' <dd>Windows, Linux oder macOS: STRG+F9\n' +
|
||||
'</dl>\n' +
|
||||
'\n' +
|
||||
'<p>Die Navigation beginnt beim ersten Benutzeroberflächenelement, welches hervorgehoben ist. Falls sich das erste Element im Pfad der Fußzeile befindet,\n' +
|
||||
' ist es unterstrichen.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Zwischen Abschnitten der Benutzeroberfläche navigieren</h1>\n' +
|
||||
'\n' +
|
||||
'<p>Um von einem Abschnitt der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie <strong>TAB</strong>.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Um von einem Abschnitt der Benutzeroberfläche zum vorherigen zu wechseln, drücken Sie <strong>UMSCHALT+TAB</strong>.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Die Abschnitte der Benutzeroberfläche haben folgende <strong>TAB</strong>-Reihenfolge:</p>\n' +
|
||||
'\n' +
|
||||
'<ol>\n' +
|
||||
' <li>Menüleiste</li>\n' +
|
||||
' <li>Einzelne Gruppen der Symbolleiste</li>\n' +
|
||||
' <li>Randleiste</li>\n' +
|
||||
' <li>Elementpfad in der Fußzeile</li>\n' +
|
||||
' <li>Umschaltfläche „Wörter zählen“ in der Fußzeile</li>\n' +
|
||||
' <li>Branding-Link in der Fußzeile</li>\n' +
|
||||
' <li>Editor-Ziehpunkt zur Größenänderung in der Fußzeile</li>\n' +
|
||||
'</ol>\n' +
|
||||
'\n' +
|
||||
'<p>Falls ein Abschnitt der Benutzeroberflächen nicht vorhanden ist, wird er übersprungen.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Wenn in der Fußzeile die Tastaturnavigation fokussiert ist und keine Randleiste angezeigt wird, wechselt der Fokus durch Drücken von <strong>UMSCHALT+TAB</strong>\n' +
|
||||
' zur ersten Gruppe der Symbolleiste, nicht zur letzten.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Innerhalb von Abschnitten der Benutzeroberfläche navigieren</h1>\n' +
|
||||
'\n' +
|
||||
'<p>Um von einem Element der Benutzeroberfläche zum nächsten zu wechseln, drücken Sie die entsprechende <strong>Pfeiltaste</strong>.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Die Pfeiltasten <strong>Links</strong> und <strong>Rechts</strong></p>\n' +
|
||||
'\n' +
|
||||
'<ul>\n' +
|
||||
' <li>wechseln zwischen Menüs in der Menüleiste.</li>\n' +
|
||||
' <li>öffnen das Untermenü eines Menüs.</li>\n' +
|
||||
' <li>wechseln zwischen Schaltflächen in einer Gruppe der Symbolleiste.</li>\n' +
|
||||
' <li>wechseln zwischen Elementen im Elementpfad der Fußzeile.</li>\n' +
|
||||
'</ul>\n' +
|
||||
'\n' +
|
||||
'<p>Die Pfeiltasten <strong>Abwärts</strong> und <strong>Aufwärts</strong></p>\n' +
|
||||
'\n' +
|
||||
'<ul>\n' +
|
||||
' <li>wechseln zwischen Menüelementen in einem Menü.</li>\n' +
|
||||
' <li>wechseln zwischen Elementen in einem Popupmenü der Symbolleiste.</li>\n' +
|
||||
'</ul>\n' +
|
||||
'\n' +
|
||||
'<p>Die <strong>Pfeiltasten</strong> rotieren innerhalb des fokussierten Abschnitts der Benutzeroberfläche.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Um ein geöffnetes Menü, ein geöffnetes Untermenü oder ein geöffnetes Popupmenü zu schließen, drücken Sie die <strong>ESC</strong>-Taste.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Wenn sich der aktuelle Fokus ganz oben in einem bestimmten Abschnitt der Benutzeroberfläche befindet, wird durch Drücken der <strong>ESC</strong>-Taste auch\n' +
|
||||
' die Tastaturnavigation beendet.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Ein Menüelement oder eine Symbolleistenschaltfläche ausführen</h1>\n' +
|
||||
'\n' +
|
||||
'<p>Wenn das gewünschte Menüelement oder die gewünschte Symbolleistenschaltfläche hervorgehoben ist, drücken Sie <strong>Zurück</strong>, <strong>Eingabe</strong>\n' +
|
||||
' oder die <strong>Leertaste</strong>, um das Element auszuführen.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>In Dialogfeldern ohne Registerkarten navigieren</h1>\n' +
|
||||
'\n' +
|
||||
'<p>In Dialogfeldern ohne Registerkarten ist beim Öffnen eines Dialogfelds die erste interaktive Komponente fokussiert.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Navigieren Sie zwischen den interaktiven Komponenten eines Dialogfelds, indem Sie <strong>TAB</strong> oder <strong>UMSCHALT+TAB</strong> drücken.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>In Dialogfeldern mit Registerkarten navigieren</h1>\n' +
|
||||
'\n' +
|
||||
'<p>In Dialogfeldern mit Registerkarten ist beim Öffnen eines Dialogfelds die erste Schaltfläche eines Registerkartenmenüs fokussiert.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Navigieren Sie zwischen den interaktiven Komponenten auf dieser Registerkarte des Dialogfelds, indem Sie <strong>TAB</strong> oder\n' +
|
||||
' <strong>UMSCHALT+TAB</strong> drücken.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Wechseln Sie zu einer anderen Registerkarte des Dialogfelds, indem Sie den Fokus auf das Registerkartenmenü legen und dann die entsprechende <strong>Pfeiltaste</strong>\n' +
|
||||
' drücken, um durch die verfügbaren Registerkarten zu rotieren.</p>\n');
|
||||
90
assets/vendor/tinymce/plugins/help/js/i18n/keynav/en.js
vendored
Normal file
90
assets/vendor/tinymce/plugins/help/js/i18n/keynav/en.js
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
tinymce.Resource.add('tinymce.html-i18n.help-keynav.en',
|
||||
'<h1>Begin keyboard navigation</h1>\n' +
|
||||
'\n' +
|
||||
'<dl>\n' +
|
||||
' <dt>Focus the Menu bar</dt>\n' +
|
||||
' <dd>Windows or Linux: Alt+F9</dd>\n' +
|
||||
' <dd>macOS: ⌥F9</dd>\n' +
|
||||
' <dt>Focus the Toolbar</dt>\n' +
|
||||
' <dd>Windows or Linux: Alt+F10</dd>\n' +
|
||||
' <dd>macOS: ⌥F10</dd>\n' +
|
||||
' <dt>Focus the footer</dt>\n' +
|
||||
' <dd>Windows or Linux: Alt+F11</dd>\n' +
|
||||
' <dd>macOS: ⌥F11</dd>\n' +
|
||||
' <dt>Focus a contextual toolbar</dt>\n' +
|
||||
' <dd>Windows, Linux or macOS: Ctrl+F9\n' +
|
||||
'</dl>\n' +
|
||||
'\n' +
|
||||
'<p>Navigation will start at the first UI item, which will be highlighted, or underlined in the case of the first item in\n' +
|
||||
' the Footer element path.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Navigate between UI sections</h1>\n' +
|
||||
'\n' +
|
||||
'<p>To move from one UI section to the next, press <strong>Tab</strong>.</p>\n' +
|
||||
'\n' +
|
||||
'<p>To move from one UI section to the previous, press <strong>Shift+Tab</strong>.</p>\n' +
|
||||
'\n' +
|
||||
'<p>The <strong>Tab</strong> order of these UI sections is:</p>\n' +
|
||||
'\n' +
|
||||
'<ol>\n' +
|
||||
' <li>Menu bar</li>\n' +
|
||||
' <li>Each toolbar group</li>\n' +
|
||||
' <li>Sidebar</li>\n' +
|
||||
' <li>Element path in the footer</li>\n' +
|
||||
' <li>Word count toggle button in the footer</li>\n' +
|
||||
' <li>Branding link in the footer</li>\n' +
|
||||
' <li>Editor resize handle in the footer</li>\n' +
|
||||
'</ol>\n' +
|
||||
'\n' +
|
||||
'<p>If a UI section is not present, it is skipped.</p>\n' +
|
||||
'\n' +
|
||||
'<p>If the footer has keyboard navigation focus, and there is no visible sidebar, pressing <strong>Shift+Tab</strong>\n' +
|
||||
' moves focus to the first toolbar group, not the last.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Navigate within UI sections</h1>\n' +
|
||||
'\n' +
|
||||
'<p>To move from one UI element to the next, press the appropriate <strong>Arrow</strong> key.</p>\n' +
|
||||
'\n' +
|
||||
'<p>The <strong>Left</strong> and <strong>Right</strong> arrow keys</p>\n' +
|
||||
'\n' +
|
||||
'<ul>\n' +
|
||||
' <li>move between menus in the menu bar.</li>\n' +
|
||||
' <li>open a sub-menu in a menu.</li>\n' +
|
||||
' <li>move between buttons in a toolbar group.</li>\n' +
|
||||
' <li>move between items in the footer’s element path.</li>\n' +
|
||||
'</ul>\n' +
|
||||
'\n' +
|
||||
'<p>The <strong>Down</strong> and <strong>Up</strong> arrow keys</p>\n' +
|
||||
'\n' +
|
||||
'<ul>\n' +
|
||||
' <li>move between menu items in a menu.</li>\n' +
|
||||
' <li>move between items in a toolbar pop-up menu.</li>\n' +
|
||||
'</ul>\n' +
|
||||
'\n' +
|
||||
'<p><strong>Arrow</strong> keys cycle within the focused UI section.</p>\n' +
|
||||
'\n' +
|
||||
'<p>To close an open menu, an open sub-menu, or an open pop-up menu, press the <strong>Esc</strong> key.</p>\n' +
|
||||
'\n' +
|
||||
'<p>If the current focus is at the ‘top’ of a particular UI section, pressing the <strong>Esc</strong> key also exits\n' +
|
||||
' keyboard navigation entirely.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Execute a menu item or toolbar button</h1>\n' +
|
||||
'\n' +
|
||||
'<p>When the desired menu item or toolbar button is highlighted, press <strong>Return</strong>, <strong>Enter</strong>,\n' +
|
||||
' or the <strong>Space bar</strong> to execute the item.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Navigate non-tabbed dialogs</h1>\n' +
|
||||
'\n' +
|
||||
'<p>In non-tabbed dialogs, the first interactive component takes focus when the dialog opens.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Navigate between interactive dialog components by pressing <strong>Tab</strong> or <strong>Shift+Tab</strong>.</p>\n' +
|
||||
'\n' +
|
||||
'<h1>Navigate tabbed dialogs</h1>\n' +
|
||||
'\n' +
|
||||
'<p>In tabbed dialogs, the first button in the tab menu takes focus when the dialog opens.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Navigate between interactive components of this dialog tab by pressing <strong>Tab</strong> or\n' +
|
||||
' <strong>Shift+Tab</strong>.</p>\n' +
|
||||
'\n' +
|
||||
'<p>Switch to another dialog tab by giving the tab menu focus and then pressing the appropriate <strong>Arrow</strong>\n' +
|
||||
' key to cycle through the available tabs.</p>\n');
|
||||
4
assets/vendor/tinymce/plugins/help/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/help/plugin.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/plugins/link/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/link/plugin.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/plugins/lists/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/lists/plugin.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/plugins/searchreplace/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/searchreplace/plugin.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/plugins/table/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/table/plugin.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/plugins/visualblocks/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/visualblocks/plugin.min.js
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* TinyMCE version 6.8.3 (2024-02-08)
|
||||
*/
|
||||
!function(){"use strict";var t=tinymce.util.Tools.resolve("tinymce.PluginManager");const s=(t,s,o)=>{t.dom.toggleClass(t.getBody(),"mce-visualblocks"),o.set(!o.get()),((t,s)=>{t.dispatch("VisualBlocks",{state:s})})(t,o.get())},o=("visualblocks_default_state",t=>t.options.get("visualblocks_default_state"));const e=(t,s)=>o=>{o.setActive(s.get());const e=t=>o.setActive(t.state);return t.on("VisualBlocks",e),()=>t.off("VisualBlocks",e)};t.add("visualblocks",((t,l)=>{(t=>{(0,t.options.register)("visualblocks_default_state",{processor:"boolean",default:!1})})(t);const a=(t=>{let s=!1;return{get:()=>s,set:t=>{s=t}}})();((t,o,e)=>{t.addCommand("mceVisualBlocks",(()=>{s(t,0,e)}))})(t,0,a),((t,s)=>{const o=()=>t.execCommand("mceVisualBlocks");t.ui.registry.addToggleButton("visualblocks",{icon:"visualblocks",tooltip:"Show blocks",onAction:o,onSetup:e(t,s)}),t.ui.registry.addToggleMenuItem("visualblocks",{text:"Show blocks",icon:"visualblocks",onAction:o,onSetup:e(t,s)})})(t,a),((t,e,l)=>{t.on("PreviewFormats AfterPreviewFormats",(s=>{l.get()&&t.dom.toggleClass(t.getBody(),"mce-visualblocks","afterpreviewformats"===s.type)})),t.on("init",(()=>{o(t)&&s(t,0,l)}))})(t,0,a)}))}();
|
||||
4
assets/vendor/tinymce/plugins/wordcount/plugin.min.js
vendored
Normal file
4
assets/vendor/tinymce/plugins/wordcount/plugin.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/vendor/tinymce/skins/content/dark/content.min.css
vendored
Normal file
1
assets/vendor/tinymce/skins/content/dark/content.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
body{background-color:#222f3e;color:#fff;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}a{color:#4099ff}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#6d737b}figure{display:table;margin:1rem auto}figure figcaption{color:#8a8f97;display:block;margin-top:.25rem;text-align:center}hr{border-color:#6d737b;border-style:solid;border-width:1px 0 0 0}code{background-color:#6d737b;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #6d737b;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #6d737b;margin-right:1.5rem;padding-right:1rem}
|
||||
1
assets/vendor/tinymce/skins/content/default/content.min.css
vendored
Normal file
1
assets/vendor/tinymce/skins/content/default/content.min.css
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Oxygen,Ubuntu,Cantarell,'Open Sans','Helvetica Neue',sans-serif;line-height:1.4;margin:1rem}table{border-collapse:collapse}table:not([cellpadding]) td,table:not([cellpadding]) th{padding:.4rem}table[border]:not([border="0"]):not([style*=border-width]) td,table[border]:not([border="0"]):not([style*=border-width]) th{border-width:1px}table[border]:not([border="0"]):not([style*=border-style]) td,table[border]:not([border="0"]):not([style*=border-style]) th{border-style:solid}table[border]:not([border="0"]):not([style*=border-color]) td,table[border]:not([border="0"]):not([style*=border-color]) th{border-color:#ccc}figure{display:table;margin:1rem auto}figure figcaption{color:#999;display:block;margin-top:.25rem;text-align:center}hr{border-color:#ccc;border-style:solid;border-width:1px 0 0 0}code{background-color:#e8e8e8;border-radius:3px;padding:.1rem .2rem}.mce-content-body:not([dir=rtl]) blockquote{border-left:2px solid #ccc;margin-left:1.5rem;padding-left:1rem}.mce-content-body[dir=rtl] blockquote{border-right:2px solid #ccc;margin-right:1.5rem;padding-right:1rem}
|
||||
1
assets/vendor/tinymce/skins/ui/oxide-dark/content.min.css
vendored
Normal file
1
assets/vendor/tinymce/skins/ui/oxide-dark/content.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/vendor/tinymce/skins/ui/oxide-dark/skin.min.css
vendored
Normal file
1
assets/vendor/tinymce/skins/ui/oxide-dark/skin.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/vendor/tinymce/skins/ui/oxide/content.min.css
vendored
Normal file
1
assets/vendor/tinymce/skins/ui/oxide/content.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/vendor/tinymce/skins/ui/oxide/skin.min.css
vendored
Normal file
1
assets/vendor/tinymce/skins/ui/oxide/skin.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/themes/silver/theme.min.js
vendored
Normal file
4
assets/vendor/tinymce/themes/silver/theme.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
assets/vendor/tinymce/tinymce.min.js
vendored
Normal file
4
assets/vendor/tinymce/tinymce.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -7,6 +7,7 @@ require_once __DIR__ . '/config.php';
|
|||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/Mailer.php';
|
||||
require_once __DIR__ . '/includes/Ui.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
|
|
@ -83,12 +84,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('reset_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<?= Ui::head($db) ?>
|
||||
</head>
|
||||
<body class="app-body focus-page">
|
||||
<div class="focus-card card">
|
||||
|
|
|
|||
145
includes/Ui.php
Normal file
145
includes/Ui.php
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<?php
|
||||
/**
|
||||
* Gemeinsame Bausteine fuer den Seitenkopf.
|
||||
*
|
||||
* - liefert versionierte Asset-URLs (Cache-Busting nach Updates)
|
||||
* - bindet die lokal ausgelieferten Assets ein (keine externen CDNs)
|
||||
* - erzeugt die Branding-Overrides aus den Einstellungen
|
||||
*
|
||||
* Alle Methoden funktionieren auch ohne Datenbank ($db = null), damit der
|
||||
* Installer dieselbe Optik nutzen kann.
|
||||
*/
|
||||
class Ui
|
||||
{
|
||||
/** Standardwerte des Design-Systems (siehe assets/global.css). */
|
||||
public const DEFAULT_ACCENT = '#5b5bd6';
|
||||
public const DEFAULT_ACCENT_DARK = '#8b8bf5';
|
||||
public const DEFAULT_GRADIENT_FROM = '#5b5bd6';
|
||||
public const DEFAULT_GRADIENT_TO = '#8b5cf6';
|
||||
public const DEFAULT_RADIUS = 14;
|
||||
|
||||
/** Projektwurzel im Dateisystem. */
|
||||
private static function root(): string
|
||||
{
|
||||
return dirname(__DIR__);
|
||||
}
|
||||
|
||||
/**
|
||||
* URL eines Projekt-Assets inkl. Versionsstempel.
|
||||
* $base ist der Pfad zur Projektwurzel ('' im Root, '../' in /admin).
|
||||
*/
|
||||
public static function asset(string $path, string $base = ''): string
|
||||
{
|
||||
$file = self::root() . '/' . ltrim($path, '/');
|
||||
$version = is_file($file) ? (string)filemtime($file) : '0';
|
||||
|
||||
return $base . $path . '?v=' . $version;
|
||||
}
|
||||
|
||||
/** <script src> mit Versionsstempel. */
|
||||
public static function script(string $path, string $base = '', bool $defer = false): string
|
||||
{
|
||||
return '<script src="' . htmlspecialchars(self::asset($path, $base)) . '"'
|
||||
. ($defer ? ' defer' : '') . '></script>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme-Bootstrap: gespeicherte Auswahl, sonst Systemeinstellung.
|
||||
* Muss im <head> stehen, damit nichts hell aufblitzt.
|
||||
*/
|
||||
public static function themeScript(): string
|
||||
{
|
||||
return '<script>(function(){'
|
||||
. 'var s=localStorage.getItem("theme");'
|
||||
. 'var t=s||(window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light");'
|
||||
. 'document.documentElement.setAttribute("data-theme",t);'
|
||||
. '})();</script>';
|
||||
}
|
||||
|
||||
/** Gueltige Hex-Farbe oder Fallback. */
|
||||
private static function color(?string $value, string $fallback): string
|
||||
{
|
||||
$value = trim((string)$value);
|
||||
|
||||
return preg_match('/^#[0-9a-fA-F]{6}$/', $value) ? strtolower($value) : $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS-Overrides fuer die Markenfarben. Gibt einen leeren String zurueck,
|
||||
* wenn nichts vom Standard abweicht.
|
||||
*/
|
||||
public static function brandingStyle($db = null): string
|
||||
{
|
||||
if (!$db) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$accent = self::color($db->getSetting('brand_accent', ''), self::DEFAULT_ACCENT);
|
||||
$accentDark = self::color($db->getSetting('brand_accent_dark', ''), self::DEFAULT_ACCENT_DARK);
|
||||
$from = self::color($db->getSetting('brand_gradient_from', ''), self::DEFAULT_GRADIENT_FROM);
|
||||
$to = self::color($db->getSetting('brand_gradient_to', ''), self::DEFAULT_GRADIENT_TO);
|
||||
$radius = (int)$db->getSetting('brand_radius', (string)self::DEFAULT_RADIUS);
|
||||
$radius = max(0, min(28, $radius));
|
||||
|
||||
$isDefault = $accent === self::DEFAULT_ACCENT
|
||||
&& $accentDark === self::DEFAULT_ACCENT_DARK
|
||||
&& $from === self::DEFAULT_GRADIENT_FROM
|
||||
&& $to === self::DEFAULT_GRADIENT_TO
|
||||
&& $radius === self::DEFAULT_RADIUS;
|
||||
|
||||
if ($isDefault) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Abgeleitete Töne über color-mix – so genügt eine einzige Grundfarbe.
|
||||
return '<style>'
|
||||
. ':root{'
|
||||
. "--accent:{$accent};"
|
||||
. "--accent-hover:color-mix(in srgb, {$accent} 84%, #000);"
|
||||
. "--accent-soft:color-mix(in srgb, {$accent} 12%, #fff);"
|
||||
. "--accent-border:color-mix(in srgb, {$accent} 32%, #fff);"
|
||||
. "--accent-2:{$to};"
|
||||
. "--input-focus:{$accent};"
|
||||
. "--ring:0 0 0 4px color-mix(in srgb, {$accent} 22%, transparent);"
|
||||
. "--brand-gradient:linear-gradient(135deg, {$from} 0%, {$to} 100%);"
|
||||
. "--r-lg:{$radius}px;"
|
||||
. "--r-xl:" . ($radius + 6) . 'px;'
|
||||
. '}'
|
||||
. '[data-theme="dark"]{'
|
||||
. "--accent:{$accentDark};"
|
||||
. "--accent-hover:color-mix(in srgb, {$accentDark} 80%, #fff);"
|
||||
. "--accent-soft:color-mix(in srgb, {$accentDark} 20%, #0a0c11);"
|
||||
. "--accent-border:color-mix(in srgb, {$accentDark} 42%, #0a0c11);"
|
||||
. "--input-focus:{$accentDark};"
|
||||
. "--ring:0 0 0 4px color-mix(in srgb, {$accentDark} 26%, transparent);"
|
||||
. '}'
|
||||
. '</style>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Kompletter Standard-Kopf: Favicon, Schrift, Icons, Design-System,
|
||||
* Theme-Bootstrap und Branding.
|
||||
*/
|
||||
public static function head($db = null, string $base = ''): string
|
||||
{
|
||||
$out = [];
|
||||
|
||||
$favicon = $db ? trim((string)$db->getSetting('favicon_url', '')) : '';
|
||||
if ($favicon !== '') {
|
||||
$out[] = '<link rel="icon" href="' . htmlspecialchars($favicon) . '">';
|
||||
}
|
||||
|
||||
$out[] = '<meta name="color-scheme" content="light dark">';
|
||||
$out[] = '<link rel="stylesheet" href="' . htmlspecialchars(self::asset('assets/vendor/inter/inter.css', $base)) . '">';
|
||||
$out[] = '<link rel="stylesheet" href="' . htmlspecialchars(self::asset('assets/vendor/fontawesome/fontawesome.css', $base)) . '">';
|
||||
$out[] = '<link rel="stylesheet" href="' . htmlspecialchars(self::asset('assets/global.css', $base)) . '">';
|
||||
$out[] = self::themeScript();
|
||||
|
||||
$branding = self::brandingStyle($db);
|
||||
if ($branding !== '') {
|
||||
$out[] = $branding;
|
||||
}
|
||||
|
||||
return implode("\n ", $out);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@
|
|||
* Expects $currentPage (string), $appTitle (string), $auth, $db to be set before include.
|
||||
* Expects I18n to be initialized.
|
||||
*/
|
||||
require_once __DIR__ . '/Ui.php';
|
||||
|
||||
$currentPage = $currentPage ?? '';
|
||||
$faviconUrl = isset($db) ? $db->getSetting('favicon_url', '') : '';
|
||||
$currentUser = isset($auth) ? $auth->getCurrentUser() : null;
|
||||
$lang = I18n::getLanguage();
|
||||
$base = $adminBase ?? ''; // Prefix bis zum admin/-Ordner
|
||||
|
|
@ -43,20 +44,7 @@ foreach ($navGroups as $items) {
|
|||
}
|
||||
}
|
||||
?>
|
||||
<?php if ($faviconUrl): ?>
|
||||
<link rel="icon" type="image/x-icon" href="<?= htmlspecialchars($faviconUrl) ?>">
|
||||
<?php endif; ?>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="<?= $rootBase ?>assets/global.css">
|
||||
<script>
|
||||
(function(){
|
||||
const t = localStorage.getItem('theme') || 'light';
|
||||
document.documentElement.setAttribute('data-theme', t);
|
||||
})();
|
||||
</script>
|
||||
<?= Ui::head($db ?? null, $rootBase) ?>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
|||
10
index.php
10
index.php
|
|
@ -19,6 +19,7 @@ require_once __DIR__ . '/includes/Mailer.php';
|
|||
require_once __DIR__ . '/includes/Notifier.php';
|
||||
require_once __DIR__ . '/includes/Captcha.php';
|
||||
require_once __DIR__ . '/includes/Sms.php';
|
||||
require_once __DIR__ . '/includes/Ui.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
|
|
@ -284,17 +285,12 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<?= Ui::head($db) ?>
|
||||
<?php if ($captchaMode === 'hcaptcha' && $hcaptchaSiteKey !== ''): ?>
|
||||
<script src="https://js.hcaptcha.com/1/api.js" async defer></script>
|
||||
<?php endif; ?>
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<?php if ($voucherCreated): ?>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js') ?>
|
||||
<?php endif; ?>
|
||||
<style>
|
||||
/* Seitenspezifisch: Druckansicht der Voucher-Karten.
|
||||
|
|
|
|||
|
|
@ -185,11 +185,8 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UniFi Voucher System - Installation</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<?php require_once __DIR__ . '/includes/Ui.php'; ?>
|
||||
<?= Ui::head(null) ?>
|
||||
<style>
|
||||
/* Installer-spezifisch: Fortschrittsanzeige und Abschnitte */
|
||||
.install-head { display: flex; align-items: center; gap: 14px; margin-bottom: 4px; }
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ ini_set('log_errors', 1);
|
|||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/Ui.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
|
||||
try {
|
||||
|
|
@ -158,12 +159,7 @@ try {
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('login_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<?= Ui::head($db) ?>
|
||||
</head>
|
||||
<body class="auth-body<?= $showPanel ? '' : ' auth-body-single' ?>">
|
||||
|
||||
|
|
|
|||
373
login_simple.php
373
login_simple.php
|
|
@ -1,189 +1,186 @@
|
|||
<?php
|
||||
// Umfassendes Error Reporting
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
// Versuche Dateien zu laden
|
||||
$loadErrors = [];
|
||||
|
||||
try {
|
||||
if (!file_exists(__DIR__ . '/config.php')) {
|
||||
throw new Exception('config.php nicht gefunden');
|
||||
}
|
||||
require_once __DIR__ . '/config.php';
|
||||
} catch (Exception $e) {
|
||||
$loadErrors[] = "Config: " . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!file_exists(__DIR__ . '/includes/Database.php')) {
|
||||
throw new Exception('includes/Database.php nicht gefunden');
|
||||
}
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
} catch (Exception $e) {
|
||||
$loadErrors[] = "Database: " . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!file_exists(__DIR__ . '/includes/Auth.php')) {
|
||||
throw new Exception('includes/Auth.php nicht gefunden');
|
||||
}
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
} catch (Exception $e) {
|
||||
$loadErrors[] = "Auth: " . $e->getMessage();
|
||||
}
|
||||
|
||||
// Wenn Ladefehler aufgetreten sind, zeige sie an
|
||||
if (!empty($loadErrors)) {
|
||||
die('<h1>Fehler beim Laden der Dateien</h1><ul><li>' . implode('</li><li>', $loadErrors) . '</li></ul>');
|
||||
}
|
||||
|
||||
// Ab hier normal weiter
|
||||
try {
|
||||
$auth = new Auth();
|
||||
} catch (Exception $e) {
|
||||
die('<h1>Fehler bei Auth-Initialisierung</h1><p>' . $e->getMessage() . '</p>');
|
||||
}
|
||||
|
||||
// Wenn bereits eingeloggt, weiterleiten
|
||||
if ($auth->isLoggedIn()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Login-Verarbeitung
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$email = $_POST['email'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($email) || empty($password)) {
|
||||
$error = 'Bitte E-Mail und Passwort eingeben';
|
||||
} elseif ($auth->login($email, $password)) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Ungültige E-Mail oder Passwort';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = 'Login-Fehler: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
$m365Enabled = !empty($db->getSetting('m365_client_id')) &&
|
||||
!empty($db->getSetting('m365_client_secret')) &&
|
||||
!empty($db->getSetting('m365_tenant_id'));
|
||||
$publicAccess = $db->getSetting('public_access', 0);
|
||||
|
||||
// M365 OAuth URL generieren falls aktiviert
|
||||
$m365LoginUrl = '';
|
||||
if ($m365Enabled) {
|
||||
$clientId = $db->getSetting('m365_client_id');
|
||||
$tenantId = $db->getSetting('m365_tenant_id');
|
||||
|
||||
// Dynamische Redirect URI basierend auf aktuellem Pfad
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
|
||||
$redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
|
||||
|
||||
$params = [
|
||||
'client_id' => $clientId,
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $redirectUri,
|
||||
'response_mode' => 'query',
|
||||
'scope' => 'openid profile email User.Read',
|
||||
'state' => bin2hex(random_bytes(16))
|
||||
];
|
||||
|
||||
$_SESSION['m365_state'] = $params['state'];
|
||||
|
||||
$m365LoginUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?" . http_build_query($params);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
die('<h1>Datenbankfehler</h1><p>' . $e->getMessage() . '</p>');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<style>
|
||||
/* Diagnose-Ausgabe am Seitenende */
|
||||
.debug-info {
|
||||
margin-top: 22px; padding: 14px;
|
||||
background: var(--bg-subtle); border: 1px solid var(--border-color); border-radius: var(--r-md);
|
||||
font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app-body focus-page">
|
||||
<div class="focus-card card login-container">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
|
||||
<?php else: ?>
|
||||
<h1><?= htmlspecialchars($appTitle) ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="subtitle">Melden Sie sich an, um fortzufahren</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="">
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" name="email" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<?php if ($m365Enabled): ?>
|
||||
<div class="divider"><span>oder</span></div>
|
||||
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn btn-microsoft">
|
||||
<i class="fab fa-microsoft"></i> Mit Microsoft 365 anmelden
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($publicAccess): ?>
|
||||
<div class="auth-links"><a href="index.php" class="back-link"><i class="fas fa-arrow-left"></i> Zurück zur Code-Erstellung</a></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Debug Info (kann nach erfolgreicher Einrichtung entfernt werden) -->
|
||||
<div class="debug-info">
|
||||
<strong>System-Status:</strong><br>
|
||||
PHP Version: <?= phpversion() ?><br>
|
||||
Session Status: <?= session_status() === PHP_SESSION_ACTIVE ? 'Aktiv' : 'Inaktiv' ?><br>
|
||||
Eingeloggt: <?= $auth->isLoggedIn() ? 'Ja' : 'Nein' ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<?php
|
||||
// Umfassendes Error Reporting
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
// Versuche Dateien zu laden
|
||||
$loadErrors = [];
|
||||
|
||||
try {
|
||||
if (!file_exists(__DIR__ . '/config.php')) {
|
||||
throw new Exception('config.php nicht gefunden');
|
||||
}
|
||||
require_once __DIR__ . '/config.php';
|
||||
} catch (Exception $e) {
|
||||
$loadErrors[] = "Config: " . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!file_exists(__DIR__ . '/includes/Database.php')) {
|
||||
throw new Exception('includes/Database.php nicht gefunden');
|
||||
}
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
} catch (Exception $e) {
|
||||
$loadErrors[] = "Database: " . $e->getMessage();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!file_exists(__DIR__ . '/includes/Auth.php')) {
|
||||
throw new Exception('includes/Auth.php nicht gefunden');
|
||||
}
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/Ui.php';
|
||||
} catch (Exception $e) {
|
||||
$loadErrors[] = "Auth: " . $e->getMessage();
|
||||
}
|
||||
|
||||
// Wenn Ladefehler aufgetreten sind, zeige sie an
|
||||
if (!empty($loadErrors)) {
|
||||
die('<h1>Fehler beim Laden der Dateien</h1><ul><li>' . implode('</li><li>', $loadErrors) . '</li></ul>');
|
||||
}
|
||||
|
||||
// Ab hier normal weiter
|
||||
try {
|
||||
$auth = new Auth();
|
||||
} catch (Exception $e) {
|
||||
die('<h1>Fehler bei Auth-Initialisierung</h1><p>' . $e->getMessage() . '</p>');
|
||||
}
|
||||
|
||||
// Wenn bereits eingeloggt, weiterleiten
|
||||
if ($auth->isLoggedIn()) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
$error = '';
|
||||
$success = '';
|
||||
|
||||
// Login-Verarbeitung
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$email = $_POST['email'] ?? '';
|
||||
$password = $_POST['password'] ?? '';
|
||||
|
||||
if (empty($email) || empty($password)) {
|
||||
$error = 'Bitte E-Mail und Passwort eingeben';
|
||||
} elseif ($auth->login($email, $password)) {
|
||||
header('Location: index.php');
|
||||
exit;
|
||||
} else {
|
||||
$error = 'Ungültige E-Mail oder Passwort';
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$error = 'Login-Fehler: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$db = Database::getInstance();
|
||||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
$m365Enabled = !empty($db->getSetting('m365_client_id')) &&
|
||||
!empty($db->getSetting('m365_client_secret')) &&
|
||||
!empty($db->getSetting('m365_tenant_id'));
|
||||
$publicAccess = $db->getSetting('public_access', 0);
|
||||
|
||||
// M365 OAuth URL generieren falls aktiviert
|
||||
$m365LoginUrl = '';
|
||||
if ($m365Enabled) {
|
||||
$clientId = $db->getSetting('m365_client_id');
|
||||
$tenantId = $db->getSetting('m365_tenant_id');
|
||||
|
||||
// Dynamische Redirect URI basierend auf aktuellem Pfad
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
$scriptPath = dirname($_SERVER['SCRIPT_NAME']);
|
||||
$scriptPath = $scriptPath === '/' ? '' : $scriptPath;
|
||||
$redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
|
||||
|
||||
$params = [
|
||||
'client_id' => $clientId,
|
||||
'response_type' => 'code',
|
||||
'redirect_uri' => $redirectUri,
|
||||
'response_mode' => 'query',
|
||||
'scope' => 'openid profile email User.Read',
|
||||
'state' => bin2hex(random_bytes(16))
|
||||
];
|
||||
|
||||
$_SESSION['m365_state'] = $params['state'];
|
||||
|
||||
$m365LoginUrl = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/authorize?" . http_build_query($params);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
die('<h1>Datenbankfehler</h1><p>' . $e->getMessage() . '</p>');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - <?= htmlspecialchars($appTitle) ?></title>
|
||||
<?= Ui::head($db) ?>
|
||||
<style>
|
||||
/* Diagnose-Ausgabe am Seitenende */
|
||||
.debug-info {
|
||||
margin-top: 22px; padding: 14px;
|
||||
background: var(--bg-subtle); border: 1px solid var(--border-color); border-radius: var(--r-md);
|
||||
font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); text-align: left;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="app-body focus-page">
|
||||
<div class="focus-card card login-container">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
|
||||
<?php else: ?>
|
||||
<h1><?= htmlspecialchars($appTitle) ?></h1>
|
||||
<?php endif; ?>
|
||||
|
||||
<p class="subtitle">Melden Sie sich an, um fortzufahren</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post" action="">
|
||||
<div class="form-group">
|
||||
<label for="email">E-Mail</label>
|
||||
<input type="email" id="email" name="email" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Passwort</label>
|
||||
<input type="password" id="password" name="password" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Anmelden</button>
|
||||
</form>
|
||||
|
||||
<?php if ($m365Enabled): ?>
|
||||
<div class="divider"><span>oder</span></div>
|
||||
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn btn-microsoft">
|
||||
<i class="fab fa-microsoft"></i> Mit Microsoft 365 anmelden
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($publicAccess): ?>
|
||||
<div class="auth-links"><a href="index.php" class="back-link"><i class="fas fa-arrow-left"></i> Zurück zur Code-Erstellung</a></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Debug Info (kann nach erfolgreicher Einrichtung entfernt werden) -->
|
||||
<div class="debug-info">
|
||||
<strong>System-Status:</strong><br>
|
||||
PHP Version: <?= phpversion() ?><br>
|
||||
Session Status: <?= session_status() === PHP_SESSION_ACTIVE ? 'Aktiv' : 'Inaktiv' ?><br>
|
||||
Eingeloggt: <?= $auth->isLoggedIn() ? 'Ja' : 'Nein' ?>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -32,8 +32,8 @@ $redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>M365 Debug</title>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<?php require_once __DIR__ . '/includes/Ui.php'; ?>
|
||||
<?= Ui::head($db ?? null) ?>
|
||||
<style>
|
||||
body { padding: 32px 20px; }
|
||||
.section { max-width: 820px; margin: 0 auto 18px; padding: 20px; background: var(--bg-card);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ ini_set('log_errors', 1);
|
|||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
require_once __DIR__ . '/includes/Ui.php';
|
||||
require_once __DIR__ . '/includes/I18n.php';
|
||||
|
||||
$auth = new Auth();
|
||||
|
|
@ -69,12 +70,7 @@ if ($valid && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?= __('reset_new_pw') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<?= Ui::head($db) ?>
|
||||
<style>
|
||||
.pw-strength { height: 4px; border-radius: var(--r-pill); margin-top: 8px; background: var(--border-color); transition: width .3s, background-color .3s; }
|
||||
.pw-strength.weak { background: var(--danger); width: 30%; }
|
||||
|
|
|
|||
5
test.php
5
test.php
|
|
@ -4,6 +4,8 @@ ini_set('display_errors', 0);
|
|||
ini_set('log_errors', 1);
|
||||
|
||||
// Diagnose-Seite nur fuer angemeldete Admins zugaenglich (verhindert Info-Leak)
|
||||
require_once __DIR__ . '/includes/Ui.php';
|
||||
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
|
|
@ -16,8 +18,7 @@ if (file_exists(__DIR__ . '/config.php')) {
|
|||
echo '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">'
|
||||
. '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
|
||||
. '<title>System-Test</title>'
|
||||
. '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">'
|
||||
. '<link rel="stylesheet" href="assets/global.css">'
|
||||
. Ui::head(isset($db) ? $db : null)
|
||||
. '<style>.focus-card h2{font-size:14px;margin:22px 0 8px;} .focus-card h2:first-of-type{margin-top:0;}'
|
||||
. 'pre{white-space:pre-wrap;}</style>'
|
||||
. '</head><body class="app-body focus-page">'
|
||||
|
|
|
|||
108
tools/demo/build.py
Executable file
108
tools/demo/build.py
Executable file
|
|
@ -0,0 +1,108 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Baut eine Demo-Instanz des Voucher-Tools ohne Datenbank.
|
||||
|
||||
Die Demo dient zwei Zwecken:
|
||||
* Screenshots fuer die Dokumentation reproduzierbar erzeugen
|
||||
* Smoke-Test: rendert jede Seite einmal ohne echte Datenbank
|
||||
|
||||
Ablauf: Projekt in ein Zielverzeichnis kopieren, das Overlay darueberlegen
|
||||
(Stubs fuer Database/Auth) und ein paar klar benannte Demo-Zustaende in die
|
||||
Kopie patchen. Das Original bleibt unveraendert.
|
||||
|
||||
python3 tools/demo/build.py /tmp/uvt-demo
|
||||
php -S 127.0.0.1:8123 -t /tmp/uvt-demo
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OVERLAY = os.path.join(ROOT, 'tools', 'demo', 'overlay')
|
||||
# Nur auf oberster Ebene ueberspringen – assets/vendor gehoert dazu!
|
||||
SKIP_TOP = {'.git', '.github', 'tools', 'docs', 'tests', 'vendor', 'node_modules'}
|
||||
|
||||
|
||||
def copy_project(target: str) -> None:
|
||||
if os.path.exists(target):
|
||||
shutil.rmtree(target)
|
||||
|
||||
def ignore(directory, names):
|
||||
if os.path.abspath(directory) == ROOT:
|
||||
return [n for n in names if n in SKIP_TOP]
|
||||
return []
|
||||
|
||||
shutil.copytree(ROOT, target, ignore=ignore)
|
||||
for name in os.listdir(OVERLAY):
|
||||
src = os.path.join(OVERLAY, name)
|
||||
dst = os.path.join(target, name)
|
||||
if os.path.isdir(src):
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
else:
|
||||
shutil.copy2(src, dst)
|
||||
|
||||
|
||||
def patch(path: str, anchor: str, replacement: str) -> None:
|
||||
"""Ersetzt einen Anker in der Demo-Kopie. Fehlt der Anker, bricht der
|
||||
Build ab – so faellt auf, wenn sich die Vorlage geaendert hat."""
|
||||
with open(path, encoding='utf-8') as fh:
|
||||
content = fh.read()
|
||||
if anchor not in content:
|
||||
raise SystemExit('Anker nicht gefunden in %s:\n%s' % (path, anchor[:80]))
|
||||
with open(path, 'w', encoding='utf-8') as fh:
|
||||
fh.write(content.replace(anchor, replacement, 1))
|
||||
|
||||
|
||||
def apply_demo_states(target: str) -> None:
|
||||
# Voucher-Ergebnis und Bulk-Ansicht ohne Controller-Zugriff zeigen
|
||||
anchor = "$currentUser = $auth->isLoggedIn() ? $auth->getCurrentUser() : null;"
|
||||
patch(os.path.join(target, 'index.php'), anchor, anchor + """
|
||||
|
||||
// --- Demo-Instanz: Zustaende ohne UniFi-Controller darstellen ---
|
||||
if (($_GET['demo'] ?? '') === 'result') {
|
||||
$voucherCreated = true;
|
||||
$voucherCode = '4829-17364';
|
||||
$voucherData = ['code' => '4829-17364', 'site_name' => 'Hauptstandort Nord', 'max_uses' => 2,
|
||||
'expire_min' => 480, 'expiry_date' => '23.09.2026', 'expiry_time' => '08:00'];
|
||||
}
|
||||
if (($_GET['demo'] ?? '') === 'bulk') {
|
||||
$bulkCreated = true;
|
||||
$bulkVouchers = [];
|
||||
foreach (['4829-17364', '5517-90422', '7731-64508', '2094-38177', '6640-52913'] as $code) {
|
||||
$bulkVouchers[] = ['code' => $code, 'site_name' => 'Hauptstandort Nord', 'max_uses' => 2,
|
||||
'expire_min' => 480, 'expiry_date' => '23.09.2026', 'expiry_time' => '08:00'];
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
# Live-Voucher-Liste automatisch laden (sonst wartet sie auf Site-Auswahl)
|
||||
patch(os.path.join(target, 'admin', 'vouchers.php'), "</script>\n</body>", """</script>
|
||||
<script>
|
||||
// Demo-Instanz: Site vorauswaehlen und Liste laden
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const sel = document.getElementById('siteSelect');
|
||||
if (sel) { sel.value = '1'; loadVouchers(false); }
|
||||
});
|
||||
</script>
|
||||
</body>""")
|
||||
|
||||
# Frisch erzeugten API-Schluessel zeigen
|
||||
patch(os.path.join(target, 'admin', 'api_keys.php'), '$keys = $db->fetchAll(',
|
||||
"if (($_GET['demo'] ?? '') === 'new') { $newKey = 'uvt_3f9a2c7d41e8b60592af18cc4d7e0b3a95f2617c'; }\n$keys = $db->fetchAll(")
|
||||
|
||||
# Theme per Query-Parameter erzwingen (fuer Dark-Mode-Screenshots)
|
||||
ui = os.path.join(target, 'includes', 'Ui.php')
|
||||
patch(ui, 'var s=localStorage.getItem("theme");',
|
||||
'var s=new URLSearchParams(location.search).get("theme")||localStorage.getItem("theme");')
|
||||
|
||||
|
||||
def main() -> int:
|
||||
target = sys.argv[1] if len(sys.argv) > 1 else '/tmp/uvt-demo'
|
||||
copy_project(target)
|
||||
apply_demo_states(target)
|
||||
print('Demo-Instanz gebaut: %s' % target)
|
||||
print('Start: php -S 127.0.0.1:8123 -t %s' % target)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
22
tools/demo/overlay/demo-assets/background.svg
Normal file
22
tools/demo/overlay/demo-assets/background.svg
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1400" viewBox="0 0 1200 1400">
|
||||
<defs>
|
||||
<linearGradient id="sky" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#7dd3fc"/><stop offset="55%" stop-color="#38bdf8"/><stop offset="100%" stop-color="#0369a1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="water" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#0c4a6e"/><stop offset="100%" stop-color="#082f49"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1200" height="1400" fill="url(#sky)"/>
|
||||
<circle cx="900" cy="260" r="90" fill="#fef3c7" opacity=".85"/>
|
||||
<path d="M0 780 L260 560 L430 780 Z" fill="#134e4a" opacity=".85"/>
|
||||
<path d="M300 800 L560 520 L830 800 Z" fill="#115e59" opacity=".9"/>
|
||||
<path d="M700 800 L980 600 L1200 800 Z" fill="#0f766e" opacity=".85"/>
|
||||
<rect y="800" width="1200" height="600" fill="url(#water)"/>
|
||||
<g fill="#ffffff" opacity=".16">
|
||||
<rect x="120" y="880" width="360" height="6" rx="3"/>
|
||||
<rect x="240" y="960" width="520" height="6" rx="3"/>
|
||||
<rect x="60" y="1060" width="420" height="6" rx="3"/>
|
||||
<rect x="520" y="1140" width="600" height="6" rx="3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
9
tools/demo/overlay/demo-assets/logo.svg
Normal file
9
tools/demo/overlay/demo-assets/logo.svg
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="220" height="42" viewBox="0 0 220 42">
|
||||
<g fill="none" stroke="#ffffff" stroke-width="2.4" stroke-linecap="round">
|
||||
<path d="M8 30c4-10 10-15 13-15s9 5 13 15"/>
|
||||
<path d="M14 30c2.5-6 5.5-9 7-9s4.5 3 7 9"/>
|
||||
</g>
|
||||
<circle cx="21" cy="30" r="2.6" fill="#ffffff"/>
|
||||
<text x="46" y="21" font-family="Inter, sans-serif" font-size="16" font-weight="700" fill="#ffffff">HOTEL SEEBLICK</text>
|
||||
<text x="46" y="34" font-family="Inter, sans-serif" font-size="10.5" letter-spacing="2" fill="rgba(255,255,255,.75)">GÄSTE-WLAN</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 588 B |
31
tools/demo/overlay/includes/Auth.php
Normal file
31
tools/demo/overlay/includes/Auth.php
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
/** Demo-Stub: angemeldeter Admin ohne echte Sitzungspruefung. */
|
||||
require_once __DIR__ . '/Database.php';
|
||||
require_once __DIR__ . '/Totp.php';
|
||||
class Auth {
|
||||
private $user = ['id'=>1,'name'=>'Marie Sander','email'=>'marie.sander@example.com','is_admin'=>1,
|
||||
'role'=>'admin','is_active'=>1,'password_hash'=>'x','totp_enabled'=>0,'auth_source'=>'local'];
|
||||
public function __construct() { if (session_status() === PHP_SESSION_NONE) @session_start(); $_SESSION['user_id'] = 1; }
|
||||
public function clientIp() { return '192.168.10.42'; }
|
||||
public function login($e, $p) { return false; }
|
||||
public function isTotpPending() { return isset($_GET['totp']); }
|
||||
public function verifyTotpLogin($c) { return false; }
|
||||
public function enableTotp($u, $s) { return true; }
|
||||
public function disableTotp($u) { return true; }
|
||||
public function generateBackupCodes($n = 8) { return ['A1B2-C3D4','E5F6-G7H8','J9K0-L1M2','N3P4-Q5R6']; }
|
||||
public function regenerateBackupCodes($u) { return $this->generateBackupCodes(); }
|
||||
public function backupCodesRemaining($u) { return 6; }
|
||||
public function writeAuditLog(...$a) { return true; }
|
||||
public function logout() {}
|
||||
public function isLoggedIn() { return !isset($_GET['anon']); }
|
||||
public function isAdmin() { return $this->isLoggedIn(); }
|
||||
public function getCurrentUser() { return $this->isLoggedIn() ? $this->user : null; }
|
||||
public function hasAccessToSite($id) { return true; }
|
||||
public function validateCsrfToken($t) { return true; }
|
||||
public function getCsrfToken() { return 'demo-csrf-token'; }
|
||||
public function registerUser(...$a) { return true; }
|
||||
public function requireAdmin() {}
|
||||
public function requireLogin() {}
|
||||
public function activeSessionCount() { return 3; }
|
||||
public function logoutOtherSessions() { return 2; }
|
||||
}
|
||||
164
tools/demo/overlay/includes/Database.php
Normal file
164
tools/demo/overlay/includes/Database.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
/** Demo-Stub: liefert feste Beispieldaten statt echter DB-Zugriffe. */
|
||||
class Database {
|
||||
private static $instance = null;
|
||||
private $settings = [
|
||||
'app_title' => 'UniFi Voucher System',
|
||||
'logo_url' => '', 'favicon_url' => '',
|
||||
'public_access' => '1', 'smtp_enabled' => '1', 'sms_enabled' => '1',
|
||||
'm365_enabled' => '1', 'oidc_enabled' => '0', 'oidc_name' => 'Keycloak',
|
||||
'default_expire_minutes' => '480', 'default_max_uses' => '2', 'max_uses_limit' => '10',
|
||||
'instruction_header' => 'So verbinden Sie sich',
|
||||
'instruction_text' => 'WLAN „Gast-WiFi" wählen, Code eingeben und bestätigen. Bei Fragen hilft der Empfang gerne weiter.',
|
||||
'last_cron_sync' => '2026-09-22 14:35:00',
|
||||
'session_driver' => 'db', 'captcha_mode' => 'math',
|
||||
'enforce_2fa_admins' => '1', 'user_daily_voucher_limit' => '25',
|
||||
'webhook_enabled' => '1', 'webhook_url' => 'https://hooks.slack.com/services/T024/B0197/XYZ',
|
||||
'twilio_sid' => 'AC8f2c1d94e77b40a2', 'twilio_from' => '+4915112345678',
|
||||
'trusted_proxy' => '10.0.0.1, 172.18.0.1',
|
||||
'cleanup_expired_days' => '90', 'cleanup_audit_days' => '365', 'cleanup_login_days' => '30',
|
||||
'last_cleanup' => '2026-09-22 03:00:00',
|
||||
'print_template' => '<div style="padding:20px;border:1px dashed #999;"><h2>{APP_TITLE}</h2><p>{SITE_NAME}</p><h1>{VOUCHER_CODE}</h1></div>',
|
||||
];
|
||||
public function __construct()
|
||||
{
|
||||
// Screenshot-Varianten: ?brand=custom|gradient|nopanel
|
||||
$brand = $_GET['brand'] ?? '';
|
||||
if ($brand === 'custom') {
|
||||
$this->settings = array_merge($this->settings, [
|
||||
'app_title' => 'Hotel Seeblick',
|
||||
'login_brand_name' => 'Hotel Seeblick',
|
||||
'login_logo_url' => '/demo-assets/logo.svg',
|
||||
'login_claim_title' => 'Willkommen im Hotel Seeblick.',
|
||||
'login_claim_text' => 'Gäste-WLAN für Zimmer, Tagungsräume und Restaurant – Zugangscodes direkt an der Rezeption erstellen.',
|
||||
'login_features' => "Code direkt beim Check-in ausdrucken\nTagungsgäste per E-Mail versorgen\nAuswertung je Haus und Etage",
|
||||
'login_footer' => '© 2026 Hotel Seeblick GmbH · Datenschutz · Impressum',
|
||||
'login_bg_image' => '/demo-assets/background.svg',
|
||||
'login_bg_overlay' => '45',
|
||||
'brand_accent' => '#0f766e',
|
||||
'brand_accent_dark' => '#2dd4bf',
|
||||
'brand_gradient_from' => '#0f766e',
|
||||
'brand_gradient_to' => '#0ea5e9',
|
||||
]);
|
||||
} elseif ($brand === 'nopanel') {
|
||||
$this->settings = array_merge($this->settings, [
|
||||
'login_panel_enabled' => '0',
|
||||
'login_brand_name' => 'Stadtwerke Nordheim',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getInstance() { return self::$instance ??= new self(); }
|
||||
public function getConnection() { return null; }
|
||||
public function getSetting($key, $default = null) { return $this->settings[$key] ?? $default; }
|
||||
public function setSetting($key, $value) { $this->settings[$key] = $value; }
|
||||
public function execute($sql, $params = []) { return 1; }
|
||||
public function query($sql, $params = []) { return new DemoStmt(); }
|
||||
public function fetchOne($sql, $params = []) { $r = $this->fetchAll($sql, $params); return $r[0] ?? false; }
|
||||
|
||||
public function fetchAll($sql, $params = []) {
|
||||
$s = preg_replace('/\s+/', ' ', strtolower($sql));
|
||||
|
||||
if (str_contains($s, 'count(*) as count from sites')) return [['count'=>4]];
|
||||
if (str_contains($s, 'count(*) as count from users')) return [['count'=>12]];
|
||||
if (str_contains($s, 'count(*) as count from vouchers where date(created_at)=curdate()')) return [['count'=>18]];
|
||||
if (str_contains($s, 'count(*) as count from vouchers where date(created_at)=')) {
|
||||
$day = $params[0] ?? '';
|
||||
$map = [6,9,5,12,8,4,7,14,11,16,9,6,13,18];
|
||||
$idx = abs((int)((strtotime($day) - strtotime('today')) / 86400));
|
||||
return [['count' => $map[13 - min($idx, 13)]]];
|
||||
}
|
||||
if (str_contains($s, 'from vouchers where site_id=?') && str_contains($s, 'count(*) as total')) {
|
||||
$per = [1=>['total'=>128,'valid'=>74,'used'=>39,'expired'=>15],
|
||||
2=>['total'=>63,'valid'=>41,'used'=>18,'expired'=>4],
|
||||
3=>['total'=>22,'valid'=>14,'used'=>6,'expired'=>2],
|
||||
4=>['total'=>0,'valid'=>0,'used'=>0,'expired'=>0]];
|
||||
return [$per[(int)($params[0] ?? 1)] ?? $per[4]];
|
||||
}
|
||||
if (str_contains($s, 'count(*) as total') && str_contains($s, 'from vouchers')) {
|
||||
return [['total'=>213,'valid'=>129,'used'=>63,'expired'=>21]];
|
||||
}
|
||||
if (str_contains($s, 'from sites')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Hauptstandort Nord','site_id'=>'default','unifi_controller_url'=>'https://unifi.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-01-12 09:00:00'],
|
||||
['id'=>2,'name'=>'Campus West','site_id'=>'campus-west','unifi_controller_url'=>'https://unifi.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-02-03 11:30:00'],
|
||||
['id'=>3,'name'=>'Showroom Berlin','site_id'=>'showroom','unifi_controller_url'=>'https://unifi-b.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>1,'created_at'=>'2026-04-21 15:10:00'],
|
||||
['id'=>4,'name'=>'Lager Süd','site_id'=>'lager','unifi_controller_url'=>'https://unifi-s.example.com:8443','unifi_username'=>'voucher-api','unifi_password'=>'x','is_active'=>1,'use_ssl'=>0,'created_at'=>'2026-06-08 08:45:00'],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'count(v.id) as voucher_count') || str_contains($s, 'from users u left join vouchers')) {
|
||||
return [
|
||||
['name'=>'Marie Sander','email'=>'marie.sander@example.com','voucher_count'=>48],
|
||||
['name'=>'Jonas Weber','email'=>'j.weber@example.com','voucher_count'=>31],
|
||||
['name'=>'Alina Brandt','email'=>'alina.brandt@example.com','voucher_count'=>27],
|
||||
['name'=>'Tim Kurz','email'=>'tim.kurz@example.com','voucher_count'=>19],
|
||||
['name'=>'Sara Vogt','email'=>'sara.vogt@example.com','voucher_count'=>12],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'from vouchers')) {
|
||||
if (str_contains($s, 'group by date')) {
|
||||
$out = []; $vals = [6,9,5,12,8,4,7,14,11,16,9,6,13,18];
|
||||
foreach ($vals as $i => $c) { $out[] = ['date'=>date('d.m', strtotime('-'.(13-$i).' days')), 'count'=>$c]; }
|
||||
return $out;
|
||||
}
|
||||
$codes = [['H7K2-M9QF',1,'Hauptstandort Nord','valid'],['P3RT-8ZXC',2,'Campus West','used'],
|
||||
['QW4E-7TYU',1,'Hauptstandort Nord','valid'],['LM2N-5BVD',3,'Showroom Berlin','expired'],
|
||||
['ZX9C-3PLO',2,'Campus West','valid'],['FG6H-1JKL',1,'Hauptstandort Nord','used']];
|
||||
$names = ['Besuch Agentur Nordlicht','Workshop Raum 2','Empfang Tagesgast','Messe-Stand','Handwerker Haustechnik','Bewerbungsgespräch'];
|
||||
$out = [];
|
||||
foreach ($codes as $i => [$c,$sid,$sname,$st]) {
|
||||
$out[] = ['id'=>$i+1,'voucher_code'=>$c,'site_id'=>$sid,'site_name'=>$sname,'user_name'=>'Marie Sander',
|
||||
'status'=>$st,'created_at'=>date('Y-m-d H:i:s', strtotime("-{$i} hours")),
|
||||
'voucher_name'=>$names[$i],'unifi_voucher_id'=>'65f1a'.$i,
|
||||
'expires_at'=>date('Y-m-d H:i:s', strtotime('+'.(8-$i).' hours')),
|
||||
'max_uses'=>[2,5,1,3,2,1][$i],'used_count'=>[2,1,0,3,0,1][$i],'expire_minutes'=>480];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if (str_contains($s, 'from users')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Marie Sander','email'=>'marie.sander@example.com','is_admin'=>1,'is_active'=>1,'created_at'=>'2026-01-10 09:00:00','last_login'=>'2026-09-22 08:12:00','totp_enabled'=>1,'auth_source'=>'local','voucher_count'=>48,'password_hash'=>'x'],
|
||||
['id'=>2,'name'=>'Jonas Weber','email'=>'j.weber@example.com','is_admin'=>0,'is_active'=>1,'created_at'=>'2026-03-02 13:20:00','last_login'=>'2026-09-21 17:40:00','totp_enabled'=>0,'auth_source'=>'m365','voucher_count'=>31,'password_hash'=>''],
|
||||
['id'=>3,'name'=>'Alina Brandt','email'=>'alina.brandt@example.com','is_admin'=>0,'is_active'=>1,'created_at'=>'2026-05-18 10:05:00','last_login'=>'2026-09-20 09:31:00','totp_enabled'=>1,'auth_source'=>'local','voucher_count'=>27,'password_hash'=>'x'],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'from api_keys')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Buchungssystem','key_prefix'=>'3f9a2c','scope'=>'write','rate_limit'=>60,'is_active'=>1,'last_used_at'=>'2026-09-22 09:12:00','creator'=>'Marie Sander','created_at'=>'2026-06-01 10:00:00'],
|
||||
['id'=>2,'name'=>'Terminal Foyer','key_prefix'=>'b71e04','scope'=>'read','rate_limit'=>0,'is_active'=>1,'last_used_at'=>'2026-09-21 17:40:00','creator'=>'Marie Sander','created_at'=>'2026-07-14 16:20:00'],
|
||||
['id'=>3,'name'=>'Altes Kassensystem','key_prefix'=>'c0d5f8','scope'=>'write','rate_limit'=>30,'is_active'=>0,'last_used_at'=>null,'creator'=>'Jonas Weber','created_at'=>'2026-02-09 08:05:00'],
|
||||
];
|
||||
}
|
||||
if (str_contains($s, 'from audit_log')) {
|
||||
if (str_contains($s, 'count(')) return [['total'=>1284,'c'=>1284,'count'=>1284]];
|
||||
if (str_contains($s, 'distinct action')) {
|
||||
return [['action'=>'voucher_created'],['action'=>'user_login'],['action'=>'settings_saved'],['action'=>'user_created'],['action'=>'template_updated']];
|
||||
}
|
||||
$rows = [['2026-09-22 14:32:00','Marie Sander','voucher_created','Hauptstandort Nord · H7K2-M9QF','192.168.10.42'],
|
||||
['2026-09-22 13:58:00','Jonas Weber','user_login','Microsoft 365','83.112.4.17'],
|
||||
['2026-09-22 11:04:00','Marie Sander','settings_saved','Tab: E-Mail (SMTP)','192.168.10.42'],
|
||||
['2026-09-21 17:40:00','Alina Brandt','user_created','tim.kurz@example.com','192.168.10.88'],
|
||||
['2026-09-21 09:12:00','Marie Sander','template_updated','Tagesgast','192.168.10.42'],
|
||||
['2026-09-20 16:03:00','Jonas Weber','voucher_bulk','5 Vouchers · Campus West','83.112.4.17']];
|
||||
$out = [];
|
||||
foreach ($rows as $i => [$d,$u,$a,$det,$ip]) {
|
||||
$out[] = ['id'=>$i+1,'created_at'=>$d,'user_name'=>$u,'user_email'=>'demo@example.com','action'=>$a,'details'=>$det,'ip_address'=>$ip,'entity_type'=>'voucher','entity_id'=>$i+1];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if (str_contains($s, 'templates')) {
|
||||
return [
|
||||
['id'=>1,'name'=>'Tagesgast','description'=>'Standardprofil für Besucher am Empfang','max_uses'=>2,'expire_minutes'=>480,'qos_rate_max_down'=>20000,'qos_rate_max_up'=>5000,'qos_usage_quota'=>0,'is_active'=>1,'is_default'=>1,'created_at'=>'2026-01-15 10:00:00'],
|
||||
['id'=>2,'name'=>'Konferenz','description'=>'Mehrtägige Veranstaltungen','max_uses'=>5,'expire_minutes'=>4320,'qos_rate_max_down'=>50000,'qos_rate_max_up'=>10000,'qos_usage_quota'=>0,'is_active'=>1,'is_default'=>0,'created_at'=>'2026-02-20 11:30:00'],
|
||||
['id'=>3,'name'=>'Handwerker','description'=>'Kurzzugang für Dienstleister','max_uses'=>1,'expire_minutes'=>240,'qos_rate_max_down'=>10000,'qos_rate_max_up'=>2000,'qos_usage_quota'=>1024,'is_active'=>1,'is_default'=>0,'created_at'=>'2026-03-08 09:15:00'],
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
class DemoStmt {
|
||||
public function fetch($m = null) { return false; }
|
||||
public function fetchAll($m = null) { return []; }
|
||||
public function rowCount() { return 1; }
|
||||
public function execute($p = []) { return true; }
|
||||
}
|
||||
|
|
@ -20,12 +20,8 @@ $channels = \Updater\UpdateManager::CHANNELS;
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>System-Update</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="../assets/global.css">
|
||||
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
|
||||
<?php require_once dirname(__DIR__, 2) . '/includes/Ui.php'; ?>
|
||||
<?= Ui::head(null, '../') ?>
|
||||
<style>
|
||||
/* Updater-spezifisch – der Rest kommt aus dem gemeinsamen Design-System. */
|
||||
.wrap { max-width: 780px; margin: 0 auto; padding: 36px 0 60px; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue