Dieses Premium Script ist exklusiv für unsere Newsletter Abonnenten!
Gebe deine eMail Adresse zum kostenlosen Freischalten ein!
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>";
?>