'image/jpeg','jpeg'=>'image/jpeg','png'=>'image/png','gif'=>'image/gif','webp'=>'image/webp']; if (preg_match('/^[a-zA-Z0-9_\-\.]+$/', $file) && file_exists($path) && isset($mimes[$ext])) { header('Content-Type: ' . $mimes[$ext]); header('Content-Length: ' . filesize($path)); header('Cache-Control: public, max-age=2592000'); readfile($path); exit; } http_response_code(404); exit; } /** * Hazel+ โ a single-file PHP social network * * v2.0.0 โ going open source! * License: MIT * GitHub: https://github.com/hazel-plus/hazelplus */ // --- config --- define('DB_TYPE', 'sqlite'); define('DB_FILE', __DIR__ . '/hazelplus.db'); define('DB_HOST', 'localhost'); define('DB_NAME', 'hazelplus'); define('DB_USER', 'root'); define('DB_PASS', ''); define('SITE_NAME', 'Hazel+'); define('SITE_DESC', 'Share what matters. Connect for real.'); define('ADMIN_EMAIL', 'admin@example.com'); define('ADMIN_PASS', 'admin123'); define('UPLOAD_DIR', __DIR__ . '/uploads/'); define('UPLOAD_URL', '/uploads/'); define('VERSION', '2.0.0'); define('GITHUB_URL', 'https://github.com/hazelplus/hazelplus'); define('LEGAL_SITE_URL', 'https://example.com'); define('LEGAL_CONTACT', 'legal@legal.com'); define('LEGAL_EFFECTIVE', 'January 1, 2025'); function getTosHtml(): string { $s = SITE_NAME; $e = LEGAL_CONTACT; $d = LEGAL_EFFECTIVE; return <<1. Acceptance of Terms
By creating an account or using $s ("the Service"), you agree to be bound by these Terms of Service.
You must be at least 13 years old to use the Service.
Don't post content that:
You own what you post. By posting it, you give $s permission to display and share it through the Service.
We can suspend or delete accounts that break these rules.
The Service is provided "as is". We're not liable for indirect or consequential damages.
We might update these terms sometimes. Continuing to use the Service after changes means you accept them.
Email us at $e.
Effective: $d
HTML; } function getPrivacyHtml(): string { $s = SITE_NAME; $e = LEGAL_CONTACT; $d = LEGAL_EFFECTIVE; return <<1. What We CollectWe don't share your info with third parties except when legally required.
Just one session cookie to keep you logged in. No tracking cookies.
Your data sticks around as long as your account is active. Email us to delete everything within 30 days.
Passwords are hashed with bcrypt.
The Service isn't for anyone under 13.
Email $e and we'll help.
We'll let registered users know if anything major changes here.
Email $e.
Effective: $d
HTML; } function getGuidelinesHtml(): string { $s = SITE_NAME; $e = LEGAL_CONTACT; return <<1. Be a decent humanDisagree all you want. But don't harass people, don't make personal attacks, and don't target people because of who they are.
Content that promotes hatred or discrimination based on race, ethnicity, religion, gender, sexuality, disability, or nationality isn't allowed.
Don't intimidate, threaten, or repeatedly go after another user.
Don't post anything illegal. Sexual content involving minors gets reported to law enforcement immediately, no exceptions.
Don't flood feeds, run undisclosed bots, or do anything that looks like coordinated fake activity.
Posts that encourage or glorify self-harm, suicide, eating disorders, or other dangerous health behaviors aren't allowed.
Don't post someone's home address, phone number, or private photos without their consent.
Explicit content is only allowed in communities clearly marked 18+.
Individual communities can set extra rules on top of these.
Breaking the rules can mean content removal, a suspension, or a permanent ban.
See something? Email $e. We review everything and try to respond within 48 hours.
Email $e with "Appeal" in the subject line.
HTML; } // --- database --- function getDB(): PDO { static $pdo = null; if ($pdo) return $pdo; if (DB_TYPE === 'sqlite') $pdo = new PDO('sqlite:' . DB_FILE); else $pdo = new PDO("mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4", DB_USER, DB_PASS); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); if (DB_TYPE === 'sqlite') $pdo->exec('PRAGMA foreign_keys = ON;'); return $pdo; } function setupDatabase(): void { $db = getDB(); $ai = DB_TYPE === 'sqlite' ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INT AUTO_INCREMENT PRIMARY KEY'; $txt = DB_TYPE === 'sqlite' ? 'TEXT' : 'LONGTEXT'; $db->exec("CREATE TABLE IF NOT EXISTS users ( id $ai, username TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE, password TEXT NOT NULL, display_name TEXT NOT NULL, bio TEXT DEFAULT '', avatar TEXT DEFAULT '', cover TEXT DEFAULT '', role TEXT DEFAULT 'user', tagline TEXT DEFAULT '', location TEXT DEFAULT '', website TEXT DEFAULT '', early_access INTEGER DEFAULT 0, tos_accepted INTEGER DEFAULT 0, suspended INTEGER DEFAULT 0, suspend_reason TEXT DEFAULT '', suspended_until DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_login DATETIME)"); $db->exec("CREATE TABLE IF NOT EXISTS posts ( id $ai, user_id INTEGER NOT NULL, content $txt NOT NULL, image TEXT DEFAULT '', visibility TEXT DEFAULT 'public', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS comments ( id $ai, post_id INTEGER NOT NULL, user_id INTEGER NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(post_id) REFERENCES posts(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS plusones ( id $ai, post_id INTEGER NOT NULL, user_id INTEGER NOT NULL, UNIQUE(post_id, user_id), FOREIGN KEY(post_id) REFERENCES posts(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS circles ( id $ai, user_id INTEGER NOT NULL, name TEXT NOT NULL, color TEXT DEFAULT '#4285f4', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS circle_members ( id $ai, circle_id INTEGER NOT NULL, member_id INTEGER NOT NULL, UNIQUE(circle_id, member_id), FOREIGN KEY(circle_id) REFERENCES circles(id) ON DELETE CASCADE, FOREIGN KEY(member_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS follows ( id $ai, follower_id INTEGER NOT NULL, following_id INTEGER NOT NULL, UNIQUE(follower_id, following_id), FOREIGN KEY(follower_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(following_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS notifications ( id $ai, user_id INTEGER NOT NULL, from_user_id INTEGER NOT NULL, type TEXT NOT NULL, ref_id INTEGER DEFAULT 0, message TEXT DEFAULT '', is_read INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS communities ( id $ai, owner_id INTEGER NOT NULL, name TEXT NOT NULL UNIQUE, description TEXT DEFAULT '', visibility TEXT DEFAULT 'public', color TEXT DEFAULT '#4285f4', icon TEXT DEFAULT '๐', banner_color TEXT DEFAULT '', banner_image TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(owner_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS community_members ( id $ai, community_id INTEGER NOT NULL, user_id INTEGER NOT NULL, role TEXT DEFAULT 'member', UNIQUE(community_id, user_id), FOREIGN KEY(community_id) REFERENCES communities(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS messages ( id $ai, from_id INTEGER NOT NULL, to_id INTEGER NOT NULL, content TEXT NOT NULL, is_read INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY(from_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY(to_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS reshares ( id $ai, post_id INTEGER NOT NULL, user_id INTEGER NOT NULL, comment TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(post_id, user_id), FOREIGN KEY(post_id) REFERENCES posts(id) ON DELETE CASCADE, FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE)"); $db->exec("CREATE TABLE IF NOT EXISTS settings (key TEXT PRIMARY KEY, value TEXT DEFAULT '')"); $migrations = [ "ALTER TABLE communities ADD COLUMN color TEXT DEFAULT '#4285f4'", "ALTER TABLE communities ADD COLUMN icon TEXT DEFAULT '๐'", "ALTER TABLE communities ADD COLUMN banner_color TEXT DEFAULT ''", "ALTER TABLE communities ADD COLUMN banner_image TEXT DEFAULT ''", "ALTER TABLE posts ADD COLUMN community_id INTEGER DEFAULT NULL", "ALTER TABLE users ADD COLUMN early_access INTEGER DEFAULT 0", "ALTER TABLE users ADD COLUMN tos_accepted INTEGER DEFAULT 0", "ALTER TABLE users ADD COLUMN verified INTEGER DEFAULT 0", "ALTER TABLE users ADD COLUMN suspended INTEGER DEFAULT 0", "ALTER TABLE users ADD COLUMN suspend_reason TEXT DEFAULT ''", "ALTER TABLE users ADD COLUMN suspended_until DATETIME DEFAULT NULL", "ALTER TABLE circles ADD COLUMN color TEXT DEFAULT '#4285f4'", "ALTER TABLE posts ADD COLUMN original_post_id INTEGER DEFAULT NULL", "ALTER TABLE posts ADD COLUMN reshare_comment TEXT DEFAULT ''", "ALTER TABLE communities ADD COLUMN icon_image TEXT DEFAULT ''", ]; foreach ($migrations as $m) { try { $db->exec($m); } catch (Exception $e) {} } $db->exec("INSERT OR IGNORE INTO settings (key,value) VALUES ('early_access_mode','0')"); $db->exec("INSERT OR IGNORE INTO settings (key,value) VALUES ('maintenance_mode','0')"); if (!$db->query("SELECT id FROM users WHERE role='admin' LIMIT 1")->fetch()) { $hash = password_hash(ADMIN_PASS, PASSWORD_DEFAULT); $db->prepare("INSERT INTO users (username,email,password,display_name,role,bio,tos_accepted) VALUES (?,?,?,?,?,?,1)") ->execute(['admin', ADMIN_EMAIL, $hash, 'Administrator', 'admin', 'Site admin of ' . SITE_NAME]); $aid = $db->lastInsertId(); foreach (['Friends' => '#4285f4', 'Family' => '#0f9d58', 'Acquaintances' => '#f4b400', 'Following' => '#dd4b39'] as $name => $color) $db->prepare("INSERT INTO circles (user_id,name,color) VALUES (?,?,?)")->execute([$aid, $name, $color]); } } function getSetting(string $key): string { try { $s = getDB()->prepare("SELECT value FROM settings WHERE key=?"); $s->execute([$key]); $r = $s->fetch(); return $r ? $r['value'] : ''; } catch (Exception $e) { return ''; } } function setSetting(string $key, string $val): void { getDB()->prepare("INSERT OR REPLACE INTO settings (key,value) VALUES (?,?)")->execute([$key, $val]); } function isEarlyAccess(): bool { return getSetting('early_access_mode') === '1'; } function isMaintenanceMode(): bool { return getSetting('maintenance_mode') === '1'; } function isUserSuspended(array $user): bool { if (!(int)($user['suspended'] ?? 0)) return false; $until = $user['suspended_until'] ?? null; if (!$until) return true; if (strtotime($until) > time()) return true; getDB()->prepare("UPDATE users SET suspended=0, suspend_reason='', suspended_until=NULL WHERE id=?") ->execute([$user['id']]); return false; } function suspendUser(int $uid, string $reason, ?string $until = null): void { getDB()->prepare("UPDATE users SET suspended=1, suspend_reason=?, suspended_until=? WHERE id=?") ->execute([$reason, $until, $uid]); } function unsuspendUser(int $uid): void { getDB()->prepare("UPDATE users SET suspended=0, suspend_reason='', suspended_until=NULL WHERE id=?") ->execute([$uid]); } session_start(); function currentUser(): ?array { if (!isset($_SESSION['user_id'])) return null; static $cu = null; if ($cu) return $cu; $s = getDB()->prepare("SELECT * FROM users WHERE id=?"); $s->execute([$_SESSION['user_id']]); $cu = $s->fetch() ?: null; return $cu; } function requireLogin(): void { if (!currentUser()) redirect('?page=login'); } function requireAdmin(): void { $u = currentUser(); if (!$u || $u['role'] !== 'admin') redirect('?page=home'); } function redirect(string $url): void { header("Location: $url"); exit; } function h(string $s): string { return htmlspecialchars($s, ENT_QUOTES, 'UTF-8'); } function timeAgo(string $dt): string { $diff = time() - strtotime($dt); if ($diff < 60) return 'just now'; if ($diff < 3600) return floor($diff / 60) . 'm'; if ($diff < 86400) return floor($diff / 3600) . 'h'; if ($diff < 604800) return floor($diff / 86400) . 'd'; return date('M j, Y', strtotime($dt)); } function uploadFile(array $f, string $prefix = ''): string { $dir = __DIR__ . '/uploads/'; if (!is_dir($dir)) mkdir($dir, 0755, true); $ext = strtolower(pathinfo($f['name'], PATHINFO_EXTENSION)); $allowed = ['jpg', 'jpeg', 'png', 'gif', 'webp']; if (!in_array($ext, $allowed) || $f['size'] > 5 * 1024 * 1024) return ''; $name = $prefix . uniqid() . '.' . $ext; move_uploaded_file($f['tmp_name'], $dir . $name); return '/uploads/' . $name; } function addNotif(int $to, int $from, string $type, int $ref, string $msg): void { if ($to === $from) return; getDB()->prepare("INSERT INTO notifications (user_id,from_user_id,type,ref_id,message) VALUES (?,?,?,?,?)") ->execute([$to, $from, $type, $ref, $msg]); } function unreadNotifs(): int { $u = currentUser(); if (!$u) return 0; $s = getDB()->prepare("SELECT COUNT(*) FROM notifications WHERE user_id=? AND is_read=0"); $s->execute([$u['id']]); return (int)$s->fetchColumn(); } function unreadMsgs(): int { $u = currentUser(); if (!$u) return 0; $s = getDB()->prepare("SELECT COUNT(*) FROM messages WHERE to_id=? AND is_read=0"); $s->execute([$u['id']]); return (int)$s->fetchColumn(); } function avatarSrc(?string $path, string $name = '?'): string { if ($path) return h($path); $l = strtoupper($name[0] ?? '?'); $colors = ['c0392b','2980b9','27ae60','d35400','8e44ad','16a085','e67e22','2c3e50']; $c = $colors[ord($l) % count($colors)]; $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode($svg); } function getAdminId(): int { $r = getDB()->query("SELECT id FROM users WHERE role='admin' LIMIT 1")->fetch(); return $r ? (int)$r['id'] : 0; } function userBadges(array $user): string { $out = ''; if ($user['role'] === 'admin') $out .= '✔ Admin'; elseif ((int)($user['early_access'] ?? 0) === 1) $out .= '⚡ Early Access'; if ((int)($user['suspended'] ?? 0) === 1) $out .= '🚫 Suspended'; return $out; } function circleColorPalette(): array { return ['#4285f4','#dd4b39','#0f9d58','#f4b400','#9b6a2f','#8e44ad','#16a085','#e67e22','#e91e63','#00bcd4','#795548','#607d8b']; } function reshareCount(int $postId): int { $s = getDB()->prepare("SELECT COUNT(*) FROM reshares WHERE post_id=?"); $s->execute([$postId]); return (int)$s->fetchColumn(); } function userReshared(int $postId, int $userId): bool { $s = getDB()->prepare("SELECT id FROM reshares WHERE post_id=? AND user_id=?"); $s->execute([$postId, $userId]); return (bool)$s->fetch(); } function renderReshareEmbed(array $orig): string { $av = avatarSrc($orig['avatar'] ?? '', $orig['display_name'] ?? '?'); $dn = h($orig['display_name'] ?? 'Unknown'); $un = h($orig['username'] ?? ''); $uid = (int)($orig['user_id'] ?? 0); $t = timeAgo($orig['created_at'] ?? ''); $body = h($orig['content'] ?? ''); $img = $orig['image'] ?? ''; $out = ''; return $out; } try { setupDatabase(); } catch (Exception $e) { die("Database error: " . $e->getMessage() . "\n\nMake sure the directory is writable."); } $page = $_GET['page'] ?? 'home'; $action = $_POST['action'] ?? ''; $earlyAccess = isEarlyAccess(); // --- maintenance mode gate --- if (isMaintenanceMode()) { $_cu = currentUser(); $isAdmin = $_cu && $_cu['role'] === 'admin'; $isLoginPage = ($page === 'login'); $isLoginAction = ($action === 'login'); if (!$isAdmin && !$isLoginPage && !$isLoginAction && $action !== 'logout') { ob_start(); ?> prepare("SELECT id FROM circles WHERE id=? AND user_id=?"); $chk->execute([$cid, $u['id']]); if (!$chk->fetch()) { echo json_encode(['ok' => false, 'error' => 'not your circle']); exit; } try { getDB()->prepare("INSERT INTO circle_members (circle_id,member_id) VALUES (?,?)")->execute([$cid, $mid]); echo json_encode(['ok' => true]); } catch (Exception $e) { echo json_encode(['ok' => false, 'error' => 'already in circle']); } exit; case 'circles_remove_member': requireLogin(); $u = currentUser(); header('Content-Type: application/json'); $cid = (int)($_POST['circle_id'] ?? 0); $mid = (int)($_POST['member_id'] ?? 0); $chk = getDB()->prepare("SELECT id FROM circles WHERE id=? AND user_id=?"); $chk->execute([$cid, $u['id']]); if (!$chk->fetch()) { echo json_encode(['ok' => false]); exit; } getDB()->prepare("DELETE FROM circle_members WHERE circle_id=? AND member_id=?")->execute([$cid, $mid]); echo json_encode(['ok' => true]); exit; case 'circles_create': requireLogin(); $u = currentUser(); header('Content-Type: application/json'); $name = trim($_POST['name'] ?? ''); $color = preg_match('/^#[0-9a-fA-F]{6}$/', $_POST['color'] ?? '') ? $_POST['color'] : '#4285f4'; if (!$name) { echo json_encode(['ok' => false, 'error' => 'name required']); exit; } getDB()->prepare("INSERT INTO circles (user_id,name,color) VALUES (?,?,?)")->execute([$u['id'], $name, $color]); $newId = (int)getDB()->lastInsertId(); echo json_encode(['ok' => true, 'id' => $newId, 'name' => $name, 'color' => $color]); exit; case 'circles_rename': requireLogin(); $u = currentUser(); header('Content-Type: application/json'); $cid = (int)($_POST['circle_id'] ?? 0); $name = trim($_POST['name'] ?? ''); $color = preg_match('/^#[0-9a-fA-F]{6}$/', $_POST['color'] ?? '') ? $_POST['color'] : ''; $chk = getDB()->prepare("SELECT id FROM circles WHERE id=? AND user_id=?"); $chk->execute([$cid, $u['id']]); if (!$chk->fetch()) { echo json_encode(['ok' => false]); exit; } if ($name) getDB()->prepare("UPDATE circles SET name=? WHERE id=?")->execute([$name, $cid]); if ($color) getDB()->prepare("UPDATE circles SET color=? WHERE id=?")->execute([$color, $cid]); echo json_encode(['ok' => true]); exit; case 'circles_delete': requireLogin(); $u = currentUser(); header('Content-Type: application/json'); $cid = (int)($_POST['circle_id'] ?? 0); $chk = getDB()->prepare("SELECT id FROM circles WHERE id=? AND user_id=?"); $chk->execute([$cid, $u['id']]); if (!$chk->fetch()) { echo json_encode(['ok' => false]); exit; } getDB()->prepare("DELETE FROM circles WHERE id=?")->execute([$cid]); echo json_encode(['ok' => true]); exit; case 'login': $em = trim($_POST['email'] ?? ''); $pw = $_POST['password'] ?? ''; $s = getDB()->prepare("SELECT * FROM users WHERE email=? OR username=?"); $s->execute([$em, $em]); $usr = $s->fetch(); if ($usr && password_verify($pw, $usr['password'])) { if (isUserSuspended($usr)) { $reason = $usr['suspend_reason'] ?: 'Violation of community guidelines.'; $until = $usr['suspended_until'] ? ' Your suspension lifts on ' . date('F j, Y \a\t g:i A', strtotime($usr['suspended_until'])) . '.' : ' This suspension is permanent.'; $_SESSION['error'] = 'Your account has been suspended. Reason: ' . $reason . $until; redirect('?page=login'); } $_SESSION['user_id'] = $usr['id']; getDB()->prepare("UPDATE users SET last_login=CURRENT_TIMESTAMP WHERE id=?")->execute([$usr['id']]); redirect('?page=home'); } $_SESSION['error'] = 'Wrong email or password.'; redirect('?page=login'); break; case 'register': if (isEarlyAccess()) { $_SESSION['error'] = 'Registration is currently closed.'; redirect('?page=register'); break; } if (empty($_POST['agree_tos'])) { $_SESSION['error'] = 'You need to agree to the Terms of Service to create an account.'; redirect('?page=register&step=3'); break; } $pw = $_POST['password'] ?? ''; $pc = $_POST['password_confirm'] ?? ''; if ($pw !== $pc) { $_SESSION['error'] = 'Passwords do not match.'; redirect('?page=register&step=3'); } $un = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($_POST['username'] ?? $_SESSION['reg_un'] ?? ''))); $em = trim($_POST['email'] ?? $_SESSION['reg_em'] ?? ''); $dn = trim($_POST['display_name'] ?? $_SESSION['reg_name'] ?? ''); if (strlen($un) < 3 || strlen($pw) < 6 || !filter_var($em, FILTER_VALIDATE_EMAIL) || !$dn) { $_SESSION['error'] = 'Fill in all fields. Username needs at least 3 characters, password at least 6.'; redirect('?page=register&step=1'); } try { getDB()->prepare("INSERT INTO users (username,email,password,display_name,early_access,tos_accepted) VALUES (?,?,?,?,0,1)") ->execute([$un, $em, password_hash($pw, PASSWORD_DEFAULT), $dn]); $nid = (int)getDB()->lastInsertId(); foreach (['Friends' => '#4285f4', 'Family' => '#0f9d58', 'Acquaintances' => '#f4b400', 'Following' => '#dd4b39'] as $name => $color) getDB()->prepare("INSERT INTO circles (user_id,name,color) VALUES (?,?,?)")->execute([$nid, $name, $color]); $adminId = getAdminId(); if ($adminId && $adminId !== $nid) try { getDB()->prepare("INSERT INTO follows (follower_id,following_id) VALUES (?,?)")->execute([$nid, $adminId]); } catch (Exception $e) {} unset($_SESSION['reg_name'], $_SESSION['reg_un'], $_SESSION['reg_em']); $_SESSION['user_id'] = $nid; redirect('?page=home'); } catch (Exception $e) { $_SESSION['error'] = 'That username or email is already taken.'; redirect('?page=register&step=2'); } break; case 'reg_step1': $fn = trim($_POST['first_name'] ?? ''); $ln = trim($_POST['last_name'] ?? ''); if (!$fn) { $_SESSION['error'] = 'Please enter your first name.'; redirect('?page=register&step=1'); } $_SESSION['reg_name'] = trim($fn . ($ln ? ' ' . $ln : '')); redirect('?page=register&step=2'); break; case 'reg_step2': $un = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($_POST['username'] ?? ''))); $em = trim($_POST['email'] ?? ''); if (strlen($un) < 3) { $_SESSION['error'] = 'Username must be at least 3 characters.'; redirect('?page=register&step=2'); } if (!filter_var($em, FILTER_VALIDATE_EMAIL)) { $_SESSION['error'] = 'That doesn\'t look like a valid email.'; redirect('?page=register&step=2'); } $chk = getDB()->prepare("SELECT id FROM users WHERE username=? OR email=?"); $chk->execute([$un, $em]); if ($chk->fetch()) { $_SESSION['error'] = 'That username or email is already taken.'; redirect('?page=register&step=2'); } $_SESSION['reg_un'] = $un; $_SESSION['reg_em'] = $em; redirect('?page=register&step=3'); break; case 'logout': session_destroy(); redirect('?page=login'); break; case 'post': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { $_SESSION['error'] = 'Your account is suspended.'; redirect('?page=home'); break; } $content = trim($_POST['content'] ?? ''); $vis = $_POST['visibility'] ?? 'public'; if (!$content && empty($_FILES['image']['name'])) { redirect('?page=home'); break; } $img = ''; if (!empty($_FILES['image']['name'])) $img = uploadFile($_FILES['image'], 'post_'); getDB()->prepare("INSERT INTO posts (user_id,content,image,visibility) VALUES (?,?,?,?)") ->execute([$u['id'], $content, $img, $vis]); redirect('?page=home'); break; case 'community_post': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { redirect('?page=communities'); break; } $cid = (int)($_POST['community_id'] ?? 0); $content = trim($_POST['content'] ?? ''); if (!$content && empty($_FILES['image']['name'])) { redirect('?page=community&id=' . $cid); break; } $commChk = getDB()->prepare("SELECT owner_id FROM communities WHERE id=?"); $commChk->execute([$cid]); $commRow = $commChk->fetch(); if (!$commRow) { redirect('?page=communities'); break; } $isCommOwner = ($commRow['owner_id'] == $u['id'] || $u['role'] === 'admin'); $memChk = getDB()->prepare("SELECT id FROM community_members WHERE community_id=? AND user_id=?"); $memChk->execute([$cid, $u['id']]); $memRow = $memChk->fetch(); if ($isCommOwner && !$memRow) { try { getDB()->prepare("INSERT INTO community_members (community_id,user_id,role) VALUES (?,?,?)")->execute([$cid, $u['id'], 'owner']); } catch (Exception $e) {} } if (!$isCommOwner && !$memRow) { redirect('?page=community&id=' . $cid); break; } $img = ''; if (!empty($_FILES['image']['name'])) $img = uploadFile($_FILES['image'], 'post_'); getDB()->prepare("INSERT INTO posts (user_id,content,image,visibility,community_id) VALUES (?,?,?,'public',?)") ->execute([$u['id'], $content, $img, $cid]); redirect('?page=community&id=' . $cid); break; case 'comment': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { redirect($_SERVER['HTTP_REFERER'] ?? '?page=home'); break; } $pid = (int)($_POST['post_id'] ?? 0); $ct = trim($_POST['content'] ?? ''); if (!$ct || !$pid) { redirect($_SERVER['HTTP_REFERER'] ?? '?page=home'); break; } getDB()->prepare("INSERT INTO comments (post_id,user_id,content) VALUES (?,?,?)")->execute([$pid, $u['id'], $ct]); $s = getDB()->prepare("SELECT user_id FROM posts WHERE id=?"); $s->execute([$pid]); $pp = $s->fetch(); if ($pp) addNotif($pp['user_id'], $u['id'], 'comment', $pid, h($u['display_name']) . ' commented on your post.'); redirect($_SERVER['HTTP_REFERER'] ?? '?page=home'); break; case 'reshare': requireLogin(); $u = currentUser(); header('Content-Type: application/json'); if (isUserSuspended($u)) { echo json_encode(['ok' => false, 'error' => 'Account suspended']); exit; } $pid = (int)($_POST['post_id'] ?? 0); $cmt = trim($_POST['comment'] ?? ''); $origStmt = getDB()->prepare("SELECT * FROM posts WHERE id=?"); $origStmt->execute([$pid]); $orig = $origStmt->fetch(); if (!$orig) { echo json_encode(['ok' => false, 'error' => 'Post not found']); exit; } if ($orig['user_id'] == $u['id']) { echo json_encode(['ok' => false, 'error' => 'Can\'t reshare your own post']); exit; } $rootId = $orig['original_post_id'] ? (int)$orig['original_post_id'] : $pid; try { getDB()->prepare("INSERT INTO reshares (post_id,user_id,comment) VALUES (?,?,?)")->execute([$rootId, $u['id'], $cmt]); getDB()->prepare("INSERT INTO posts (user_id,content,image,visibility,original_post_id,reshare_comment) VALUES (?,?,?,?,?,?)") ->execute([$u['id'], '', '', $orig['visibility'], $rootId, $cmt]); $rootStmt = getDB()->prepare("SELECT user_id FROM posts WHERE id=?"); $rootStmt->execute([$rootId]); $rootRow = $rootStmt->fetch(); if ($rootRow) addNotif($rootRow['user_id'], $u['id'], 'reshare', $rootId, h($u['display_name']) . ' reshared your post.'); $cnt = getDB()->prepare("SELECT COUNT(*) FROM reshares WHERE post_id=?"); $cnt->execute([$rootId]); echo json_encode(['ok' => true, 'count' => (int)$cnt->fetchColumn()]); } catch (Exception $e) { getDB()->prepare("DELETE FROM reshares WHERE post_id=? AND user_id=?")->execute([$rootId, $u['id']]); getDB()->prepare("DELETE FROM posts WHERE user_id=? AND original_post_id=?")->execute([$u['id'], $rootId]); $cnt = getDB()->prepare("SELECT COUNT(*) FROM reshares WHERE post_id=?"); $cnt->execute([$rootId]); echo json_encode(['ok' => true, 'count' => (int)$cnt->fetchColumn(), 'undone' => true]); } exit; case 'ripples': header('Content-Type: application/json'); $pid = (int)($_GET['post_id'] ?? 0); $rs2 = getDB()->prepare("SELECT r.*,u.display_name,u.username,u.avatar,u.tagline FROM reshares r JOIN users u ON u.id=r.user_id WHERE r.post_id=? ORDER BY r.created_at ASC"); $rs2->execute([$pid]); $reshares = $rs2->fetchAll(); $nodes = []; $edges = []; $rootPost = getDB()->prepare("SELECT p.*,u.display_name,u.username,u.avatar FROM posts p JOIN users u ON u.id=p.user_id WHERE p.id=?"); $rootPost->execute([$pid]); $root = $rootPost->fetch(); if ($root) $nodes[0] = ['id' => 0, 'uid' => $root['user_id'], 'name' => $root['display_name'], 'avatar' => $root['avatar'], 'username' => $root['username'], 'type' => 'root']; foreach ($reshares as $i => $r) { $nodes[] = ['id' => $i+1, 'uid' => $r['user_id'], 'name' => $r['display_name'], 'avatar' => $r['avatar'], 'username' => $r['username'], 'type' => 'reshare', 'comment' => $r['comment'], 'time' => $r['created_at']]; $edges[] = ['from' => 0, 'to' => $i+1]; } echo json_encode(['nodes' => $nodes, 'edges' => $edges, 'total' => count($reshares)]); exit; case 'plusone': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { header('Content-Type: application/json'); $c = getDB()->prepare("SELECT COUNT(*) FROM plusones WHERE post_id=?"); $c->execute([$_POST['post_id'] ?? 0]); echo json_encode(['count' => (int)$c->fetchColumn()]); exit; } $pid = (int)($_POST['post_id'] ?? 0); try { getDB()->prepare("INSERT INTO plusones (post_id,user_id) VALUES (?,?)")->execute([$pid, $u['id']]); $s = getDB()->prepare("SELECT user_id FROM posts WHERE id=?"); $s->execute([$pid]); $pp = $s->fetch(); if ($pp) addNotif($pp['user_id'], $u['id'], 'plusone', $pid, h($u['display_name']) . " +1'd your post."); } catch (Exception $e) { getDB()->prepare("DELETE FROM plusones WHERE post_id=? AND user_id=?")->execute([$pid, $u['id']]); } header('Content-Type: application/json'); $c = getDB()->prepare("SELECT COUNT(*) FROM plusones WHERE post_id=?"); $c->execute([$pid]); echo json_encode(['count' => (int)$c->fetchColumn()]); exit; case 'follow': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { redirect($_SERVER['HTTP_REFERER'] ?? '?page=home'); break; } $tid = (int)($_POST['target_id'] ?? 0); if ($tid === $u['id']) break; try { getDB()->prepare("INSERT INTO follows (follower_id,following_id) VALUES (?,?)")->execute([$u['id'], $tid]); addNotif($tid, $u['id'], 'follow', 0, h($u['display_name']) . ' started following you.'); } catch (Exception $e) { getDB()->prepare("DELETE FROM follows WHERE follower_id=? AND following_id=?")->execute([$u['id'], $tid]); } redirect($_SERVER['HTTP_REFERER'] ?? '?page=home'); break; case 'toggle_follow': header('Content-Type: application/json'); requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { echo json_encode(['ok' => false, 'error' => 'Account suspended']); exit; } $tid = (int)($_POST['followee_id'] ?? 0); if (!$tid || $tid === $u['id']) { echo json_encode(['ok' => false, 'error' => 'Invalid user']); exit; } $existing = getDB()->prepare("SELECT 1 FROM follows WHERE follower_id=? AND following_id=?"); $existing->execute([$u['id'], $tid]); if ($existing->fetch()) { getDB()->prepare("DELETE FROM follows WHERE follower_id=? AND following_id=?")->execute([$u['id'], $tid]); echo json_encode(['ok' => true, 'following' => false]); } else { getDB()->prepare("INSERT INTO follows (follower_id,following_id) VALUES (?,?)")->execute([$u['id'], $tid]); addNotif($tid, $u['id'], 'follow', 0, h($u['display_name']) . ' started following you.'); echo json_encode(['ok' => true, 'following' => true]); } exit; case 'update_profile': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { redirect('?page=home'); break; } $dn = trim($_POST['display_name'] ?? $u['display_name']); $bio = trim($_POST['bio'] ?? ''); $tl = trim($_POST['tagline'] ?? ''); $loc = trim($_POST['location'] ?? ''); $web = trim($_POST['website'] ?? ''); $av = $u['avatar']; $cv = $u['cover']; if (!empty($_FILES['avatar']['name'])) $av = uploadFile($_FILES['avatar'], 'av_'); if (!empty($_FILES['cover']['name'])) $cv = uploadFile($_FILES['cover'], 'cv_'); getDB()->prepare("UPDATE users SET display_name=?,bio=?,tagline=?,location=?,website=?,avatar=?,cover=? WHERE id=?") ->execute([$dn, $bio, $tl, $loc, $web, $av, $cv, $u['id']]); redirect('?page=profile&id=' . $u['id']); break; case 'send_message': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { redirect('?page=messages'); break; } $tid = (int)($_POST['to_id'] ?? 0); $ct = trim($_POST['content'] ?? ''); if (!$ct || !$tid) break; getDB()->prepare("INSERT INTO messages (from_id,to_id,content) VALUES (?,?,?)")->execute([$u['id'], $tid, $ct]); addNotif($tid, $u['id'], 'message', 0, h($u['display_name']) . ' sent you a message.'); redirect('?page=messages&with=' . $tid); break; case 'create_community': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { redirect('?page=communities'); break; } $nm = trim($_POST['name'] ?? ''); $ds = trim($_POST['description'] ?? ''); $vi = $_POST['visibility'] ?? 'public'; $color = preg_match('/^#[0-9a-fA-F]{6}$/', $_POST['color'] ?? '') ? $_POST['color'] : '#4285f4'; $icon = trim($_POST['icon'] ?? '') ?: '๐'; $banner = preg_match('/^#[0-9a-fA-F]{6}$/', $_POST['banner_color'] ?? '') ? $_POST['banner_color'] : ''; $bannerImg = !empty($_FILES['banner_image']['name']) ? uploadFile($_FILES['banner_image'], 'comm_banner_') : ''; $iconImg = !empty($_FILES['icon_image']['name']) ? uploadFile($_FILES['icon_image'], 'comm_icon_') : ''; if (!$nm) { redirect('?page=communities'); break; } try { getDB()->prepare("INSERT INTO communities (owner_id,name,description,visibility,color,icon,banner_color,banner_image,icon_image) VALUES (?,?,?,?,?,?,?,?,?)") ->execute([$u['id'], $nm, $ds, $vi, $color, $icon, $banner, $bannerImg, $iconImg]); $cid = getDB()->lastInsertId(); getDB()->prepare("INSERT INTO community_members (community_id,user_id,role) VALUES (?,?,?)")->execute([$cid, $u['id'], 'owner']); } catch (Exception $e) {} redirect('?page=communities'); break; case 'edit_community': requireLogin(); $u = currentUser(); $cid = (int)($_POST['community_id'] ?? 0); $chk = getDB()->prepare("SELECT * FROM communities WHERE id=?"); $chk->execute([$cid]); $row = $chk->fetch(); if (!$row || ($row['owner_id'] != $u['id'] && $u['role'] !== 'admin')) { redirect('?page=communities'); break; } $nm = trim($_POST['name'] ?? ''); $ds = trim($_POST['description'] ?? ''); $vi = $_POST['visibility'] ?? 'public'; $color = preg_match('/^#[0-9a-fA-F]{6}$/', $_POST['color'] ?? '') ? $_POST['color'] : '#4285f4'; $icon = trim($_POST['icon'] ?? '') ?: '๐'; $banner = preg_match('/^#[0-9a-fA-F]{6}$/', $_POST['banner_color'] ?? '') ? $_POST['banner_color'] : ''; $bannerImg = $row['banner_image'] ?? ''; if (!empty($_FILES['banner_image']['name'])) $bannerImg = uploadFile($_FILES['banner_image'], 'comm_banner_'); if (!empty($_POST['clear_banner_image'])) $bannerImg = ''; $iconImg = $row['icon_image'] ?? ''; if (!empty($_FILES['icon_image']['name'])) $iconImg = uploadFile($_FILES['icon_image'], 'comm_icon_'); if (!empty($_POST['clear_icon_image'])) $iconImg = ''; if (!$nm) { redirect('?page=community&id=' . $cid); break; } try { getDB()->prepare("UPDATE communities SET name=?,description=?,visibility=?,color=?,icon=?,banner_color=?,banner_image=?,icon_image=? WHERE id=?") ->execute([$nm, $ds, $vi, $color, $icon, $banner, $bannerImg, $iconImg, $cid]); } catch (Exception $e) {} redirect('?page=community&id=' . $cid . '&edited=1'); break; case 'join_community': requireLogin(); $u = currentUser(); if (isUserSuspended($u)) { redirect('?page=community&id=' . ($_POST['community_id'] ?? 0)); break; } $cid = (int)($_POST['community_id'] ?? 0); try { getDB()->prepare("INSERT INTO community_members (community_id,user_id) VALUES (?,?)")->execute([$cid, $u['id']]); } catch (Exception $e) { getDB()->prepare("DELETE FROM community_members WHERE community_id=? AND user_id=?")->execute([$cid, $u['id']]); } redirect('?page=community&id=' . $cid); break; case 'mark_notifs': requireLogin(); $u = currentUser(); getDB()->prepare("UPDATE notifications SET is_read=1 WHERE user_id=?")->execute([$u['id']]); redirect('?page=notifications'); break; case 'delete_post': requireLogin(); $u = currentUser(); $pid = (int)($_POST['post_id'] ?? 0); $s = getDB()->prepare("SELECT user_id FROM posts WHERE id=?"); $s->execute([$pid]); $pp = $s->fetch(); if ($pp && ($pp['user_id'] == $u['id'] || $u['role'] === 'admin')) getDB()->prepare("DELETE FROM posts WHERE id=?")->execute([$pid]); redirect($_SERVER['HTTP_REFERER'] ?? '?page=home'); break; case 'admin_delete_user': requireAdmin(); $uid = (int)($_POST['user_id'] ?? 0); if ($uid !== (int)currentUser()['id']) getDB()->prepare("DELETE FROM users WHERE id=?")->execute([$uid]); redirect('?page=admin'); break; case 'admin_toggle_role': requireAdmin(); $uid = (int)($_POST['user_id'] ?? 0); $s = getDB()->prepare("SELECT role FROM users WHERE id=?"); $s->execute([$uid]); $r = $s->fetch(); if ($r && $uid !== (int)currentUser()['id']) getDB()->prepare("UPDATE users SET role=? WHERE id=?")->execute([$r['role'] === 'admin' ? 'user' : 'admin', $uid]); redirect('?page=admin'); break; case 'admin_toggle_early_access': requireAdmin(); setSetting('early_access_mode', isEarlyAccess() ? '0' : '1'); redirect('?page=admin'); break; case 'admin_verify': header('Content-Type: application/json'); requireAdmin(); $uid = (int)($_POST['user_id'] ?? 0); $v = (int)($_POST['verified'] ?? 0); if (!$uid) { echo json_encode(['ok' => false]); exit; } getDB()->prepare("UPDATE users SET verified=? WHERE id=?")->execute([$v ? 1 : 0, $uid]); echo json_encode(['ok' => true, 'verified' => (bool)$v]); exit; case 'admin_toggle_maintenance': requireAdmin(); setSetting('maintenance_mode', isMaintenanceMode() ? '0' : '1'); redirect('?page=admin&tab=settings'); break; case 'admin_save_settings': requireAdmin(); foreach (['site_name', 'site_description'] as $k) if (isset($_POST[$k])) setSetting($k, trim($_POST[$k])); $_SESSION['success'] = 'Settings saved.'; redirect('?page=admin&tab=settings'); break; case 'admin_clear_base64': requireAdmin(); getDB()->exec("UPDATE users SET avatar='' WHERE avatar LIKE 'data:%'"); getDB()->exec("UPDATE users SET cover='' WHERE cover LIKE 'data:%'"); getDB()->exec("UPDATE posts SET image='' WHERE image LIKE 'data:%'"); getDB()->exec("UPDATE communities SET banner_image='' WHERE banner_image LIKE 'data:%'"); getDB()->exec("UPDATE communities SET icon_image='' WHERE icon_image LIKE 'data:%'"); $_SESSION['success'] = 'Cleared all base64 images.'; redirect('?page=admin'); break; case 'admin_suspend_user': requireAdmin(); $uid = (int)($_POST['user_id'] ?? 0); $reason = trim($_POST['suspend_reason'] ?? ''); $type = $_POST['suspend_type'] ?? 'permanent'; $days = max(1, (int)($_POST['suspend_days'] ?? 1)); if (!$reason) { $_SESSION['error'] = 'Please provide a reason for the suspension.'; redirect('?page=admin'); break; } $until = $type === 'temporary' ? date('Y-m-d H:i:s', strtotime("+{$days} days")) : null; suspendUser($uid, $reason, $until); $uRow = getDB()->prepare("SELECT display_name FROM users WHERE id=?"); $uRow->execute([$uid]); $uRow = $uRow->fetch(); $_SESSION['success'] = 'User "' . h($uRow['display_name'] ?? '') . '" has been suspended.'; redirect('?page=admin'); break; case 'admin_unsuspend_user': requireAdmin(); $uid = (int)($_POST['user_id'] ?? 0); unsuspendUser($uid); $uRow = getDB()->prepare("SELECT display_name FROM users WHERE id=?"); $uRow->execute([$uid]); $uRow = $uRow->fetch(); $_SESSION['success'] = 'Lifted suspension for "' . h($uRow['display_name'] ?? '') . '".'; redirect('?page=admin'); break; case 'admin_create_user': requireAdmin(); $un = preg_replace('/[^a-z0-9_]/', '', strtolower(trim($_POST['username'] ?? ''))); $em = trim($_POST['email'] ?? ''); $pw = $_POST['password'] ?? ''; $dn = trim($_POST['display_name'] ?? ''); $ea = isEarlyAccess() ? 1 : 0; if (strlen($un) < 3 || strlen($pw) < 6 || !filter_var($em, FILTER_VALIDATE_EMAIL) || !$dn) { $_SESSION['error'] = 'Fill in all fields. Username needs 3+ chars, password 6+.'; redirect('?page=admin'); break; } try { getDB()->prepare("INSERT INTO users (username,email,password,display_name,early_access,tos_accepted) VALUES (?,?,?,?,?,1)") ->execute([$un, $em, password_hash($pw, PASSWORD_DEFAULT), $dn, $ea]); $nid = (int)getDB()->lastInsertId(); foreach (['Friends' => '#4285f4', 'Family' => '#0f9d58', 'Acquaintances' => '#f4b400', 'Following' => '#dd4b39'] as $name => $color) getDB()->prepare("INSERT INTO circles (user_id,name,color) VALUES (?,?,?)")->execute([$nid, $name, $color]); $_SESSION['success'] = 'Created user "' . $dn . '".'; } catch (Exception $e) { $_SESSION['error'] = 'That username or email is already taken.'; } redirect('?page=admin'); break; } } $u = currentUser(); $notifCount = unreadNotifs(); $msgCount = unreadMsgs(); $error = $_SESSION['error'] ?? ''; $success = $_SESSION['success'] ?? ''; unset($_SESSION['error'], $_SESSION['success']); if ($u && isUserSuspended($u) && $action !== 'logout' && !in_array($page, ['login', 'suspended'])) { $page = 'suspended'; } $COMM_ICONS = ['๐','๐ฎ','๐จ','๐ธ','๐ต','๐','๐ก','๐ฌ','๐','๐ฑ','๐ ','โ๏ธ','๐','โฝ','๐ฌ','๐ป','๐พ','๐ธ','๐ฏ','๐ฅ']; $COMM_COLORS = ['#4285f4','#dd4b39','#0f9d58','#f4b400','#9b6a2f','#8e44ad','#16a085','#2c3e50','#e67e22','#e91e63']; $maintenanceMode = isMaintenanceMode(); $isLegalPage = in_array($page, ['tos','privacy','guidelines','about']); ?>
You can't access = SITE_NAME ?> right now.
This suspension is permanent.
Think this is a mistake? Email = LEGAL_CONTACT ?>.
Effective: = LEGAL_EFFECTIVE ?>
= getTosHtml() ?>Effective: = LEGAL_EFFECTIVE ?>
= getPrivacyHtml() ?>Effective: = LEGAL_EFFECTIVE ?>
= getGuidelinesHtml() ?>to continue to = SITE_NAME ?>
This site is in Early Access Mode. New accounts are created by the admin only.
What should we call you?
At least 6 characters.
Drag someone onto a circle to add them, or drop onto the + to create a new circle.
User not found.
= h($prof['bio']) ?>
“= h($prof['tagline']) ?>”
📍 = h($prof['location']) ?>
Joined = date('F Y', strtotime($prof['created_at'])) ?>
No posts yet.
Community not found.
| User | Role | Joined | Actions |
|---|---|---|---|
|
= h($au['display_name']) ?> = (int)($au['suspended']??0)?'Suspended':'' ?>
@= h($au['username']) ?> · = h($au['email']) ?>
|
= h($au['role']) ?> | = date('M j, Y', strtotime($au['created_at'])) ?> |
|
| Author | Content | Date | |
|---|---|---|---|
| = h($ap['display_name']) ?> | = date('M j, Y', strtotime($ap['created_at'])) ?> |