'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.

2. Eligibility

You must be at least 13 years old to use the Service.

3. Your Account

4. Acceptable Use

Don't post content that:

5. Content Ownership

You own what you post. By posting it, you give $s permission to display and share it through the Service.

6. Termination

We can suspend or delete accounts that break these rules.

7. Disclaimers

The Service is provided "as is". We're not liable for indirect or consequential damages.

8. Changes

We might update these terms sometimes. Continuing to use the Service after changes means you accept them.

9. Questions?

Email us at $e.

Effective: $d

HTML; } function getPrivacyHtml(): string { $s = SITE_NAME; $e = LEGAL_CONTACT; $d = LEGAL_EFFECTIVE; return <<1. What We Collect

2. How We Use It

3. Sharing

We don't share your info with third parties except when legally required.

4. Cookies

Just one session cookie to keep you logged in. No tracking cookies.

5. Data Retention

Your data sticks around as long as your account is active. Email us to delete everything within 30 days.

6. Security

Passwords are hashed with bcrypt.

7. Kids

The Service isn't for anyone under 13.

8. Your Rights

Email $e and we'll help.

9. Changes

We'll let registered users know if anything major changes here.

10. Contact

Email $e.

Effective: $d

HTML; } function getGuidelinesHtml(): string { $s = SITE_NAME; $e = LEGAL_CONTACT; return <<1. Be a decent human

Disagree all you want. But don't harass people, don't make personal attacks, and don't target people because of who they are.

2. No hate speech

Content that promotes hatred or discrimination based on race, ethnicity, religion, gender, sexuality, disability, or nationality isn't allowed.

3. No harassment

Don't intimidate, threaten, or repeatedly go after another user.

4. Keep it legal

Don't post anything illegal. Sexual content involving minors gets reported to law enforcement immediately, no exceptions.

5. No spam

Don't flood feeds, run undisclosed bots, or do anything that looks like coordinated fake activity.

6. Don't promote self-harm

Posts that encourage or glorify self-harm, suicide, eating disorders, or other dangerous health behaviors aren't allowed.

7. Respect privacy

Don't post someone's home address, phone number, or private photos without their consent.

8. Adult content

Explicit content is only allowed in communities clearly marked 18+.

9. Community rules

Individual communities can set extra rules on top of these.

10. Consequences

Breaking the rules can mean content removal, a suspension, or a permanent ban.

11. Reporting

See something? Email $e. We review everything and try to respond within 48 hours.

12. Appeals

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 = '' . '' . '' . htmlspecialchars($l) . '' . ''; 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 = '
'; $out .= '
'; $out .= ''; $out .= '
'; $out .= '
' . $dn . '
'; $out .= '
@' . $un . ' · ' . $t . '
'; $out .= '
'; if ($img) $out .= ''; if ($body) $out .= '
' . $body . '
'; $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(); ?>

is being worked on

We're making things better โ€” check back soon.

Admin Sign In
· v
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']); ?> <?= SITE_NAME ?><?php if ($isLegalPage): ?> โ€” <?php if ($page==='tos') echo 'Terms of Service'; elseif ($page==='privacy') echo 'Privacy Policy'; elseif ($page==='guidelines') echo 'Community Guidelines'; else echo 'About'; endif; ?>
0): ?> 0): ?>
Sign in Join
๐Ÿšซ

Your account has been suspended

You can't access right now.

Reason:
🕐 Suspension lifts:

This suspension is permanent.

Think this is a mistake? Email .

Hazoogle

Sign in

to continue to

🛡 Maintenance mode is on.
Only admins can sign in right now.
Early Access Mode.
New registrations are closed for now.
Create account

Registrations are closed

This site is in Early Access Mode. New accounts are created by the admin only.

Back to Sign In
'16%', '2' => '50%', '3' => '83%'][$step] ?? '16%'; $regName = h($_SESSION['reg_name'] ?? ''); $regUn = h($_SESSION['reg_un'] ?? ''); $regEm = h($_SESSION['reg_em'] ?? ''); ?>
Hazoogle

Create your
Hazel+ account

What should we call you?

Already have an account? Sign in

Pick a username & email

Set a password

At least 6 characters.

Quick summary: Use lawfully and respectfully. You must be at least 13. We don't sell your data or show ads.

