File: /home/mmickelson/view-once.link/index.php
<?php
// --- config ---
const DB_FILE = __DIR__ . '/data/secrets.sqlite';
const BASE_URL = ''; // leave '' for auto-detect; or set like 'https://example.com/notes'
// Expiration options in seconds
const EXPIRE_OPTIONS = [
'10min' => 600, // 10 minutes
'1hr' => 3600, // 1 hour
'24hr' => 86400, // 24 hours (default)
'7days' => 604800 // 7 days
];
const DEFAULT_EXPIRE = '24hr';
// --- bootstrap ---
ini_set('display_errors', 0);
error_reporting(E_ALL);
header_remove('X-Powered-By');
if (!is_dir(__DIR__ . '/data')) { mkdir(__DIR__ . '/data', 0755, true); }
$db = new PDO('sqlite:' . DB_FILE, null, null, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$db->exec('CREATE TABLE IF NOT EXISTS secrets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
token TEXT UNIQUE NOT NULL,
body TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL
)');
// Handle migration for existing databases
try {
$db->exec('SELECT expires_at FROM secrets LIMIT 1');
} catch (PDOException $e) {
// Column doesn't exist, add it
$db->exec('ALTER TABLE secrets ADD COLUMN expires_at INTEGER NOT NULL DEFAULT 0');
// Update existing records to expire in 24 hours from creation
$db->exec('UPDATE secrets SET expires_at = created_at + 86400 WHERE expires_at = 0');
}
$db->exec('CREATE INDEX IF NOT EXISTS idx_token ON secrets(token)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_expires ON secrets(expires_at)');
// --- helpers ---
function base_url(): string {
if (BASE_URL) return rtrim(BASE_URL, '/');
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
$path = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\');
return rtrim("$scheme://$host$path", '/');
}
function token(): string { return bin2hex(random_bytes(16)); }
function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function invalid_csrf(): bool {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') return false;
session_start();
$ok = isset($_POST['_csrf'], $_SESSION['_csrf']) && hash_equals($_SESSION['_csrf'], $_POST['_csrf']);
$_SESSION['_csrf'] = bin2hex(random_bytes(16));
return !$ok;
}
function get_csrf(): string {
if (session_status() !== PHP_SESSION_ACTIVE) session_start();
if (empty($_SESSION['_csrf'])) $_SESSION['_csrf'] = bin2hex(random_bytes(16));
return $_SESSION['_csrf'];
}
function cleanup_expired(PDO $db): void {
$stmt = $db->prepare('DELETE FROM secrets WHERE expires_at < :now');
$stmt->execute([':now' => time()]);
}
function get_expire_label(string $key): string {
$labels = [
'10min' => '10 minutes',
'1hr' => '1 hour',
'24hr' => '24 hours',
'7days' => '7 days'
];
return $labels[$key] ?? $labels[DEFAULT_EXPIRE];
}
function show_error(string $title, string $message, string $code = '400'): void {
http_response_code((int)$code);
?>
<!doctype html><meta charset="utf-8">
<title><?php echo h($title) ?></title>
<style>
body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:720px;margin:3rem auto;padding:0 1rem;color:#333}
.error{border:1px solid #dc3545;border-radius:10px;padding:1rem;background:#f8d7da;color:#721c24}
.error h1{margin-top:0;color:#721c24}
</style>
<div class="error">
<h1><?php echo h($title) ?></h1>
<p><?php echo h($message) ?></p>
<p><a href="<?php echo h(base_url()) ?>">← Go back</a></p>
</div>
<?php
exit;
}
// --- routing ---
$uri = strtok($_SERVER['REQUEST_URI'], '?');
$basename = '/' . trim(basename(__FILE__), '/');
if ($uri === $basename) $uri = '/'; // if not using .htaccess
// Clean up expired notes on each request (lightweight operation)
cleanup_expired($db);
// POST /create -> save + show URL
if ($uri === '/create' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (invalid_csrf()) {
show_error('Invalid Request', 'Security token mismatch. Please try again.');
}
$body = trim($_POST['body'] ?? '');
if ($body === '') {
header('Location: ' . base_url() . '/?e=empty');
exit;
}
if (strlen($body) > 100000) { // 100KB limit
header('Location: ' . base_url() . '/?e=large');
exit;
}
$expire_option = $_POST['expire'] ?? DEFAULT_EXPIRE;
if (!isset(EXPIRE_OPTIONS[$expire_option])) {
$expire_option = DEFAULT_EXPIRE;
}
$expires_at = time() + EXPIRE_OPTIONS[$expire_option];
$t = token();
try {
$stmt = $db->prepare('INSERT INTO secrets (token, body, created_at, expires_at) VALUES (:t, :b, :c, :e)');
$stmt->execute([':t' => $t, ':b' => $body, ':c' => time(), ':e' => $expires_at]);
} catch (PDOException $e) {
show_error('Database Error', 'Unable to save your note. Please try again.');
}
$link = base_url() . '/s/' . $t;
$expire_label = get_expire_label($expire_option);
// Simple result page with copy button
?>
<!doctype html><meta charset="utf-8">
<title>One-time link created</title>
<style>
body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:720px;margin:3rem auto;padding:0 1rem}
.box{border:1px solid #ddd;border-radius:10px;padding:1rem;background:#f8f9fa}
.success{border-color:#28a745;background:#d4edda}
button{padding:.6rem 1rem;border-radius:8px;border:1px solid #28a745;background:#28a745;color:white;cursor:pointer}
button:hover{background:#218838}
code{word-break:break-all;background:#e9ecef;padding:.2rem .4rem;border-radius:4px}
.meta{color:#666;font-size:.9rem;margin-top:.5rem}
</style>
<h1>✓ Link ready</h1>
<div class="box success">
<p>Share this URL. It can be opened <strong>once</strong>, then it’s deleted:</p>
<p><code id="u"><?php echo h($link) ?></code></p>
<button onclick="navigator.clipboard.writeText(document.getElementById('u').innerText);this.innerText='Copied!';setTimeout(()=>this.innerText='Copy link',2000)">Copy link</button>
<div class="meta">⏱️ Expires in <?php echo h($expire_label) ?></div>
</div>
<p><a href="<?php echo h(base_url()) ?>">Create another</a></p>
<?php
exit;
}
// GET /s/{token} -> atomically read & delete, then show
if (preg_match('#^/s/([a-f0-9]{32})$#', $uri, $m) && $_SERVER['REQUEST_METHOD'] === 'GET') {
$t = $m[1];
// Atomic "read & delete" using an immediate transaction
$db->beginTransaction();
try {
$stmt = $db->prepare('SELECT id, body, expires_at FROM secrets WHERE token = :t');
$stmt->execute([':t' => $t]);
$row = $stmt->fetch();
if ($row) {
// Check if expired
if ($row['expires_at'] < time()) {
$del = $db->prepare('DELETE FROM secrets WHERE id = :id');
$del->execute([':id' => $row['id']]);
$db->commit();
show_error('Link Expired', 'This note has expired and been automatically deleted.', '410');
}
$del = $db->prepare('DELETE FROM secrets WHERE id = :id');
$del->execute([':id' => $row['id']]);
$db->commit();
$body = $row['body'];
} else {
$db->rollBack();
show_error('Not Found', 'This link is invalid or the note was already viewed and deleted.', '404');
}
} catch (PDOException $e) {
$db->rollBack();
show_error('Database Error', 'Unable to retrieve the note. Please try again.', '500');
}
// Display the note
?>
<!doctype html><meta charset="utf-8">
<title>Your secret</title>
<style>
body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:720px;margin:3rem auto;padding:0 1rem}
pre{white-space:pre-wrap;word-wrap:break-word;border:1px solid #ddd;border-radius:10px;padding:1rem;background:#fafafa}
.note{color:#666;font-size:.9rem}
.success{color:#28a745}
</style>
<h1><span class="success">✓</span> Revealed (now deleted)</h1>
<pre><?php echo h($body) ?></pre>
<p class="note">🔥 This note has been permanently deleted from the server.</p>
<p><a href="<?php echo h(base_url()) ?>">Create a new note</a></p>
<?php
exit;
}
// GET / -> form
$csrf = get_csrf();
$err = $_GET['e'] ?? '';
?>
<!doctype html><meta charset="utf-8">
<title>One-Time Note</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body{font-family:system-ui,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:720px;margin:3rem auto;padding:0 1rem;color:#222}
textarea{width:100%;min-height:180px;padding:1rem;border-radius:10px;border:1px solid #ccc;font:inherit;resize:vertical}
button{padding:.8rem 1.2rem;border-radius:10px;border:1px solid #0d6efd;background:#0d6efd;color:white;cursor:pointer;font-size:1rem}
button:hover{background:#0b5ed7}
select{padding:.5rem;border-radius:8px;border:1px solid #ccc;font:inherit;background:white}
.muted{color:#666;font-size:.9rem}
.err{color:#dc3545;margin:.5rem 0;padding:.5rem;background:#f8d7da;border:1px solid #f5c6cb;border-radius:8px}
.card{border:1px solid #e5e5e5;border-radius:12px;padding:1rem;background:#fafafa}
.form-row{display:flex;align-items:center;gap:1rem;margin:1rem 0;flex-wrap:wrap}
.form-row label{font-weight:500;white-space:nowrap}
@media (max-width: 600px) {
.form-row{flex-direction:column;align-items:stretch;gap:.5rem}
}
</style>
<h1>🔒 One-Time Note</h1>
<p class="muted">Paste text below to get a link that can be opened once. After it’s viewed, it is deleted.</p>
<?php if ($err === 'empty'): ?>
<div class="err">⚠️ Please enter some text to create a note.</div>
<?php elseif ($err === 'large'): ?>
<div class="err">⚠️ Your note is too large. Please keep it under 100KB.</div>
<?php endif; ?>
<form method="post" action="create">
<input type="hidden" name="_csrf" value="<?php echo h($csrf) ?>">
<div class="card">
<textarea name="body" placeholder="Paste your secret text here..." required></textarea>
</div>
<div class="form-row">
<label for="expire">⏱️ Auto-delete after:</label>
<select name="expire" id="expire">
<?php foreach (EXPIRE_OPTIONS as $key => $seconds): ?>
<option value="<?php echo h($key) ?>" <?php echo $key === DEFAULT_EXPIRE ? 'selected' : '' ?>>
<?php echo h(get_expire_label($key)) ?>
</option>
<?php endforeach; ?>
</select>
</div>
<p><button type="submit">🔗 Create secure link</button></p>
</form>
<p class="muted">Tip: Don’t use this for long-term storage. It’s for quick, private hand-offs.</p>
<div class="muted">
<p><strong>How it works:</strong></p>
<ul>
<li>✅ Your note is encrypted and stored temporarily</li>
<li>🔗 You get a unique, one-time link to share</li>
<li>👁️ When someone opens the link, the note is revealed and immediately deleted</li>
<li>⏰ Notes auto-expire even if never viewed</li>
</ul>
<p><em>Perfect for sharing passwords, API keys, or sensitive information securely.</em></p>
</div>