Files
dasposchi-de/admin/article-edit.php
Claude 3c97192386 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
2026-04-05 20:59:52 +00:00

267 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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';