Terms of Service โ†—  ·  Privacy Policy โ†—  ·  Community Guidelines โ†—
prepare( "SELECT c.*, (SELECT COUNT(*) FROM circle_members WHERE circle_id=c.id) AS mc FROM circles c WHERE c.user_id=? ORDER BY c.created_at ASC" ); $circlesRaw->execute([$u['id']]); $circlesData = $circlesRaw->fetchAll(); $circleMemberMap = []; foreach ($circlesData as $circ) { $ms = getDB()->prepare("SELECT member_id FROM circle_members WHERE circle_id=?"); $ms->execute([$circ['id']]); $circleMemberMap[$circ['id']] = array_column($ms->fetchAll(), 'member_id'); } $allPeople = getDB()->prepare("SELECT id,display_name,username,avatar,tagline FROM users WHERE id!=? ORDER BY display_name ASC"); $allPeople->execute([$u['id']]); $allPeople = $allPeople->fetchAll(); $jsCircles = json_encode($circlesData); $jsMemberMap = json_encode($circleMemberMap); $jsPeople = json_encode($allPeople); $jsColors = json_encode(circleColorPalette()); $jsMe = json_encode(['id' => $u['id'], 'display_name' => $u['display_name']]); ?>

People

Drag someone onto a circle to add them, or drop onto the + to create a new circle.

'.$initials.''; $avSrc = $p['avatar'] ? h($p['avatar']) : 'data:image/svg+xml;base64,' . base64_encode($svg); ?>
Loadingโ€ฆ

◯ Your Circles

View as list
+
Create circle
or drop a person here
Circle

Create a new circle

"> 0 ? $post['reshare_count'] : '' ?>
+1
Comment prepare("SELECT * FROM users WHERE id=?"); $ps->execute([$pid]); $prof = $ps->fetch(); if (!$prof) { echo '

User not found.

'; goto END_OUTPUT; } $profTab = $_GET['tab'] ?? 'posts'; $isOwn = $u && $u['id'] == $prof['id']; $isFollowing = false; if ($u && !$isOwn) { $fq = getDB()->prepare("SELECT id FROM follows WHERE follower_id=? AND following_id=?"); $fq->execute([$u['id'], $pid]); $isFollowing = (bool)$fq->fetch(); } $followerCount = (int)getDB()->prepare("SELECT COUNT(*) FROM follows WHERE following_id=?")->execute([$pid]) ? getDB()->query("SELECT COUNT(*) FROM follows WHERE following_id=$pid")->fetchColumn() : 0; $followingCount = (int)getDB()->query("SELECT COUNT(*) FROM follows WHERE follower_id=$pid")->fetchColumn(); $postCount = (int)getDB()->query("SELECT COUNT(*) FROM posts WHERE user_id=$pid")->fetchColumn(); ?>
📍 🌐
Posts
Followers
Following

About

📍

🌐

Joined

prepare("SELECT p.*,u.display_name,u.username,u.avatar, (SELECT COUNT(*) FROM plusones WHERE post_id=COALESCE(p.original_post_id,p.id)) AS po_count, (SELECT COUNT(*) FROM comments WHERE post_id=COALESCE(p.original_post_id,p.id)) AS cm_count, (SELECT COUNT(*) FROM reshares WHERE post_id=COALESCE(p.original_post_id,p.id)) AS reshare_count" . ($u ? ",(SELECT COUNT(*) FROM plusones WHERE post_id=COALESCE(p.original_post_id,p.id) AND user_id={$u['id']}) AS user_po" : ",0 AS user_po") . " FROM posts p JOIN users u ON u.id=p.user_id WHERE p.user_id=? ORDER BY p.created_at DESC LIMIT 30"); $profPostStmt->execute([$pid]); $profPosts = $profPostStmt->fetchAll(); if (empty($profPosts)): ?>

No posts yet.

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=?"); $os->execute([$rootPostId]); $origPost = $os->fetch(); } $userResharedProf = $u ? userReshared($rootPostId, $u['id']) : false; ?>
reshared this
query("SELECT p.*,u.display_name,u.username,u.avatar, (SELECT COUNT(*) FROM plusones WHERE post_id=p.id) AS po_count, (SELECT COUNT(*) FROM comments WHERE post_id=p.id) AS cm_count FROM posts p JOIN users u ON u.id=p.user_id WHERE p.visibility='public' AND p.community_id IS NULL AND p.original_post_id IS NULL ORDER BY p.created_at DESC LIMIT 40"); $explorePosts = $exploreQ->fetchAll(); $communities = getDB()->query("SELECT c.*, (SELECT COUNT(*) FROM community_members WHERE community_id=c.id) AS mc FROM communities c WHERE c.visibility='public' ORDER BY mc DESC LIMIT 8")->fetchAll(); ?>

🌐 Explore

Communities
prepare("SELECT u.*, (SELECT COUNT(*) FROM follows WHERE following_id=u.id) AS follower_count, (SELECT COUNT(*) FROM follows WHERE follower_id=? AND following_id=u.id) AS i_follow FROM users u WHERE u.id!=? AND u.suspended=0 ORDER BY follower_count DESC, u.display_name ASC LIMIT 60"); $peopleQ->execute([$u['id'], $u['id']]); $people = $peopleQ->fetchAll(); ?>

👥 People

@
prepare("SELECT c.*,(SELECT COUNT(*) FROM community_members WHERE community_id=c.id) AS mc FROM communities c JOIN community_members cm ON cm.community_id=c.id WHERE cm.user_id=? ORDER BY c.name ASC"); $myComms->execute([$u['id']]); $myComms = $myComms->fetchAll(); $allComms = getDB()->query("SELECT c.*,(SELECT COUNT(*) FROM community_members WHERE community_id=c.id) AS mc FROM communities c WHERE c.visibility='public' ORDER BY mc DESC")->fetchAll(); $createError = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'create_community') { $cname = trim($_POST['cname'] ?? ''); $cdesc = trim($_POST['cdesc'] ?? ''); $cvis = ($_POST['cvis'] ?? 'public') === 'private' ? 'private' : 'public'; $cicon = $_POST['cicon'] ?? '๐ŸŒ'; $ccolor = $_POST['ccolor'] ?? '#4285f4'; if (!$cname) { $createError = 'Name is required.'; } else { try { getDB()->prepare("INSERT INTO communities (owner_id,name,description,visibility,icon,color) VALUES (?,?,?,?,?,?)") ->execute([$u['id'], $cname, $cdesc, $cvis, $cicon, $ccolor]); $newId = getDB()->lastInsertId(); getDB()->prepare("INSERT INTO community_members (community_id,user_id,role) VALUES (?,?,'owner')")->execute([$newId, $u['id']]); redirect("?page=community&id=$newId"); } catch (Exception $e) { $createError = 'A community with that name already exists.'; } } } ?>

