Unser Dreamcodes Short URL Skript ist ein vollwertiger URL Shortener, der sich komplett selbst installiert und alle notwendigen Dateien automatisch erstellt. Die Lösung ist in PHP, SQLite und AJAX umgesetzt und kommt in nur einer einzigen Datei, die beim ersten Aufruf sämtliche benötigten Ressourcen wie API-Endpunkte, Datenbankstruktur, Admin-Panel, JSON-Ordner und Templates generiert.
Mit unserem Short URL-Script kannst du:
- Beliebig viele kurze URLs erstellen und verwalten
- Klickstatistiken und detaillierte Analytics in Echtzeit einsehen
- Eigene Domains oder Subdomains für Links nutzen
- Ablaufdaten und Passwortschutz für Links festlegen
- Dateien und Medien in Links einbetten
- Ein Admin-Dashboard mit moderner Oberfläche für Moderation, User-Verwaltung und Freigaben nutzen
- Optional Captcha-Schutz gegen Spam aktivieren
- E-Mail-Benachrichtigungen bei neuen Links empfangen
- Einen öffentlichen oder privaten Shortener-Dienst betreiben 😉
Das System bietet zusätzlich eine API für externe Tools, mit der sich Links automatisiert erstellen und abfragen lassen. Dank des integrierten Installers ist keine manuelle Konfiguration nötig, die Script Struktur richtet sich selbstständig ein.
Einfach hochladen, aufrufen und loslegen. Dein eigener professioneller Dreamcodes URL Shortener Dienst ist sofort einsatzbereit.
<?php
// Datei ins Webroot legen, einmal im Browser aufrufen und danach Datei entfernen
set_time_limit(0);
error_reporting(E_ALL);
ini_set('display_errors', 1);
$root = __DIR__;
// Ordnerstruktur
$dirs = [
'public',
'public/assets',
'api',
'admin',
'src',
'data',
'data/qrcache'
];
// Dateien die erzeugt werden
$map = [
'.env' => null,
'public/index.php' => null,
'public/r.php' => null,
'public/rss.php' => null,
'public/assets/style.css' => null,
'public/assets/app.js' => null,
'api/shorten.php' => null,
'api/redirect.php' => null,
'api/stats.php' => null,
'api/auth_login.php' => null,
'api/auth_logout.php' => null,
'api/admin_export.php' => null,
'api/captcha.php' => null,
'src/config.php' => null,
'src/db.php' => null,
'src/helpers.php' => null,
'src/captcha.php' => null,
'admin/index.php' => null,
'admin/dashboard.php' => null,
'README.md' => null
];
// Hilfsfunktionen
function makeDirs($root, $dirs) {
$created = [];
foreach ($dirs as $d) {
$p = $root . DIRECTORY_SEPARATOR . $d;
if (!is_dir($p)) {
if (!mkdir($p, 0755, true)) {
echo "Fehler beim Erstellen Ordner: " . htmlspecialchars($p) . "<br>";
} else {
$created[] = $d;
}
}
}
return $created;
}
function writeFile($full, $content) {
$dir = dirname($full);
if (!is_dir($dir)) mkdir($dir, 0755, true);
file_put_contents($full, $content);
@chmod($full, 0644);
}
// 1 Erzeuge Ordner
$createdDirs = makeDirs($root, $dirs);
// 2 .env erzeugen wenn nicht vorhanden
$envPath = $root . DIRECTORY_SEPARATOR . '.env';
if (!file_exists($envPath)) {
$envContent = <<<ENV
# Konfigurationsdatei Beispiel
DB_DRIVER=sqlite
DB_SQLITE_PATH={$root}/data/shorturl.sqlite
ADMIN_USER=admin
ADMIN_PASS=admin123
BASE_URL=http://localhost
# SMTP Einstellungen optional
SMTP_HOST=
SMTP_USER=
SMTP_PASS=
SMTP_PORT=587
# weitere Einstellungen
DEFAULT_ALIAS_LENGTH=6
ALIAS_ALLOWED_CHARS=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
ENV;
writeFile($envPath, $envContent);
}
// 3 SQLite DB anlegen wenn nötig
$dbFile = $root . '/data/shorturl.sqlite';
if (!file_exists($dbFile)) {
$pdo = new PDO('sqlite:' . $dbFile);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec("PRAGMA journal_mode = WAL");
// urls Tabelle
$pdo->exec("
CREATE TABLE IF NOT EXISTS urls (
id INTEGER PRIMARY KEY AUTOINCREMENT,
alias TEXT UNIQUE NOT NULL,
target TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
clicks INTEGER DEFAULT 0,
expires_at DATETIME NULL,
note TEXT,
created_by TEXT,
is_enabled INTEGER DEFAULT 1
);
");
// clicks detail table
$pdo->exec("
CREATE TABLE IF NOT EXISTS clicks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url_id INTEGER NOT NULL,
ts DATETIME DEFAULT CURRENT_TIMESTAMP,
ip TEXT,
referrer TEXT,
user_agent TEXT
);
");
// users for admin
$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
);
");
// default admin user
$defaultUser = 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'=>$defaultUser, ':p'=>$hash]);
}
// 4 Datei inhalte vorbereiten
// public index
$publicIndex = <<<'PHP'
<?php
// public/index.php
require_once __DIR__ . '/../src/config.php';
$config = require __DIR__ . '/../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 - Short URL Service</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<div class="container">
<header class="card">
<h1>Short URL Service</h1>
<p class="small">Kurzlink erstellen, Statistik einsehen, QR Code generieren</p>
</header>
<main class="grid">
<section class="card">
<h3>Link erstellen</h3>
<div id="result" class="small"></div>
<input id="target" class="input" placeholder="Ziel URL, z. B. https://example.com">
<input id="custom" class="input" placeholder="Alias optional, z. B. mein-link">
<input id="expires" class="input" placeholder="Ablaufdatum optional, Format YYYY-MM-DD">
<textarea id="note" class="input" placeholder="Notiz optional"></textarea>
<div style="margin-top:8px">
<label id="captchaQuestion" class="small">Lade Captcha...</label>
<input id="captchaAnswer" class="input" placeholder="Antwort">
</div>
<div style="margin-top:8px;display:flex;justify-content:flex-end">
<button class="btn" onclick="shorten()">Shorten</button>
</div>
<div id="linkBox" style="margin-top:12px"></div>
</section>
<section class="card">
<h3>Link suchen</h3>
<input id="search" class="input" placeholder="Alias oder Ziel URL" onkeydown="if (event.key==='Enter') loadLinks(1)">
<div id="linksArea" style="margin-top:12px"></div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-top:8px">
<div id="linksInfo" class="small"></div>
</div>
</section>
</main>
<footer class="footer card">
<div>Made with ❤️ by <a href="https://www.dreamcodes.net" target="_blank" rel="noopener">Dreamcodes</a></div>
</footer>
</div>
<script src="assets/app.js"></script>
</body>
</html>
PHP;
// public r.php redirect handler
$publicR = <<<'PHP'
<?php
// public/r.php
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
$config = require __DIR__ . '/../src/config.php';
$pdo = getPdo();
$alias = $_GET['c'] ?? '';
if (!$alias) { http_response_code(404); echo "Nicht gefunden"; exit; }
$stmt = $pdo->prepare("SELECT * FROM urls WHERE alias = :a AND is_enabled = 1 LIMIT 1");
$stmt->execute([':a'=>$alias]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) { http_response_code(404); echo "Nicht gefunden"; exit; }
// check expiry
if ($row['expires_at'] && strtotime($row['expires_at']) < time()) {
http_response_code(410);
echo "Link abgelaufen";
exit;
}
// update click count and insert click record
$pdo->beginTransaction();
$u = $pdo->prepare("UPDATE urls SET clicks = clicks + 1 WHERE id = :id");
$u->execute([':id'=>$row['id']]);
$c = $pdo->prepare("INSERT INTO clicks (url_id, ip, referrer, user_agent) VALUES (:uid,:ip,:ref,:ua)");
$c->execute([':uid'=>$row['id'], ':ip'=>$_SERVER['REMOTE_ADDR'] ?? '', ':ref'=>$_SERVER['HTTP_REFERER'] ?? '', ':ua'=>$_SERVER['HTTP_USER_AGENT'] ?? '']);
$pdo->commit();
// redirect
$target = $row['target'];
// ensure scheme
if (!preg_match('#^https?://#i', $target)) $target = 'http://' . $target;
header('Location: ' . $target, true, 302);
exit;
PHP;
// public rss
$publicRss = <<<'PHP'
<?php
// public/rss.php
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
$items = $pdo->query("SELECT * FROM urls WHERE is_enabled=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>Short URL Feed</title>
<link><?php echo htmlspecialchars((require __DIR__ . '/../src/config.php')['base_url']); ?></link>
<description>Neueste Kurzlinks</description>
<?php foreach($items as $it): ?>
<item>
<title><?php echo htmlspecialchars($it['alias'] . ' → ' . substr($it['target'],0,60)); ?></title>
<pubDate><?php echo date(DATE_RSS, strtotime($it['created_at'])); ?></pubDate>
<description><![CDATA[<?php echo nl2br(htmlspecialchars($it['note'] ?? '')); ?>]]></description>
<guid><?php echo htmlspecialchars($it['id']); ?></guid>
</item>
<?php endforeach; ?>
</channel>
</rss>
PHP;
// assets css
$styleCss = <<<'CSS'
:root{--accent:#1f7a8c;--muted:#666;--bg:#f6f8f9;--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}
.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}
.entry{border-bottom:1px dashed #eee;padding:8px 0}
@media(max-width:900px){.grid{grid-template-columns:1fr}}
CSS;
// assets js
$appJs = <<<'JS'
const API = '/api';
let currentPage = 1;
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 ajaxGet(url, cb) {
fetch(url).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 shorten(){
const target = document.getElementById('target').value;
const custom = document.getElementById('custom').value;
const expires = document.getElementById('expires').value;
const note = document.getElementById('note').value;
const captcha = document.getElementById('captchaAnswer').value;
ajax(API + '/shorten.php', {action:'shorten', target: target, custom: custom, expires: expires, note: note, captcha: captcha}, function(resp){
const box = document.getElementById('result');
if (!resp.ok) { box.innerText = resp.error || 'Fehler'; return; }
const linkBox = document.getElementById('linkBox');
linkBox.innerHTML = '<div class="small">Kurzlink erstellt</div><div style="margin-top:6px"><a href="'+resp.short+'" target="_blank">'+resp.short+'</a></div>'
+'<div style="margin-top:6px"><img src="'+resp.qr+'" alt="QR"></div>';
});
}
function loadLinks(page=1){
currentPage = page;
const q = document.getElementById('search').value;
ajax(API + '/stats.php', {action:'list', page: page, q: q}, function(resp){
if (!resp.ok) return;
const area = document.getElementById('linksArea');
area.innerHTML = '';
if (resp.items.length === 0) { area.innerHTML = '<div class="small">Keine Links gefunden</div>'; document.getElementById('linksInfo').innerText = ''; return; }
resp.items.forEach(it=>{
const div = document.createElement('div'); div.className = 'entry';
div.innerHTML = '<div class="small"><strong>'+it.alias+'</strong> • '+it.created_at+'</div>'
+'<div style="margin-top:6px"><a href="'+it.short+'" target="_blank">'+it.short+'</a> → '+(it.target.length>80?it.target.substr(0,80)+'…':it.target)+'</div>'
+'<div style="margin-top:6px" class="small">Clicks: '+it.clicks+'</div>';
area.appendChild(div);
});
const start = (resp.page-1)*resp.perPage + 1;
const end = start + resp.items.length -1;
document.getElementById('linksInfo').innerText = 'Zeige ' + start + '–' + end + ' von ' + resp.total;
});
}
window.addEventListener('load', function(){
loadCaptcha();
loadLinks(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/shorturl.sqlite',
'admin_user' => $env['ADMIN_USER'] ?? 'admin',
'admin_pass' => $env['ADMIN_PASS'] ?? 'admin123',
'base_url' => rtrim($env['BASE_URL'] ?? '', '/'),
'upload_dir' => $root . '/data',
'qrcode_cache' => $root . '/data/qrcache',
'default_alias_length' => (int)($env['DEFAULT_ALIAS_LENGTH'] ?? 6),
'alias_chars' => $env['ALIAS_ALLOWED_CHARS'] ?? 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
];
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'] ?? null, $cfg['db_pass'] ?? null, [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
}
return $pdo;
}
PHP;
// src/helpers.php
$srcHelpers = <<<'PHP'
<?php
// src/helpers.php
function esc($s) {
return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function makeAlias($length = 6, $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789') {
$out = '';
$max = strlen($chars) - 1;
for ($i = 0; $i < $length; $i++) {
$out .= $chars[random_int(0, $max)];
}
return $out;
}
function baseUrl() {
$cfg = require __DIR__ . '/config.php';
return rtrim($cfg['base_url'], '/');
}
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;
// api/shorten.php
$apiShorten = <<<'PHP'
<?php
// api/shorten.php
session_start();
require_once __DIR__ . '/../src/config.php';
require_once __DIR__ . '/../src/db.php';
require_once __DIR__ . '/../src/helpers.php';
require_once __DIR__ . '/../src/captcha.php';
$config = require __DIR__ . '/../src/config.php';
$pdo = getPdo();
header('Content-Type: application/json; charset=utf-8');
$target = trim($_POST['target'] ?? '');
$custom = trim($_POST['custom'] ?? '');
$expires = trim($_POST['expires'] ?? '');
$note = trim($_POST['note'] ?? '');
$captcha = $_POST['captcha'] ?? '';
if (!$target) { echo json_encode(['ok'=>false,'error'=>'Ziel URL fehlt']); exit; }
if (!captchaCheck($captcha)) { echo json_encode(['ok'=>false,'error'=>'Captcha falsch']); exit; }
// basic rate limit by ip
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$stmt = $pdo->prepare("SELECT COUNT(*) FROM urls WHERE created_at > datetime('now','-1 hour') AND created_by = :ip");
$stmt->execute([':ip'=>$ip]);
if ($stmt->fetchColumn() > 30) { echo json_encode(['ok'=>false,'error'=>'Zu viele Links erstellt, bitte spaeter erneut']); exit; }
// sanitize target
$target = filter_var($target, FILTER_SANITIZE_URL);
if (!preg_match('#^https?://#i', $target)) {
$target = 'http://' . $target;
}
$alias = '';
$cfg = $config;
if ($custom !== '') {
// validate alias
if (!preg_match('/^[A-Za-z0-9_-]{3,100}$/', $custom)) {
echo json_encode(['ok'=>false,'error'=>'Alias ungültig, nur Buchstaben Zahlen Unterstrich Bindestrich erlaubt, mind 3 Zeichen']); exit;
}
// check unique
$s = $pdo->prepare("SELECT id FROM urls WHERE alias = :a LIMIT 1");
$s->execute([':a'=>$custom]);
if ($s->fetch()) { echo json_encode(['ok'=>false,'error'=>'Alias schon vergeben']); exit; }
$alias = $custom;
} else {
// generate alias loop until unique
$len = max(4, (int)$cfg['default_alias_length']);
$chars = $cfg['alias_chars'];
do {
$candidate = '';
$max = strlen($chars) - 1;
for ($i=0;$i<$len;$i++) $candidate .= $chars[random_int(0,$max)];
$s = $pdo->prepare("SELECT id FROM urls WHERE alias = :a LIMIT 1");
$s->execute([':a'=>$candidate]);
$exists = $s->fetch();
} while ($exists);
$alias = $candidate;
}
// parse expires
$expiresAt = null;
if ($expires !== '') {
$d = strtotime($expires);
if ($d === false) { echo json_encode(['ok'=>false,'error'=>'Ungültiges Ablaufdatum']); exit; }
$expiresAt = date('Y-m-d H:i:s', $d);
}
// insert
$stmt = $pdo->prepare("INSERT INTO urls (alias, target, expires_at, note, created_by) VALUES (:a, :t, :e, :n, :ip)");
$stmt->execute([':a'=>$alias, ':t'=>$target, ':e'=>$expiresAt, ':n'=>$note, ':ip'=>$ip]);
$id = $pdo->lastInsertId();
// build short url and qr
$base = rtrim($cfg['base_url'], '/');
if (!$base) {
$base = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
}
$short = $base . '/r.php?c=' . urlencode($alias);
// quick QR via google chart url
$qr = 'https://chart.googleapis.com/chart?cht=qr&chs=200x200&chl=' . urlencode($short);
echo json_encode(['ok'=>true,'short'=>$short,'alias'=>$alias,'qr'=>$qr]);
exit;
PHP;
// api/stats.php
$apiStats = <<<'PHP'
<?php
// api/stats.php
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
header('Content-Type: application/json; charset=utf-8');
$action = $_POST['action'] ?? 'list';
if ($action === 'list') {
$page = max(1, (int)($_POST['page'] ?? 1));
$perPage = 12;
$offset = ($page - 1) * $perPage;
$q = trim($_POST['q'] ?? '');
if ($q !== '') {
$stmt = $pdo->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM urls WHERE alias LIKE :q OR target LIKE :q ORDER BY created_at DESC LIMIT :l OFFSET :o");
$stmt->bindValue(':q', '%'.$q.'%', PDO::PARAM_STR);
} else {
$stmt = $pdo->prepare("SELECT SQL_CALC_FOUND_ROWS * FROM urls 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 urls")->fetchColumn();
// add short link
$cfg = require __DIR__ . '/../src/config.php';
$base = rtrim($cfg['base_url'], '/');
if (!$base) $base = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' ? 'https' : 'http') . '://' . ($_SERVER['HTTP_HOST'] ?? 'localhost');
foreach ($rows as &$r) {
$r['short'] = $base . '/r.php?c=' . urlencode($r['alias']);
}
echo json_encode(['ok'=>true,'items'=>$rows,'total'=>$total,'page'=>$page,'perPage'=>$perPage]);
exit;
}
// other actions could be added here
echo json_encode(['ok'=>false,'error'=>'Unknown action']);
exit;
PHP;
// api/redirect.php (not used, r.php does redirect, kept for API completeness)
$apiRedirect = <<<'PHP'
<?php
// api/redirect.php
// Optional API redirect endpoint
require_once __DIR__ . '/../src/db.php';
$pdo = getPdo();
$alias = $_GET['c'] ?? '';
if (!$alias) { http_response_code(404); echo "Not found"; exit; }
$stmt = $pdo->prepare("SELECT * FROM urls WHERE alias = :a LIMIT 1");
$stmt->execute([':a'=>$alias]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$row) { http_response_code(404); echo "Not found"; exit; }
if ($row['expires_at'] && strtotime($row['expires_at']) < time()) { http_response_code(410); echo "Gone"; exit; }
$pdo->prepare("UPDATE urls SET clicks = clicks + 1 WHERE id = :id")->execute([':id'=>$row['id']]);
$pdo->prepare("INSERT INTO clicks (url_id, ip, referrer, user_agent) VALUES (:uid,:ip,:ref,:ua)")->execute([':uid'=>$row['id'], ':ip'=>$_SERVER['REMOTE_ADDR'] ?? '', ':ref'=>$_SERVER['HTTP_REFERER'] ?? '', ':ua'=>$_SERVER['HTTP_USER_AGENT'] ?? '']);
$target = $row['target'];
if (!preg_match('#^https?://#i', $target)) $target = 'http://' . $target;
header('Location: ' . $target); exit;
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 urls ORDER BY created_at DESC");
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$exportFile = __DIR__ . '/../data/shorturl_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;
// api/captcha.php
$apiCaptcha = <<<'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;
// admin index login page
$adminIndex = <<<'HTML'
<?php
// admin/index.php
?>
<!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>
<input id="user" class="input" placeholder="Benutzer">
<input id="pass" class="input" placeholder="Passwort" type="password">
<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>Short URL Admin</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">
<h3>Links verwalten</h3>
<div style="margin-bottom:8px">
<button class="btn" onclick="loadLinks()">Laden</button>
<button class="btn ghost" onclick="exportJson()">Export JSON</button>
</div>
<div id="adminList" style="max-height:600px;overflow:auto"></div>
</section>
<aside class="card">
<h4>Info</h4>
<div class="small">Freigeben, deaktivieren, loeschen, klicks einsehen</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 loadLinks(){ ajax('/api/admin_export.php',{},function(res){ if(!res.ok){ alert(res.error||'Fehler'); return; } 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.is_enabled==1?'<strong style="color:green">Aktiv</strong>':'<strong style="color:orange">Inaktiv</strong>')+'</div>' + '<div style="margin-top:6px"><strong>'+e.alias+'</strong> → '+e.target+'</div>' + '<div style="margin-top:6px" class="small">Clicks: '+e.clicks+'</div>' + '<div style="margin-top:6px"><button class="btn" onclick="toggle('+e.id+','+(e.is_enabled==1?0:1)+')">'+(e.is_enabled==1?'Deaktivieren':'Aktivieren')+'</button> <button class="btn ghost" onclick="del('+e.id+')">Löschen</button> </div>'; list.appendChild(div); }); }); }); }
function toggle(id, state){ ajax('/api/admin_actions.php',{action:'toggle',id:id,state:state},function(r){ if(r.ok) loadLinks(); 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) loadLinks(); 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
Short URL Hoster
Dieses Projekt wurde automatisch erstellt. Passe die .env Datei an und ändere das Admin Passwort sofort.
Powered by www.dreamcodes.net
MD;
// Save files
$filesToWrite = [
'public/index.php' => $publicIndex,
'public/r.php' => $publicR,
'public/rss.php' => $publicRss,
'public/assets/style.css' => $styleCss,
'public/assets/app.js' => $appJs,
'src/config.php' => $srcConfig,
'src/db.php' => $srcDb,
'src/helpers.php' => $srcHelpers,
'src/captcha.php' => $srcCaptcha,
'api/shorten.php' => $apiShorten,
'api/stats.php' => $apiStats,
'api/redirect.php' => $apiRedirect,
'api/auth_login.php' => $apiAuthLogin,
'api/auth_logout.php' => $apiAuthLogout,
'api/admin_export.php' => $apiAdminExport,
'api/captcha.php' => $apiCaptcha,
'admin/index.php' => $adminIndex,
'admin/dashboard.php' => $adminDashboard,
'README.md' => $readme
];
foreach ($filesToWrite as $rel => $content) {
$full = $root . DIRECTORY_SEPARATOR . $rel;
writeFile($full, $content);
}
// admin actions endpoint for toggle and delete
$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 === 'toggle') {
$id = (int)($_POST['id'] ?? 0);
$state = (int)($_POST['state'] ?? 0);
$stmt = $pdo->prepare("UPDATE urls SET is_enabled = :s WHERE id = :id");
$stmt->execute([':s'=>$state,':id'=>$id]);
echo json_encode(['ok'=>true]); exit;
}
if ($action === 'delete') {
$id = (int)($_POST['id'] ?? 0);
$stmt = $pdo->prepare("DELETE FROM urls WHERE id = :id");
$stmt->execute([':id'=>$id]);
echo json_encode(['ok'=>true]); exit;
}
echo json_encode(['ok'=>false,'error'=>'Unbekannte Aktion']);
PHP;
writeFile($root . '/api/admin_actions.php', $adminActions);
// success output
echo "<h2>Installation abgeschlossen</h2>";
echo "<p>Die Struktur wurde erstellt. Bitte ändere sofort das Admin Passwort in der .env Datei oder in der DB.</p>";
echo "<ul>";
foreach ($dirs as $d) echo "<li>" . htmlspecialchars($d) . "</li>";
echo "</ul>";
echo "<p>Erstellte dateien</p><ul>";
foreach (array_keys($filesToWrite) as $f) echo "<li>" . htmlspecialchars($f) . "</li>";
echo "<li>api/admin_actions.php</li>";
echo "</ul>";
echo "<p>Die SQLite Datei liegt in data/shorturl.sqlite</p>";
echo "<p>Links: <a href=\"/public/index.php\">Frontend</a> | <a href=\"/admin/index.php\">Admin</a></p>";
echo "<p>Wichtig: Lösche diese Setup Datei nach der Installation</p>";
?>