Initiales CMS: Deutschsprachiges Blog-System mit Admin-Bereich

Vollständiges, schlankes PHP/SQLite-CMS für IT-, KI- und Gaming-Inhalte:

- Core: DB-Singleton, Auth mit Passwort-Hashing, Session-Cookies,
  CSRF-Schutz, Login-Rate-Limit, Bild-Upload mit serverseitiger Validierung
- Admin: Dashboard, Artikel/Seiten-Verwaltung mit Quill WYSIWYG-Editor,
  Kategorien, Navigation (Drag & Drop), Medienbibliothek, Profil
- Frontend: Responsive Dark-Theme, Artikel-Grid, Kategorie-Filter,
  Archiv, Paginierung, SEO-Meta-Tags
- Sicherheit: Prepared Statements, HTML-Sanitizer, .htaccess-Schutz
  für sensible Verzeichnisse, PHP-Ausführungsschutz im Upload-Ordner
- Installation: install.php erstellt DB-Schema und Admin-Account

https://claude.ai/code/session_01Xsg4j2t4S9goMuWVpF3ezG
This commit is contained in:
Claude
2026-04-05 20:59:52 +00:00
commit 3c97192386
45 changed files with 2839 additions and 0 deletions

169
admin/page-edit.php Normal file
View File

@@ -0,0 +1,169 @@
<?php
require_once __DIR__ . '/../core/auth.php';
auth_start_session();
auth_require_login();
$pdo = db();
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
$page = null;
if ($id) {
$stmt = $pdo->prepare('SELECT * FROM pages WHERE id = ?');
$stmt->execute([$id]);
$page = $stmt->fetch();
if (!$page) {
flash('error', 'Seite nicht gefunden.');
redirect('/admin/pages.php');
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
csrf_verify();
$title = trim($_POST['title'] ?? '');
$slug = trim($_POST['slug'] ?? '');
$body = $_POST['body'] ?? '';
$status = in_array($_POST['status'] ?? '', ['draft', 'published']) ? $_POST['status'] : 'draft';
$errors = [];
if ($title === '') {
$errors[] = 'Titel ist erforderlich.';
}
if ($slug === '') {
$slug = slugify($title);
} else {
$slug = slugify($slug);
}
$slugCheck = $pdo->prepare('SELECT id FROM pages WHERE slug = ? AND id != ?');
$slugCheck->execute([$slug, $id ?? 0]);
if ($slugCheck->fetch()) {
$errors[] = 'Dieser Slug wird bereits verwendet.';
}
$body = sanitize_html($body);
if (empty($errors)) {
if ($id) {
$stmt = $pdo->prepare("UPDATE pages SET title=?, slug=?, body=?, status=?, updated_at=datetime('now') WHERE id=?");
$stmt->execute([$title, $slug, $body, $status, $id]);
flash('success', 'Seite aktualisiert.');
} else {
$stmt = $pdo->prepare('INSERT INTO pages (title, slug, body, status) VALUES (?, ?, ?, ?)');
$stmt->execute([$title, $slug, $body, $status]);
$id = $pdo->lastInsertId();
flash('success', 'Seite erstellt.');
}
redirect('/admin/page-edit.php?id=' . $id);
} else {
foreach ($errors as $err) {
flash('error', $err);
}
}
}
$pageTitle = $page ? 'Seite bearbeiten' : 'Neue Seite';
$currentPage = 'pages';
$extraHead = '<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">';
ob_start();
?>
<form method="post" class="edit-form" id="pageForm">
<?= csrf_field() ?>
<div class="form-row">
<div class="form-col-8">
<div class="form-group">
<label for="title">Titel</label>
<input type="text" id="title" name="title" required
value="<?= e($page['title'] ?? $_POST['title'] ?? '') ?>">
</div>
<div class="form-group">
<label for="slug">Slug</label>
<input type="text" id="slug" name="slug"
value="<?= e($page['slug'] ?? $_POST['slug'] ?? '') ?>"
placeholder="Wird automatisch generiert">
</div>
<div class="form-group">
<label>Inhalt</label>
<div id="editor"><?= $page['body'] ?? $_POST['body'] ?? '' ?></div>
<textarea name="body" id="bodyHidden" style="display:none"><?= e($page['body'] ?? $_POST['body'] ?? '') ?></textarea>
</div>
</div>
<div class="form-col-4">
<div class="card sidebar-card">
<h4>Status</h4>
<div class="form-group">
<label>
<input type="radio" name="status" value="draft"
<?= ($page['status'] ?? 'draft') === 'draft' ? 'checked' : '' ?>>
Entwurf
</label>
<label>
<input type="radio" name="status" value="published"
<?= ($page['status'] ?? '') === 'published' ? 'checked' : '' ?>>
Veröffentlicht
</label>
</div>
</div>
<button type="submit" class="btn btn-primary btn-block">Speichern</button>
</div>
</div>
</form>
<?php
$content = ob_get_clean();
$extraScripts = '
<script src="https://cdn.quilljs.com/1.3.7/quill.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function() {
var quill = new Quill("#editor", {
theme: "snow",
modules: {
toolbar: {
container: [
[{"header": [2, 3, 4, false]}],
["bold", "italic", "underline"],
[{"list": "ordered"}, {"list": "bullet"}],
["blockquote", "code-block"],
["link", "image"],
["clean"]
],
handlers: {
image: function() {
var input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", "image/*");
input.click();
input.onchange = function() {
var file = input.files[0];
if (!file) return;
var formData = new FormData();
formData.append("image", file);
formData.append("csrf_token", document.querySelector("[name=csrf_token]").value);
fetch("/admin/upload-handler.php", {method:"POST", body:formData})
.then(function(r){return r.json();})
.then(function(data){
if(data.success){var range=quill.getSelection(true);quill.insertEmbed(range.index,"image",data.url);}
else{alert(data.error||"Upload fehlgeschlagen");}
}).catch(function(){alert("Upload fehlgeschlagen");});
};
}
}
}
}
});
document.getElementById("pageForm").addEventListener("submit", function(){
document.getElementById("bodyHidden").value = quill.root.innerHTML;
});
var titleEl=document.getElementById("title"),slugEl=document.getElementById("slug"),slugManual=slugEl.value!=="";
slugEl.addEventListener("input",function(){slugManual=slugEl.value!=="";});
titleEl.addEventListener("input",function(){
if(!slugManual){slugEl.value=titleEl.value.toLowerCase().replace(/[äÄ]/g,"ae").replace(/[öÖ]/g,"oe").replace(/[üÜ]/g,"ue").replace(/ß/g,"ss").replace(/[^a-z0-9\\s-]/g,"").replace(/[\\s-]+/g,"-").replace(/^-|-$/g,"");}
});
});
</script>
';
include __DIR__ . '/templates/layout.php';