📍 Communities

Your Communities
All Communities

No communities yet. Create the first one!

Create a Community

prepare("SELECT * FROM communities WHERE id=?")->execute([$cid]) ? getDB()->query("SELECT * FROM communities WHERE id=$cid")->fetch() : null; if (!$comm) { echo '

Community not found.

'; goto END_OUTPUT; } $isMember = (bool)getDB()->query("SELECT id FROM community_members WHERE community_id=$cid AND user_id={$u['id']}")->fetch(); $isOwner = $comm['owner_id'] == $u['id']; $memberCount = (int)getDB()->query("SELECT COUNT(*) FROM community_members WHERE community_id=$cid")->fetchColumn(); $commPosts = getDB()->query("SELECT p.*,u.display_name,u.username,u.avatar, (SELECT COUNT(*) FROM plusones WHERE post_id=p.id) AS po_count, (SELECT COUNT(*) FROM comments WHERE post_id=p.id) AS cm_count FROM posts p JOIN users u ON u.id=p.user_id WHERE p.community_id=$cid ORDER BY p.created_at DESC LIMIT 30")->fetchAll(); $bannerStyle = $comm['banner_image'] ? '' : ('background:'.h($comm['banner_color'] ?: $comm['color'])); $iconImage = $comm['icon_image'] ?? ''; $hasIconImg = !empty($iconImage); ?>

member
Edit

prepare("UPDATE messages SET is_read=1 WHERE to_id=? AND from_id=?")->execute([$u['id'], $withId]); } // Get conversation list $convQ = getDB()->query("SELECT DISTINCT CASE WHEN m.from_id={$u['id']} THEN m.to_id ELSE m.from_id END AS other_id, MAX(m.created_at) AS last_msg FROM messages m WHERE m.from_id={$u['id']} OR m.to_id={$u['id']} GROUP BY other_id ORDER BY last_msg DESC LIMIT 30"); $conversations = $convQ->fetchAll(); // Get messages in current conversation $chatMessages = []; $chatWith = null; if ($withId) { $cwQ = getDB()->prepare("SELECT * FROM users WHERE id=?"); $cwQ->execute([$withId]); $chatWith = $cwQ->fetch(); if ($chatWith) { $msgsQ = getDB()->prepare("SELECT * FROM messages WHERE (from_id=? AND to_id=?) OR (from_id=? AND to_id=?) ORDER BY created_at ASC LIMIT 100"); $msgsQ->execute([$u['id'], $withId, $withId, $u['id']]); $chatMessages = $msgsQ->fetchAll(); } } ?>

💬 Messages

Conversations
No messages yet.
prepare("SELECT id,display_name,username,avatar FROM users WHERE id=?"); $othQ->execute([$conv['other_id']]); $other = $othQ->fetch(); if (!$other) continue; ?>
@
Start the conversation!
Select a conversation or find people to message.
prepare("UPDATE notifications SET is_read=1 WHERE user_id=?")->execute([$u['id']]); $notifsQ = getDB()->prepare("SELECT n.*,u.display_name,u.username,u.avatar FROM notifications n JOIN users u ON u.id=n.from_user_id WHERE n.user_id=? ORDER BY n.created_at DESC LIMIT 50"); $notifsQ->execute([$u['id']]); $notifs = $notifsQ->fetchAll(); ?>

