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:
266
admin/article-edit.php
Normal file
266
admin/article-edit.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../core/auth.php';
|
||||
require_once __DIR__ . '/../core/upload.php';
|
||||
auth_start_session();
|
||||
auth_require_login();
|
||||
|
||||
$pdo = db();
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
$article = null;
|
||||
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare('SELECT * FROM articles WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
$article = $stmt->fetch();
|
||||
if (!$article) {
|
||||
flash('error', 'Artikel nicht gefunden.');
|
||||
redirect('/admin/articles.php');
|
||||
}
|
||||
}
|
||||
|
||||
$categories = $pdo->query('SELECT id, name FROM categories ORDER BY sort_order, name')->fetchAll();
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
csrf_verify();
|
||||
|
||||
$title = trim($_POST['title'] ?? '');
|
||||
$slug = trim($_POST['slug'] ?? '');
|
||||
$excerpt = trim($_POST['excerpt'] ?? '');
|
||||
$body = $_POST['body'] ?? '';
|
||||
$categoryId = !empty($_POST['category_id']) ? (int) $_POST['category_id'] : null;
|
||||
$status = in_array($_POST['status'] ?? '', ['draft', 'published']) ? $_POST['status'] : 'draft';
|
||||
$publishedAt = trim($_POST['published_at'] ?? '');
|
||||
|
||||
// Validierung
|
||||
$errors = [];
|
||||
if ($title === '') {
|
||||
$errors[] = 'Titel ist erforderlich.';
|
||||
}
|
||||
if ($slug === '') {
|
||||
$slug = slugify($title);
|
||||
} else {
|
||||
$slug = slugify($slug);
|
||||
}
|
||||
if ($slug === '') {
|
||||
$errors[] = 'Slug konnte nicht generiert werden.';
|
||||
}
|
||||
|
||||
// Slug-Eindeutigkeit prüfen
|
||||
$slugCheck = $pdo->prepare('SELECT id FROM articles WHERE slug = ? AND id != ?');
|
||||
$slugCheck->execute([$slug, $id ?? 0]);
|
||||
if ($slugCheck->fetch()) {
|
||||
$errors[] = 'Dieser Slug wird bereits verwendet.';
|
||||
}
|
||||
|
||||
// Body sanitieren
|
||||
$body = sanitize_html($body);
|
||||
|
||||
// Cover-Bild
|
||||
$coverImage = $article['cover_image'] ?? null;
|
||||
if (!empty($_FILES['cover_image']['name'])) {
|
||||
$uploaded = handle_upload($_FILES['cover_image']);
|
||||
if ($uploaded === false) {
|
||||
$errors[] = 'Bild-Upload fehlgeschlagen. Erlaubt: JPG, PNG, GIF, WebP (max. 5 MB).';
|
||||
} else {
|
||||
$coverImage = $uploaded;
|
||||
}
|
||||
}
|
||||
if (isset($_POST['remove_cover']) && $_POST['remove_cover'] === '1') {
|
||||
$coverImage = null;
|
||||
}
|
||||
|
||||
if ($status === 'published' && empty($publishedAt)) {
|
||||
$publishedAt = date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
if (empty($errors)) {
|
||||
if ($id) {
|
||||
$stmt = $pdo->prepare(
|
||||
"UPDATE articles SET title=?, slug=?, excerpt=?, body=?, cover_image=?,
|
||||
category_id=?, status=?, published_at=?, updated_at=datetime('now') WHERE id=?"
|
||||
);
|
||||
$stmt->execute([$title, $slug, $excerpt, $body, $coverImage, $categoryId, $status, $publishedAt ?: null, $id]);
|
||||
flash('success', 'Artikel aktualisiert.');
|
||||
} else {
|
||||
$stmt = $pdo->prepare(
|
||||
'INSERT INTO articles (title, slug, excerpt, body, cover_image, category_id, status, published_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
);
|
||||
$stmt->execute([$title, $slug, $excerpt, $body, $coverImage, $categoryId, $status, $publishedAt ?: null]);
|
||||
$id = $pdo->lastInsertId();
|
||||
flash('success', 'Artikel erstellt.');
|
||||
}
|
||||
redirect('/admin/article-edit.php?id=' . $id);
|
||||
} else {
|
||||
foreach ($errors as $err) {
|
||||
flash('error', $err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pageTitle = $article ? 'Artikel bearbeiten' : 'Neuer Artikel';
|
||||
$currentPage = 'articles';
|
||||
|
||||
$extraHead = '
|
||||
<link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
|
||||
';
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<form method="post" enctype="multipart/form-data" class="edit-form" id="articleForm">
|
||||
<?= 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($article['title'] ?? $_POST['title'] ?? '') ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="slug">Slug</label>
|
||||
<input type="text" id="slug" name="slug"
|
||||
value="<?= e($article['slug'] ?? $_POST['slug'] ?? '') ?>"
|
||||
placeholder="Wird automatisch aus dem Titel generiert">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="excerpt">Kurzfassung</label>
|
||||
<textarea id="excerpt" name="excerpt" rows="3"><?= e($article['excerpt'] ?? $_POST['excerpt'] ?? '') ?></textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Inhalt</label>
|
||||
<div id="editor"><?= $article['body'] ?? $_POST['body'] ?? '' ?></div>
|
||||
<textarea name="body" id="bodyHidden" style="display:none"><?= e($article['body'] ?? $_POST['body'] ?? '') ?></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-col-4">
|
||||
<div class="card sidebar-card">
|
||||
<h4>Veröffentlichung</h4>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="radio" name="status" value="draft"
|
||||
<?= ($article['status'] ?? 'draft') === 'draft' ? 'checked' : '' ?>>
|
||||
Entwurf
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="status" value="published"
|
||||
<?= ($article['status'] ?? '') === 'published' ? 'checked' : '' ?>>
|
||||
Veröffentlicht
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="published_at">Datum</label>
|
||||
<input type="datetime-local" id="published_at" name="published_at"
|
||||
value="<?= e(str_replace(' ', 'T', $article['published_at'] ?? '')) ?>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category_id">Kategorie</label>
|
||||
<select id="category_id" name="category_id">
|
||||
<option value="">– Keine –</option>
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
<option value="<?= $cat['id'] ?>"
|
||||
<?= ($article['category_id'] ?? '') == $cat['id'] ? 'selected' : '' ?>>
|
||||
<?= e($cat['name']) ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card sidebar-card">
|
||||
<h4>Titelbild</h4>
|
||||
<?php if (!empty($article['cover_image'])): ?>
|
||||
<div class="cover-preview">
|
||||
<img src="<?= e($article['cover_image']) ?>" alt="Cover">
|
||||
<label>
|
||||
<input type="checkbox" name="remove_cover" value="1"> Entfernen
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<div class="form-group">
|
||||
<input type="file" name="cover_image" accept="image/*">
|
||||
</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("articleForm").addEventListener("submit", function() {
|
||||
document.getElementById("bodyHidden").value = quill.root.innerHTML;
|
||||
});
|
||||
|
||||
// Slug automatisch generieren
|
||||
var titleEl = document.getElementById("title");
|
||||
var slugEl = document.getElementById("slug");
|
||||
var slugManual = slugEl.value !== "";
|
||||
slugEl.addEventListener("input", function() { slugManual = slugEl.value !== ""; });
|
||||
titleEl.addEventListener("input", function() {
|
||||
if (!slugManual) {
|
||||
var s = 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,"");
|
||||
slugEl.value = s;
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
';
|
||||
|
||||
include __DIR__ . '/templates/layout.php';
|
||||
Reference in New Issue
Block a user