Dieses Dreamcodes Script ist ein vollwertiger Gästebuch Hosting Service in nur einer einzigen PHP-Datei. Beim ersten Start entpackt das Master-Skript automatisch alle notwendigen Ordner, Konfigurationsdateien, API-Endpunkte und Admin-Templates. So entsteht eine komplette, modular aufgebaute Anwendung, ohne dass zusätzliche Dateien hochgeladen werden müssen.
Die Lösung bietet eine moderne Architektur mit API und Admin-Dashboard, die sowohl für Einsteiger als auch für fortgeschrittene Anwender leicht zu bedienen ist:
- Automatische Installation: Erstaufruf erstellt alle benötigten Dateien, Ordner und SQLite-Datenbank.
- API-Endpunkte: Trennung von Logik und Darstellung mit REST-ähnlicher Struktur für Einträge, Authentifizierung und Moderation.
- Admin-Dashboard: Integriertes HTML- und JS-Template mit moderner UI für Freigaben, Löschungen, Benachrichtigungen und Statistiken.
- Captcha-Absicherung: Einfache Mathe-Challenges verhindern Spam.
- Datei-Uploads: Bilder hochladen, automatisch skalieren und als Thumbnails einbinden.
- E-Mail-Benachrichtigung: Bei neuen Einträgen wird automatisch eine E-Mail verschickt (SMTP / PHPMailer).
- RSS-Feed: Generierung eines Feeds mit allen freigegebenen Einträgen für einfache Einbindung in andere Systeme.
- Theming: Farben, Logo und Layout lassen sich flexibel anpassen.
Das Skript ist so konzipiert, dass es auf jedem Standard-Webspace mit PHP Unterstützung sofort lauffähig ist, keine manuelle Konfiguration notwendig. Extrem einfach und schnell in Betrieb zu nehmen. 🙂
<?php
// Als setup.php abspeichern
// Lege diese Datei in dein Webroot und rufe sie einmal im Browser auf
// Danach Datei löschen oder sichern. Rufe dann die erstellte index.php Datei auf
set_time_limit(0);
error_reporting(E_ALL);
ini_set('display_errors', 1);
$root = __DIR__;
// gewünschte Struktur
$dirs = [
'public',
'public/assets',
'api',
'admin',
'src',
'data',
'data/uploads',
'data/thumbnails'
];
// dateien die erstellt werden
$files = [
'.env',
'public/index.php',
'public/rss.php',
'public/assets/style.css',
'public/assets/app.js',
'api/entries_add.php',
'api/entries_list.php',
'api/upload.php',
'api/auth_login.php',
'api/auth_logout.php',
'api/admin_export.php',
'src/config.php',
'src/db.php',
'src/captcha.php',
'src/upload_utils.php',
'src/helpers.php',
'admin/index.php',
'admin/dashboard.php',
'README.md'
];
// helper zur erzeugung
function makeDirs($root, $dirs) {
$created = [];
foreach ($dirs as $d) {
$path = $root . DIRECTORY_SEPARATOR . $d;
if (!is_dir($path)) {
if (!mkdir($path, 0755, true)) {
echo "Fehler beim Erstellen des Ordners: $path<br>";
} else {
$created[] = $d;
}
}
}
return $created;
}
function writeFile($fullPath, $content, $mode = 0644) {
$dir = dirname($fullPath);
if (!is_dir($dir)) mkdir($dir, 0755, true);
file_put_contents($fullPath, $content);
@chmod($fullPath, $mode);
}
// 1 create dirs
$createdDirs = makeDirs($root, $dirs);
// 2 create .env if not exists
$envPath = $root . DIRECTORY_SEPARATOR . '.env';
if (!file_exists($envPath)) {
$envContent = <<<ENV
# Beispiel konfigurationsdatei
DB_DRIVER=sqlite
DB_SQLITE_PATH={$root}/data/guestbook.sqlite
# für MySQL statt sqlite die DB_DSN nutzen
# DB_DSN=mysql:host=host;dbname=guestbook;charset=utf8mb4
# DB_USER=youruser
# DB_PASS=yourpass
ADMIN_USER=admin
ADMIN_PASS=admin123
SMTP_HOST=
SMTP_USER=
SMTP_PASS=
SMTP_PORT=587
BASE_URL=http://localhost
ADMIN_NOTIFICATION_EMAIL=
ENV;
writeFile($envPath, $envContent);
}
// 3 create sqlite db and tables if not exist
$dbFile = $root . '/data/guestbook.sqlite';
if (!file_exists($dbFile)) {
$pdo = new PDO('sqlite:' . $dbFile);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("PRAGMA journal_mode = WAL");
$pdo->exec("
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
passhash TEXT NOT NULL,
role TEXT DEFAULT 'admin',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
");
$pdo->exec("
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT,
site TEXT,
message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
approved INTEGER DEFAULT 0,
ip TEXT,
user_agent TEXT
);
");
$pdo->exec("
CREATE TABLE IF NOT EXISTS uploads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entry_id INTEGER,
filename TEXT,
stored_path TEXT,
thumb_path TEXT,
mime TEXT,
size INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
");
// default admin user
$defaultAdmin = getenv('ADMIN_USER') ?: 'admin';
$defaultPass = getenv('ADMIN_PASS') ?: 'admin123';
$hash = password_hash($defaultPass, PASSWORD_DEFAULT);
$stmt = $pdo->prepare("INSERT OR IGNORE INTO users (username, passhash) VALUES (:u, :p)");
$stmt->execute([':u' => $defaultAdmin, ':p' => $hash]);
}
// 4 helper dateiinhalte erzeugen
// public index
$publicIndex = <<<'PHP'
<?php
// public/index.php
// Frontend mit einfachem Formular und lade der Eintraege via AJAX
$root = dirname(__DIR__);
require_once $root . '/src/config.php';
$config = require $root . '/src/config.php';
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Dreamcodes - Gästebuch</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<div class="container">
<header class="card">
<h1>Gästebuch</h1>
<p class="small">Schreibe einen Eintrag oder lese die freigegebenen Einträge</p>
</header>
<main class="grid">
<section class="card">
<h3>Neuer Eintrag</h3>
<div id="result" class="small"></div>
<div>
<input id="name" class="input" placeholder="Name optional">
<input id="email" class="input" placeholder="E Mail optional">
<input id="site" class="input" placeholder="Website optional">
<label id="captchaQuestion" class="small">Lade Captcha...</label>
<input id="captchaAnswer" class="input" placeholder="Antwort">
<textarea id="message" class="input" rows="5" placeholder="Deine Nachricht"></textarea>
<div style="margin-top:8px">
<input type="file" id="file" />
</div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px">
<div class="small">Max 3 MB</div>
<button class="btn" onclick="submitEntry()">Absenden</button>
</div>
</div>
</section>
<section class="card">
<h3>Freigegebene Einträge</h3>
<div>
<input id="search" class="input" placeholder="Suche..." onkeydown="if (event.key==='Enter') loadEntries(1)" />
</div>
<div id="entriesArea" style="margin-top:12px"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px">
<div id="entriesInfo" class="small"></div>
<div>
<button class="btn ghost" onclick="prevPage()">‹</button>
<button class="btn" onclick="nextPage()">›</button>
</div>
</div>
</section>
</main>
<footer class="footer card">
<div>Made with ❤️ by <a href="http://www.dreamcodes.net" target="_blank" rel="noopener">Dreamcodes</a></div>
</footer>
</div>
<script src="assets/app.js"></script>
</body>
</html>
PHP;
// public rss
$publicRss = <<<'PHP'
<?php
// public/rss.php
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
$items = $pdo->query("SELECT * FROM entries WHERE approved=1 ORDER BY created_at DESC LIMIT 30")->fetchAll(PDO::FETCH_ASSOC);
header('Content-Type: application/rss+xml; charset=utf-8');
echo '<?xml version="1.0" encoding="utf-8"?>';
?>
<rss version="2.0">
<channel>
<title>Gästebuch</title>
<link><?php echo htmlspecialchars((require __DIR__ . '/../src/config.php')['base_url']); ?></link>
<description>Neueste Gästebucheinträge</description>
<?php foreach($items as $it): ?>
<item>
<title><?php echo htmlspecialchars(substr($it['message'],0,60)); ?></title>
<pubDate><?php echo date(DATE_RSS, strtotime($it['created_at'])); ?></pubDate>
<description><![CDATA[<?php echo nl2br(htmlspecialchars($it['message'])); ?>]]></description>
<guid><?php echo htmlspecialchars($it['id']); ?></guid>
</item>
<?php endforeach; ?>
</channel>
</rss>
PHP;
// assets css
$styleCss = <<<'CSS'
:root{--accent:#2b7a78;--muted:#666;--bg:#f7f7f7;--card:#fff}
body{font-family:Inter,Arial,sans-serif;background:var(--bg);color:#222;margin:0}
.container{max-width:1100px;margin:28px auto;padding:18px}
.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px}
.card{background:var(--card);border-radius:10px;padding:14px;box-shadow:0 6px 16px rgba(20,20,30,0.04)}
.grid{display:grid;grid-template-columns:1fr 420px;gap:18px}
.input, textarea, select{width:100%;padding:10px;border:1px solid #e6e6e9;border-radius:8px;font-size:14px}
.btn{background:var(--accent);color:#fff;padding:10px 12px;border-radius:8px;border:none;cursor:pointer}
.btn.ghost{background:transparent;color:var(--accent);border:1px solid #dfe6e6}
.small{font-size:13px;color:var(--muted)}
.footer{margin-top:16px;font-size:13px;color:var(--muted);text-align:center}
@media(max-width:900px){.grid{grid-template-columns:1fr}}
CSS;
// assets js
$appJs = <<<'JS'
const API_BASE = '/api';
let currentPage = 1;
function ajaxFormData(url, fd, cb) {
fetch(url, {method:'POST', body:fd}).then(r=>r.json()).then(cb).catch(e=>console.error(e));
}
function ajax(url, data, cb) {
const fd = new URLSearchParams();
for (const k in data) fd.append(k, data[k]);
fetch(url, {method:'POST', body:fd}).then(r=>r.json()).then(cb).catch(e=>console.error(e));
}
function loadCaptcha(){
fetch('/api/captcha.php').then(r=>r.json()).then(d=>{
document.getElementById('captchaQuestion').innerText = d.question;
});
}
function submitEntry(){
const name = document.getElementById('name').value;
const email = document.getElementById('email').value;
const site = document.getElementById('site').value;
const message = document.getElementById('message').value;
const captcha = document.getElementById('captchaAnswer').value;
const file = document.getElementById('file').files[0];
const fd = new FormData();
fd.append('action','addEntry');
fd.append('name', name);
fd.append('email', email);
fd.append('site', site);
fd.append('message', message);
fd.append('captcha', captcha);
if (file) fd.append('file', file);
ajaxFormData('/api/entries_add.php', fd, function(resp){
document.getElementById('result').innerText = resp.ok ? resp.msg : (resp.error||'Fehler');
if (resp.ok) {
document.getElementById('message').value = '';
loadCaptcha();
}
});
}
function loadEntries(page = 1){
currentPage = page;
const q = document.getElementById('search').value;
ajax('/api/entries_list.php', {page: page, q: q}, function(resp){
if (!resp.ok) return;
const area = document.getElementById('entriesArea');
area.innerHTML = '';
if (resp.entries.length === 0) { area.innerHTML = '<div class="small">Keine Einträge gefunden</div>'; document.getElementById('entriesInfo').innerText = ''; return; }
resp.entries.forEach(e=>{
const div = document.createElement('div');
div.className = 'card';
div.style.marginBottom = '10px';
div.innerHTML = '<div class="small">'+(e.name||'Anonym')+' • '+(e.created_at)+'</div><div style="margin-top:6px">'+e.message.replace(/\n/g,'<br>')+'</div>';
area.appendChild(div);
});
const start = (resp.page-1)*resp.perPage + 1;
const end = start + resp.entries.length - 1;
document.getElementById('entriesInfo').innerText = 'Zeige '+start+'–'+end+' von '+resp.total;
});
}
function prevPage(){ if (currentPage>1) loadEntries(currentPage-1); }
function nextPage(){ loadEntries(currentPage+1); }
window.addEventListener('load', function(){
loadCaptcha();
loadEntries(1);
});
JS;
// src/config.php
$srcConfig = <<<'PHP'
<?php
// src/config.php
$root = dirname(__DIR__);
$envFile = $root . '/.env';
$env = [];
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || strpos($line, '#') === 0) continue;
if (strpos($line, '=') === false) continue;
[$k, $v] = explode('=', $line, 2);
$env[trim($k)] = trim($v);
}
}
return [
'db_driver' => $env['DB_DRIVER'] ?? 'sqlite',
'db_dsn' => $env['DB_DSN'] ?? null,
'db_sqlite_path' => $env['DB_SQLITE_PATH'] ?? $root . '/data/guestbook.sqlite',
'db_user' => $env['DB_USER'] ?? null,
'db_pass' => $env['DB_PASS'] ?? null,
'admin_user' => $env['ADMIN_USER'] ?? 'admin',
'admin_pass' => $env['ADMIN_PASS'] ?? 'admin123',
'smtp_host' => $env['SMTP_HOST'] ?? '',
'smtp_user' => $env['SMTP_USER'] ?? '',
'smtp_pass' => $env['SMTP_PASS'] ?? '',
'smtp_port' => $env['SMTP_PORT'] ?? 587,
'upload_dir' => $env['UPLOAD_DIR'] ?? $root . '/data/uploads',
'thumb_dir' => $env['THUMB_DIR'] ?? $root . '/data/thumbnails',
'base_url' => rtrim($env['BASE_URL'] ?? '', '/'),
'admin_notify_email' => $env['ADMIN_NOTIFICATION_EMAIL'] ?? ($env['SMTP_USER'] ?? null),
'max_upload_size' => 3 * 1024 * 1024
];
PHP;
// src/db.php
$srcDb = <<<'PHP'
<?php
// src/db.php
function getPdo() {
static $pdo = null;
if ($pdo) return $pdo;
$cfg = require __DIR__ . '/config.php';
if ($cfg['db_driver'] === 'sqlite' || empty($cfg['db_dsn'])) {
$path = $cfg['db_sqlite_path'];
if (!file_exists(dirname($path))) mkdir(dirname($path), 0755, true);
$pdo = new PDO('sqlite:' . $path);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("PRAGMA journal_mode = WAL");
} else {
$pdo = new PDO($cfg['db_dsn'], $cfg['db_user'], $cfg['db_pass'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
}
// ensure tables
$pdo->exec("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE, passhash TEXT, role TEXT DEFAULT 'admin', created_at DATETIME DEFAULT CURRENT_TIMESTAMP)");
$pdo->exec("CREATE TABLE IF NOT EXISTS entries (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, site TEXT, message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, approved INTEGER DEFAULT 0, ip TEXT, user_agent TEXT)");
$pdo->exec("CREATE TABLE IF NOT EXISTS uploads (id INTEGER PRIMARY KEY AUTOINCREMENT, entry_id INTEGER, filename TEXT, stored_path TEXT, thumb_path TEXT, mime TEXT, size INTEGER, created_at DATETIME DEFAULT CURRENT_TIMESTAMP)");
return $pdo;
}
PHP;
// src/captcha.php
$srcCaptcha = <<<'PHP'
<?php
// src/captcha.php
session_start();
function captchaQuestion() {
$a = random_int(2,9);
$b = random_int(2,9);
$_SESSION['captcha_answer'] = $a + $b;
return "Was ist $a + $b ?";
}
function captchaCheck($val) {
if (!isset($_SESSION['captcha_answer'])) return false;
$ok = ((int)$val === (int)$_SESSION['captcha_answer']);
unset($_SESSION['captcha_answer']);
return $ok;
}
PHP;
// src/upload_utils.php
$srcUpload = <<<'PHP'
<?php
// src/upload_utils.php
function sanitizeFilename($name) {
$name = preg_replace('/[^a-zA-Z0-9._-]/', '_', $name);
$name = preg_replace('/_+/', '_', $name);
return substr($name, 0, 200);
}
function createThumbnail($srcPath, $dstPath, $maxW = 400, $maxH = 400) {
if (!file_exists($srcPath)) return false;
$info = @getimagesize($srcPath);
if (!$info) return false;
[$w, $h] = $info;
$mime = $info['mime'];
$ratio = min($maxW / $w, $maxH / $h, 1);
$nw = max(1, (int)($w * $ratio));
$nh = max(1, (int)($h * $ratio));
$dst = imagecreatetruecolor($nw, $nh);
switch ($mime) {
case 'image/jpeg': $src = imagecreatefromjpeg($srcPath); break;
case 'image/png': $src = imagecreatefrompng($srcPath); imagealphablending($dst, false); imagesavealpha($dst, true); break;
case 'image/gif': $src = imagecreatefromgif($srcPath); break;
default: return false;
}
imagecopyresampled($dst, $src, 0,0,0,0,$nw,$nh,$w,$h);
imagejpeg($dst, $dstPath, 85);
imagedestroy($dst); imagedestroy($src);
return true;
}
PHP;
// src/helpers.php
$srcHelpers = <<<'PHP'
<?php
// src/helpers.php
function esc($s) {
return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
PHP;
// api/entries_add.php
$apiEntriesAdd = <<<'PHP'
<?php
// api/entries_add.php
session_start();
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/captcha.php';
require_once __DIR__ . '/../src/upload_utils.php';
$config = require __DIR__ . '/../src/config.php';
$pdo = getPdo();
header('Content-Type: application/json; charset=utf-8');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$name = trim(substr($_POST['name'] ?? 'Anonym', 0, 120));
$email = trim(substr($_POST['email'] ?? '', 0, 180));
$site = trim(substr($_POST['site'] ?? '', 0, 255));
$message = trim(substr($_POST['message'] ?? '', 0, 5000));
$captcha = $_POST['captcha'] ?? '';
// validation
if (strlen($message) < 3) { echo json_encode(['ok'=>false,'error'=>'Die Nachricht ist zu kurz']); exit; }
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) { echo json_encode(['ok'=>false,'error'=>'Ungültige E Mail']); exit; }
if (!captchaCheck($captcha)) { echo json_encode(['ok'=>false,'error'=>'Captcha falsch']); exit; }
// rate limit simple
$stmt = $pdo->prepare("SELECT COUNT(*) FROM entries WHERE ip = :ip AND created_at > datetime('now','-1 hour')");
$stmt->execute([':ip'=>$ip]);
if ($stmt->fetchColumn() >= 6) { echo json_encode(['ok'=>false,'error'=>'Zu viele Anfragen']); exit; }
// handle file
$storedPath = null; $thumbPath = null;
if (!is_dir($config['upload_dir'])) mkdir($config['upload_dir'], 0755, true);
if (!is_dir($config['thumb_dir'])) mkdir($config['thumb_dir'], 0755, true);
if (!empty($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
$allowed = ['image/jpeg','image/png','image/gif'];
$fileType = $_FILES['file']['type'] ?? '';
if (!in_array($fileType, $allowed)) { echo json_encode(['ok'=>false,'error'=>'Nur Bilder']); exit; }
if ($_FILES['file']['size'] > $config['max_upload_size']) { echo json_encode(['ok'=>false,'error'=>'Datei zu groß']); exit; }
$fname = sanitizeFilename(basename($_FILES['file']['name']));
$uniq = uniqid('', true) . '_' . $fname;
$dest = rtrim($config['upload_dir'],'/') . '/' . $uniq;
if (!move_uploaded_file($_FILES['file']['tmp_name'], $dest)) { echo json_encode(['ok'=>false,'error'=>'Upload fehlgeschlagen']); exit; }
$thumb = rtrim($config['thumb_dir'],'/') . '/' . 'thumb_' . $uniq . '.jpg';
createThumbnail($dest, $thumb, 400, 400);
$storedPath = $dest;
$thumbPath = $thumb;
}
// insert
$stmt = $pdo->prepare("INSERT INTO entries (name,email,site,message,approved,ip,user_agent) VALUES (:n,:e,:s,:m,0,:ip,:ua)");
$stmt->execute([':n'=>$name,':e'=>$email,':s'=>$site,':m'=>$message,':ip'=>$ip,':ua'=>$_SERVER['HTTP_USER_AGENT'] ?? '']);
$entryId = $pdo->lastInsertId();
if ($storedPath) {
$stmt = $pdo->prepare("INSERT INTO uploads (entry_id,filename,stored_path,thumb_path,mime,size) VALUES (:eid,:fn,:sp,:tp,:mime,:size)");
$stmt->execute([':eid'=>$entryId,':fn'=>basename($storedPath),':sp'=>$storedPath,':tp'=>$thumbPath,':mime'=>$fileType,':size'=>filesize($storedPath)]);
}
// notify admin
if (!empty($config['admin_notify_email'])) {
$to = $config['admin_notify_email'];
$subject = "Neuer Gästebucheintrag zur Prüfung";
$body = "Neuer Eintrag von " . ($name ?: 'Anonym') . "\n\n" . strip_tags($message) . "\n\n";
@mail($to, $subject, $body, 'From: noreply@' . ($_SERVER['SERVER_NAME'] ?? 'localhost'));
}
echo json_encode(['ok'=>true,'msg'=>'Danke, dein Eintrag wurde gesendet']); exit;
PHP;
// api/entries_list.php
$apiEntriesList = <<<'PHP'
<?php
// api/entries_list.php
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
header('Content-Type: application/json; charset=utf-8');
$page = max(1, (int)($_GET['page'] ?? $_POST['page'] ?? 1));
$perPage = 10;
$offset = ($page - 1) * $perPage;
$q = trim($_GET['q'] ?? $_POST['q'] ?? '');
if ($q !== '') {
$stmt = $pdo->prepare("SELECT * FROM entries WHERE approved = 1 AND (name LIKE :q OR message LIKE :q) ORDER BY created_at DESC LIMIT :l OFFSET :o");
$stmt->bindValue(':q', '%' . $q . '%', PDO::PARAM_STR);
} else {
$stmt = $pdo->prepare("SELECT * FROM entries WHERE approved = 1 ORDER BY created_at DESC LIMIT :l OFFSET :o");
}
$stmt->bindValue(':l', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':o', $offset, PDO::PARAM_INT);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$total = (int)$pdo->query("SELECT COUNT(*) FROM entries WHERE approved = 1")->fetchColumn();
echo json_encode(['ok'=>true,'entries'=>$rows,'total'=>$total,'page'=>$page,'perPage'=>$perPage]);
PHP;
// api/upload.php
$apiUpload = <<<'PHP'
<?php
// api/upload.php
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/upload_utils.php';
$config = require __DIR__ . '/../src/config.php';
header('Content-Type: application/json; charset=utf-8');
if (empty($_FILES['file']) || $_FILES['file']['error'] !== UPLOAD_ERR_OK) { echo json_encode(['ok'=>false,'error'=>'Keine Datei']); exit; }
$allowed = ['image/jpeg','image/png','image/gif'];
$fileType = $_FILES['file']['type'] ?? '';
if (!in_array($fileType, $allowed)) { echo json_encode(['ok'=>false,'error'=>'Nur Bilder']); exit; }
if ($_FILES['file']['size'] > $config['max_upload_size']) { echo json_encode(['ok'=>false,'error'=>'Datei zu groß']); exit; }
if (!is_dir($config['upload_dir'])) mkdir($config['upload_dir'], 0755, true);
if (!is_dir($config['thumb_dir'])) mkdir($config['thumb_dir'], 0755, true);
$fname = sanitizeFilename(basename($_FILES['file']['name']));
$uniq = uniqid('', true) . '_' . $fname;
$dest = rtrim($config['upload_dir'],'/') . '/' . $uniq;
if (!move_uploaded_file($_FILES['file']['tmp_name'], $dest)) { echo json_encode(['ok'=>false,'error'=>'Upload fehlgeschlagen']); exit; }
$thumb = rtrim($config['thumb_dir'],'/') . '/' . 'thumb_' . $uniq . '.jpg';
createThumbnail($dest, $thumb, 400, 400);
echo json_encode(['ok'=>true,'path'=>$dest,'thumb'=>$thumb]);
PHP;
// api/auth_login.php
$apiAuthLogin = <<<'PHP'
<?php
// api/auth_login.php
session_start();
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
header('Content-Type: application/json; charset=utf-8');
$user = trim($_POST['user'] ?? '');
$pass = $_POST['pass'] ?? '';
if ($user === '' || $pass === '') { echo json_encode(['ok'=>false,'error'=>'Ungültige Zugangsdaten']); exit; }
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :u LIMIT 1");
$stmt->execute([':u'=>$user]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && password_verify($pass, $row['passhash'])) {
$_SESSION['admin'] = $row['username'];
echo json_encode(['ok'=>true]); exit;
} else {
echo json_encode(['ok'=>false,'error'=>'Login fehlgeschlagen']); exit;
}
PHP;
// api/auth_logout.php
$apiAuthLogout = <<<'PHP'
<?php
// api/auth_logout.php
session_start();
unset($_SESSION['admin']);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['ok'=>true]);
PHP;
// api/admin_export.php
$apiAdminExport = <<<'PHP'
<?php
// api/admin_export.php
session_start();
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
header('Content-Type: application/json; charset=utf-8');
if (!isset($_SESSION['admin'])) { echo json_encode(['ok'=>false,'error'=>'Admin erforderlich']); exit; }
$stmt = $pdo->query("SELECT * FROM entries ORDER BY created_at DESC");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$exportFile = __DIR__ . '/../data/guestbook_export_' . date('Ymd_His') . '.json';
file_put_contents($exportFile, json_encode($rows, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
echo json_encode(['ok'=>true,'file'=>basename($exportFile)]);
PHP;
// admin index
$adminIndex = <<<'HTML'
<?php
// admin/index.php
// Einfacher login seite mit ajax
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin Login</title>
<link rel="stylesheet" href="/public/assets/style.css">
</head>
<body>
<div class="container">
<div class="card" style="max-width:420px;margin:40px auto">
<h2>Admin Login</h2>
<div><input id="user" class="input" placeholder="Benutzer"></div>
<div style="margin-top:8px"><input id="pass" type="password" class="input" placeholder="Passwort"></div>
<div style="margin-top:8px"><button class="btn" onclick="login()">Login</button></div>
<div id="logMsg" class="small" style="margin-top:8px"></div>
</div>
</div>
<script>
function ajax(url, data, cb) {
const fd = new URLSearchParams();
for (const k in data) fd.append(k, data[k]);
fetch(url, {method:'POST', body:fd}).then(r=>r.json()).then(cb);
}
function login(){
const u = document.getElementById('user').value;
const p = document.getElementById('pass').value;
ajax('/api/auth_login.php',{user:u,pass:p}, function(res){
if (res.ok) { location.href = '/admin/dashboard.php'; } else { document.getElementById('logMsg').innerText = res.error || 'Fehler'; }
});
}
</script>
</body>
</html>
HTML;
// admin dashboard
$adminDashboard = <<<'HTML'
<?php
// admin/dashboard.php
session_start();
if (!isset($_SESSION['admin'])) { header('Location: /admin/index.php'); exit; }
?>
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Admin Dashboard</title>
<link rel="stylesheet" href="/public/assets/style.css">
</head>
<body>
<div class="container">
<header class="card" style="display:flex;justify-content:space-between;align-items:center">
<h2>Moderation</h2>
<div>
Eingeloggt als <strong><?php echo htmlspecialchars($_SESSION['admin']); ?></strong>
<button class="btn ghost" onclick="logout()">Logout</button>
</div>
</header>
<main class="grid">
<section class="card" style="min-height:400px">
<h3>Einträge</h3>
<div style="margin-bottom:8px">
<button class="btn" onclick="fetchEntries()">Laden</button>
<button class="btn ghost" onclick="exportJson()">Export JSON</button>
</div>
<div id="adminList" style="max-height:500px;overflow:auto"></div>
</section>
<aside class="card">
<h4>Info</h4>
<div class="small">Moderation Werkzeuge: Freigeben, bearbeiten, löschen</div>
</aside>
</main>
</div>
<script>
function ajax(url, data, cb) {
const fd = new URLSearchParams();
for (const k in data) fd.append(k, data[k]);
fetch(url, {method:'POST', body:fd}).then(r=>r.json()).then(cb);
}
function fetchEntries(){
ajax('/api/admin_export.php', {}, function(res){
if (!res.ok) { alert(res.error||'Fehler'); return; }
// liste anzeigen: lade die export datei
fetch('/data/' + res.file).then(r=>r.json()).then(data=>{
const list = document.getElementById('adminList'); list.innerHTML='';
data.forEach(e=>{
const div = document.createElement('div'); div.className='entry';
div.innerHTML = '<div class="small">#'+e.id+' • '+e.created_at+' • '+(e.approved==1?'<strong style="color:green">Freigegeben</strong>':'<strong style="color:orange">Ausstehend</strong>')+'</div>'
+'<div style="margin-top:6px">'+(e.name||'Anonym')+' • '+(e.email||'')+'</div>'
+'<div style="margin-top:6px">'+e.message.replace(/\n/g,"<br>")+'</div>'
+'<div style="margin-top:6px"><button class="btn" onclick="approve('+e.id+')">Freigeben</button> <button class="btn ghost" onclick="del('+e.id+')">Löschen</button></div>';
list.appendChild(div);
});
});
});
}
function approve(id){ ajax('/api/admin_actions.php', {action:'approve',id:id}, function(r){ if (r.ok) fetchEntries(); else alert(r.error||'Fehler'); }); }
function del(id){ if (!confirm('Eintrag wirklich löschen?')) return; ajax('/api/admin_actions.php', {action:'delete',id:id}, function(r){ if (r.ok) fetchEntries(); else alert(r.error||'Fehler'); }); }
function logout(){ ajax('/api/auth_logout.php', {}, function(r){ if (r.ok) location.href='/admin/index.php'; }); }
function exportJson(){ ajax('/api/admin_export.php', {}, function(r){ if (r.ok) alert('Export erstellt: ' + r.file); else alert(r.error||'Fehler'); }); }
</script>
</body>
</html>
HTML;
// README
$readme = <<<MD
Dreamcodes Guestbook Hoster
Dieses Projekt wurde automatisch erstellt.
Passe die .env Datei an und ändere das Admin Passwort sofort nach der ersten Anmeldung.
Powered by www.Dreamcodes.net
MD;
// schreibe dateien
$map = [
'public/index.php' => $publicIndex,
'public/rss.php' => $publicRss,
'public/assets/style.css' => $styleCss,
'public/assets/app.js' => $appJs,
'src/config.php' => $srcConfig,
'src/db.php' => $srcDb,
'src/captcha.php' => $srcCaptcha,
'src/upload_utils.php' => $srcUpload,
'src/helpers.php' => $srcHelpers,
'api/entries_add.php' => $apiEntriesAdd,
'api/entries_list.php' => $apiEntriesList,
'api/upload.php' => $apiUpload,
'api/auth_login.php' => $apiAuthLogin,
'api/auth_logout.php' => $apiAuthLogout,
'api/admin_export.php' => $apiAdminExport,
'admin/index.php' => $adminIndex,
'admin/dashboard.php' => $adminDashboard,
'README.md' => $readme
];
foreach ($map as $rel => $content) {
$full = $root . DIRECTORY_SEPARATOR . $rel;
writeFile($full, $content);
}
// create api/admin_actions.php used by dashboard for approve delete edit
$adminActions = <<<'PHP'
<?php
// api/admin_actions.php
session_start();
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
header('Content-Type: application/json; charset=utf-8');
if (!isset($_SESSION['admin'])) { echo json_encode(['ok'=>false,'error'=>'Admin erforderlich']); exit; }
$action = $_POST['action'] ?? '';
if ($action === 'approve') {
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("UPDATE entries SET approved = 1 WHERE id = :id");
$stmt->execute([':id'=>$id]);
echo json_encode(['ok'=>true]); exit;
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM entries WHERE id = :id");
$stmt->execute([':id'=>$id]);
echo json_encode(['ok'=>true]); exit;
}
// other actions like edit can be added
echo json_encode(['ok'=>false,'error'=>'Unbekannte Aktion']);
PHP;
writeFile($root . '/api/admin_actions.php', $adminActions);
// create captcha endpoint
$captchaApi = <<<'PHP'
<?php
// api/captcha.php
require_once __DIR__ . '/../src/captcha.php';
session_start();
$q = captchaQuestion();
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['question'=>$q]);
PHP;
writeFile($root . '/api/captcha.php', $captchaApi);
// success output
echo "<h2>Installation abgeschlossen</h2>";
echo "<p>Folgende Ordner wurden erstellt oder geprüft:</p><ul>";
foreach ($dirs as $d) echo "<li>" . htmlspecialchars($d) . "</li>";
echo "</ul>";
echo "<p>Folgende Dateipfade wurden angelegt:</p><ul>";
foreach (array_keys($map) as $f) echo "<li>" . htmlspecialchars($f) . "</li>";
echo "<li>api/admin_actions.php</li><li>api/captcha.php</li>";
echo "</ul>";
echo "<p>Die SQLite Datenbank liegt unter data/guestbook.sqlite</p>";
echo "<p>Standard Admin: admin Passwort: admin123 Bitte Passwort sofort ändern</p>";
echo "<p>Lösche diese Setup Datei nach erfolgreicher Installation</p>";
echo "<p><a href=\"/public/index.php\">Zum Frontend</a> | <a href=\"/admin/index.php\">Zum Admin Login</a></p>";
?>