🔔 Notifications

No notifications yet.

prepare("UPDATE users SET display_name=?,bio=?,tagline=?,location=?,website=?,avatar=?,cover=? WHERE id=?") ->execute([$dn, $bio, $tag, $loc, $web, $newAv, $newCov, $u['id']]); $settingsSuccess = 'Profile updated!'; // Refresh user $su = getDB()->prepare("SELECT * FROM users WHERE id=?"); $su->execute([$u['id']]); $u = $su->fetch(); } if ($_POST['new_password'] ?? '') { if ($_POST['new_password'] !== ($_POST['confirm_password'] ?? '')) { $settingsError = 'Passwords do not match.'; } elseif (strlen($_POST['new_password']) < 6) { $settingsError = 'Password must be at least 6 characters.'; } else { getDB()->prepare("UPDATE users SET password=? WHERE id=?")->execute([password_hash($_POST['new_password'], PASSWORD_DEFAULT), $u['id']]); $settingsSuccess = 'Password updated!'; } } } ?>

⚙ Settings

Profile

Photos

Change Password

prepare("DELETE FROM users WHERE id=?")->execute([$uid]); $adminMsg = 'User deleted.'; } } elseif ($act === 'admin_suspend_user') { $uid = (int)$_POST['uid']; $reason = trim($_POST['reason'] ?? ''); $until = $_POST['until'] ?? null; suspendUser($uid, $reason, $until ?: null); $adminMsg = 'User suspended.'; } elseif ($act === 'admin_unsuspend_user') { $uid = (int)$_POST['uid']; unsuspendUser($uid); $adminMsg = 'User unsuspended.'; } elseif ($act === 'admin_delete_post') { $pid = (int)$_POST['pid']; getDB()->prepare("DELETE FROM posts WHERE id=?")->execute([$pid]); $adminMsg = 'Post deleted.'; } elseif ($act === 'admin_toggle_ea') { $cur = getSetting('early_access_mode'); setSetting('early_access_mode', $cur === '1' ? '0' : '1'); $adminMsg = 'Early access mode ' . ($cur === '1' ? 'disabled' : 'enabled') . '.'; } elseif ($act === 'admin_toggle_maint') { $cur = getSetting('maintenance_mode'); setSetting('maintenance_mode', $cur === '1' ? '0' : '1'); $adminMsg = 'Maintenance mode ' . ($cur === '1' ? 'disabled' : 'enabled') . '.'; } elseif ($act === 'admin_grant_ea') { $uid = (int)$_POST['uid']; getDB()->prepare("UPDATE users SET early_access=1 WHERE id=?")->execute([$uid]); $adminMsg = 'Early access granted.'; } elseif ($act === 'admin_create_user') { $adn = trim($_POST['adn'] ?? ''); $aun = trim($_POST['aun'] ?? ''); $aem = trim($_POST['aem'] ?? ''); $apw = trim($_POST['apw'] ?? ''); $arl = $_POST['arl'] ?? 'user'; if (!$adn || !$aun || !$aem || !$apw) { $adminErr = 'All fields required.'; } else { try { getDB()->prepare("INSERT INTO users (display_name,username,email,password,role,tos_accepted,early_access) VALUES (?,?,?,?,?,1,1)") ->execute([$adn, $aun, $aem, password_hash($apw, PASSWORD_DEFAULT), $arl]); $adminMsg = 'User created.'; } catch (Exception $e) { $adminErr = 'Username or email already exists.'; } } } } $totalUsers = (int)getDB()->query("SELECT COUNT(*) FROM users")->fetchColumn(); $totalPosts = (int)getDB()->query("SELECT COUNT(*) FROM posts")->fetchColumn(); $totalComms = (int)getDB()->query("SELECT COUNT(*) FROM communities")->fetchColumn(); $totalFollows = (int)getDB()->query("SELECT COUNT(*) FROM follows")->fetchColumn(); $eaMode = isEarlyAccess(); $maintMode = isMaintenanceMode(); ?>

☉ Admin Panel

Overview Users Posts Settings
Users
Posts
Communities
Follows

Create User

query("SELECT * FROM users ORDER BY created_at DESC")->fetchAll(); foreach ($usersAll as $au): ?>
UserRoleJoinedActions
Suspended':'' ?>
@ ·
View
query("SELECT p.*,u.display_name FROM posts p JOIN users u ON u.id=p.user_id ORDER BY p.created_at DESC LIMIT 60")->fetchAll(); foreach ($postsAll as $ap): ?>
AuthorContentDate
⚡ Early Access Mode
When on, new user registration is disabled. Only admins can create accounts.
🛡 Maintenance Mode
When on, only admins can sign in. Other users see a maintenance message.

☀ Ripples