// ────────────────────────────────────────────── // MemberAuth // Handles registration, login, sessions, ratings // ────────────────────────────────────────────── class MemberAuth { private $mysqli; // How long "remember me" cookies last (30 days) const REMEMBER_DAYS = 30; public function __construct($mysqli) { $this->mysqli = $mysqli; } // ── Registration ───────────────────────── /** * Register a new member. * Returns ['ok' => true, 'member' => [...]] or ['ok' => false, 'error' => '...'] */ public function register(string $username, string $email, string $password, string $display_name = ''): array { $username = clean_string($username, 50); $email = trim($email); $display_name = clean_string($display_name ?: $username, 100); // Basic validation if (strlen($username) < 3) { return ['ok' => false, 'error' => 'Username must be at least 3 characters.']; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return ['ok' => false, 'error' => 'Please enter a valid email address.']; } if (strlen($password) < 8) { return ['ok' => false, 'error' => 'Password must be at least 8 characters.']; } if (!preg_match('/^[a-zA-Z0-9_\-\.]+$/', $username)) { return ['ok' => false, 'error' => 'Username may only contain letters, numbers, underscores, hyphens, and dots.']; } // Check uniqueness if ($this->findByUsername($username)) { return ['ok' => false, 'error' => 'That username is already taken.']; } if ($this->findByEmail($email)) { return ['ok' => false, 'error' => 'An account with that email already exists.']; } $hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]); $stmt = $this->mysqli->prepare(" INSERT INTO stream_members (username, email, password_hash, display_name) VALUES (?, ?, ?, ?) "); $stmt->bind_param('ssss', $username, $email, $hash, $display_name); $stmt->execute(); $member_id = $this->mysqli->insert_id; $stmt->close(); $member = $this->findById($member_id); return ['ok' => true, 'member' => $member]; } // ── Login / Logout ──────────────────────── /** * Attempt login with username-or-email + password. * On success: stores member in session, optionally sets remember cookie. * Returns ['ok' => true, 'member' => [...]] or ['ok' => false, 'error' => '...'] */ public function login(string $identifier, string $password, bool $remember = false): array { $identifier = trim($identifier); // Accept either username or email $member = filter_var($identifier, FILTER_VALIDATE_EMAIL) ? $this->findByEmail($identifier) : $this->findByUsername($identifier); if (!$member) { return ['ok' => false, 'error' => 'Invalid username/email or password.']; } if ($member['status'] === 'suspended') { return ['ok' => false, 'error' => 'This account has been suspended.']; } if (!password_verify($password, $member['password_hash'])) { return ['ok' => false, 'error' => 'Invalid username/email or password.']; } // Update last_login $id = $member['id']; $this->mysqli->query("UPDATE stream_members SET last_login = NOW() WHERE id = $id"); // Store in session $_SESSION['stream_member'] = $this->safeArray($member); // Remember me cookie if ($remember) { $token = bin2hex(random_bytes(32)); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET remember_token = ? WHERE id = ?" ); $stmt->bind_param('si', $token, $id); $stmt->execute(); $stmt->close(); setcookie( 'stream_remember', $id . ':' . $token, time() + (86400 * self::REMEMBER_DAYS), '/', '', // domain — empty = current domain true, // secure (HTTPS only) true // httpOnly ); } return ['ok' => true, 'member' => $_SESSION['stream_member']]; } /** * Log the current member out and clear remember cookie. */ public function logout(): void { if (isset($_SESSION['stream_member'])) { $id = (int)$_SESSION['stream_member']['id']; $this->mysqli->query( "UPDATE stream_members SET remember_token = NULL WHERE id = $id" ); } unset($_SESSION['stream_member']); setcookie('stream_remember', '', time() - 3600, '/'); } // ── Session / remember-me resolution ───── /** * Return the currently logged-in member array, or null. * Checks session first, then remember-me cookie. * Call this at the top of every page that needs auth state. */ public function currentMember(): ?array { // Already in session if (!empty($_SESSION['stream_member'])) { return $_SESSION['stream_member']; } // Try remember-me cookie if (!empty($_COOKIE['stream_remember'])) { $parts = explode(':', $_COOKIE['stream_remember'], 2); if (count($parts) === 2) { [$id, $token] = $parts; $id = (int)$id; $stmt = $this->mysqli->prepare( "SELECT * FROM stream_members WHERE id = ? AND remember_token = ? AND status = 'active' LIMIT 1" ); $stmt->bind_param('is', $id, $token); $stmt->execute(); $member = $stmt->get_result()->fetch_assoc(); $stmt->close(); if ($member) { $_SESSION['stream_member'] = $this->safeArray($member); return $_SESSION['stream_member']; } } // Bad cookie — clear it setcookie('stream_remember', '', time() - 3600, '/'); } return null; } /** * Redirect to login page unless a member is logged in. * Returns the member array if logged in. */ public function requireLogin(string $redirect_to = 'member-login.php'): array { $member = $this->currentMember(); if (!$member) { header('Location: ' . $redirect_to . '?next=' . urlencode($_SERVER['REQUEST_URI'])); exit; } return $member; } // ── Profile updates ─────────────────────── /** * Update display_name, bio, avatar_url for a member. */ public function updateProfile(int $member_id, string $display_name, string $bio, string $avatar_url): bool { $display_name = clean_string($display_name, 100); $bio = clean_string($bio, 500); $avatar_url = clean_url($avatar_url); $stmt = $this->mysqli->prepare(" UPDATE stream_members SET display_name = ?, bio = ?, avatar_url = ? WHERE id = ? "); $stmt->bind_param('sssi', $display_name, $bio, $avatar_url, $member_id); $result = $stmt->execute(); $stmt->close(); // Refresh session if editing self if ($result && isset($_SESSION['stream_member']) && (int)$_SESSION['stream_member']['id'] === $member_id) { $member = $this->findById($member_id); $_SESSION['stream_member'] = $this->safeArray($member); } return $result; } /** * Change password — verifies old password first. */ public function changePassword(int $member_id, string $old_password, string $new_password): array { if (strlen($new_password) < 8) { return ['ok' => false, 'error' => 'New password must be at least 8 characters.']; } $member = $this->findById($member_id); if (!$member || !password_verify($old_password, $member['password_hash'])) { return ['ok' => false, 'error' => 'Current password is incorrect.']; } $hash = password_hash($new_password, PASSWORD_BCRYPT, ['cost' => 12]); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET password_hash = ? WHERE id = ?" ); $stmt->bind_param('si', $hash, $member_id); $stmt->execute(); $stmt->close(); return ['ok' => true]; } // ── Member ratings ──────────────────────── /** * Give +1 or -1 reputation to a member for a specific post. * A rater can only vote once per post per rated member. * Members cannot rate themselves. * Returns ['ok' => bool, 'new_score' => int, 'error' => '...'] */ public function rateMember(int $rater_id, int $rated_id, int $post_id, int $points = 1): array { if ($rater_id === $rated_id) { return ['ok' => false, 'error' => 'You cannot rate yourself.']; } $points = $points >= 0 ? 1 : -1; // Check for existing vote $stmt = $this->mysqli->prepare(" SELECT id FROM stream_member_ratings WHERE rater_id = ? AND rated_id = ? AND post_id = ? "); $stmt->bind_param('iii', $rater_id, $rated_id, $post_id); $stmt->execute(); $existing = $stmt->get_result()->fetch_assoc(); $stmt->close(); if ($existing) { return ['ok' => false, 'error' => 'You have already rated this member for this post.']; } // Insert rating $stmt = $this->mysqli->prepare(" INSERT INTO stream_member_ratings (rater_id, rated_id, post_id, points) VALUES (?, ?, ?, ?) "); $stmt->bind_param('iiii', $rater_id, $rated_id, $post_id, $points); $stmt->execute(); $stmt->close(); // Update running total on member $stmt = $this->mysqli->prepare( "UPDATE stream_members SET rating_score = rating_score + ? WHERE id = ?" ); $stmt->bind_param('ii', $points, $rated_id); $stmt->execute(); $stmt->close(); // Return new score $row = $this->findById($rated_id); return ['ok' => true, 'new_score' => (int)($row['rating_score'] ?? 0)]; } /** * Get the rating score (and badge label) for a member. */ public static function ratingBadge(int $score): array { if ($score >= 500) return ['label' => 'Legend', 'class' => 'badge-legend']; if ($score >= 100) return ['label' => 'Expert', 'class' => 'badge-expert']; if ($score >= 25) return ['label' => 'Trusted', 'class' => 'badge-trusted']; if ($score >= 5) return ['label' => 'Member', 'class' => 'badge-member']; return ['label' => 'New', 'class' => 'badge-new']; } // ── Admin actions ───────────────────────── /** * Return all members for admin listing, newest first. */ public function getAllMembers(int $limit = 200, int $offset = 0): array { $members = []; $stmt = $this->mysqli->prepare(" SELECT id, username, email, display_name, role, rating_score, status, last_login, created_at FROM stream_members ORDER BY created_at DESC LIMIT ? OFFSET ? "); $stmt->bind_param('ii', $limit, $offset); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { $members[] = $row; } $stmt->close(); return $members; } /** * Count total members. */ public function countMembers(): int { $result = $this->mysqli->query("SELECT COUNT(*) AS n FROM stream_members"); return (int)($result->fetch_assoc()['n'] ?? 0); } /** * Admin: change a member's role. */ public function setRole(int $member_id, string $role): bool { $role = whitelist($role, ['member', 'moderator', 'admin'], 'member'); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET role = ? WHERE id = ?" ); $stmt->bind_param('si', $role, $member_id); $result = $stmt->execute(); $stmt->close(); return $result; } /** * Admin: suspend or reactivate a member. */ public function setStatus(int $member_id, string $status): bool { $status = whitelist($status, ['active', 'suspended'], 'active'); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET status = ? WHERE id = ?" ); $stmt->bind_param('si', $status, $member_id); $result = $stmt->execute(); $stmt->close(); return $result; } // ── Finders ─────────────────────────────── public function findById(int $id): ?array { $stmt = $this->mysqli->prepare( "SELECT * FROM stream_members WHERE id = ? LIMIT 1" ); $stmt->bind_param('i', $id); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); return $row ?: null; } public function findByUsername(string $username): ?array { $stmt = $this->mysqli->prepare( "SELECT * FROM stream_members WHERE username = ? LIMIT 1" ); $stmt->bind_param('s', $username); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); return $row ?: null; } public function findByEmail(string $email): ?array { $stmt = $this->mysqli->prepare( "SELECT * FROM stream_members WHERE email = ? LIMIT 1" ); $stmt->bind_param('s', $email); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); return $row ?: null; } // ── Helpers ─────────────────────────────── /** * Strip password_hash before storing in session. */ private function safeArray(array $member): array { unset($member['password_hash'], $member['remember_token'], $member['reset_token']); return $member; } } // ────────────────────────────────────────────── // MemberAuth // Registration, login, sessions, password reset, // and reputation rating. // ────────────────────────────────────────────── class MemberAuth { private $mysqli; // Role hierarchy for permission checks private const ROLES = ['member' => 1, 'moderator' => 2, 'admin' => 3]; public function __construct($mysqli) { $this->mysqli = $mysqli; } // ── Session helpers ─────────────────────── /** * Return the currently logged-in member row, or null. * Re-validates against the DB on first call per request. */ public function currentMember(): ?array { if (!isset($_SESSION['member_id'])) { // Try remember-me cookie return $this->loginFromCookie(); } $id = (int)$_SESSION['member_id']; $stmt = $this->mysqli->prepare( "SELECT id, username, display_name, email, avatar_url, role, rating_score, status FROM stream_members WHERE id = ? AND status = 'active' LIMIT 1" ); $stmt->bind_param('i', $id); $stmt->execute(); $member = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!$member) { // Session member no longer exists / was suspended $this->logout(); return null; } return $member; } public function isLoggedIn(): bool { return $this->currentMember() !== null; } public function hasRole(string $min_role): bool { $member = $this->currentMember(); if (!$member) return false; $member_level = self::ROLES[$member['role']] ?? 0; $required = self::ROLES[$min_role] ?? 99; return $member_level >= $required; } // ── Registration ────────────────────────── /** * Register a new member. * Returns ['ok' => true, 'member_id' => N] on success, * or ['ok' => false, 'error' => '...'] on failure. */ public function register(string $username, string $email, string $password, string $display_name = ''): array { // Validate $username = clean_string($username, 50); $email = trim($email); $display_name = clean_string($display_name ?: $username, 100); if (strlen($username) < 3) { return ['ok' => false, 'error' => 'Username must be at least 3 characters.']; } if (!preg_match('/^[a-zA-Z0-9_.-]+$/', $username)) { return ['ok' => false, 'error' => 'Username may only contain letters, numbers, underscores, hyphens, and dots.']; } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { return ['ok' => false, 'error' => 'Please enter a valid email address.']; } if (strlen($password) < 8) { return ['ok' => false, 'error' => 'Password must be at least 8 characters.']; } // Check uniqueness $stmt = $this->mysqli->prepare( "SELECT id FROM stream_members WHERE username = ? OR email = ? LIMIT 1" ); $stmt->bind_param('ss', $username, $email); $stmt->execute(); if ($stmt->get_result()->fetch_assoc()) { $stmt->close(); return ['ok' => false, 'error' => 'That username or email is already registered.']; } $stmt->close(); // Insert $hash = password_hash($password, PASSWORD_DEFAULT); $stmt = $this->mysqli->prepare( "INSERT INTO stream_members (username, email, password_hash, display_name) VALUES (?, ?, ?, ?)" ); $stmt->bind_param('ssss', $username, $email, $hash, $display_name); if (!$stmt->execute()) { $stmt->close(); return ['ok' => false, 'error' => 'Registration failed. Please try again.']; } $new_id = $stmt->insert_id; $stmt->close(); return ['ok' => true, 'member_id' => $new_id]; } // ── Login / logout ──────────────────────── /** * Attempt login by username or email. * Returns the member row on success, or ['error' => '...'] on failure. */ public function login(string $login, string $password, bool $remember = false): array { $login = trim($login); $stmt = $this->mysqli->prepare( "SELECT * FROM stream_members WHERE (username = ? OR email = ?) AND status != 'suspended' LIMIT 1" ); $stmt->bind_param('ss', $login, $login); $stmt->execute(); $member = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!$member || !password_verify($password, $member['password_hash'])) { return ['error' => 'Invalid username/email or password.']; } // Upgrade hash if needed (e.g. new PHP default algo) if (password_needs_rehash($member['password_hash'], PASSWORD_DEFAULT)) { $new_hash = password_hash($password, PASSWORD_DEFAULT); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET password_hash = ? WHERE id = ?" ); $stmt->bind_param('si', $new_hash, $member['id']); $stmt->execute(); $stmt->close(); } // Stamp last login $id = $member['id']; $stmt = $this->mysqli->prepare( "UPDATE stream_members SET last_login = NOW() WHERE id = ?" ); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->close(); // Start session session_regenerate_id(true); $_SESSION['member_id'] = $member['id']; // Remember-me cookie (30 days) if ($remember) { $token = bin2hex(random_bytes(32)); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET remember_token = ? WHERE id = ?" ); $stmt->bind_param('si', $token, $member['id']); $stmt->execute(); $stmt->close(); setcookie('stream_remember', $member['id'] . ':' . $token, [ 'expires' => time() + 86400 * 30, 'path' => '/', 'secure' => isset($_SERVER['HTTPS']), 'httponly' => true, 'samesite' => 'Lax', ]); } return $member; } public function logout(): void { // Clear remember-me cookie and token if (!empty($_COOKIE['stream_remember'])) { [$member_id, ] = explode(':', $_COOKIE['stream_remember'], 2) + ['', '']; if (is_numeric($member_id)) { $id = (int)$member_id; $stmt = $this->mysqli->prepare( "UPDATE stream_members SET remember_token = NULL WHERE id = ?" ); $stmt->bind_param('i', $id); $stmt->execute(); $stmt->close(); } setcookie('stream_remember', '', time() - 3600, '/'); } unset($_SESSION['member_id']); session_destroy(); } private function loginFromCookie(): ?array { if (empty($_COOKIE['stream_remember'])) return null; $parts = explode(':', $_COOKIE['stream_remember'], 2); if (count($parts) !== 2) return null; [$member_id, $token] = $parts; $member_id = (int)$member_id; if ($member_id <= 0 || !ctype_xdigit($token)) return null; $stmt = $this->mysqli->prepare( "SELECT * FROM stream_members WHERE id = ? AND remember_token = ? AND status = 'active' LIMIT 1" ); $stmt->bind_param('is', $member_id, $token); $stmt->execute(); $member = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!$member) return null; session_regenerate_id(true); $_SESSION['member_id'] = $member['id']; return $member; } // ── Password reset ──────────────────────── /** * Generate a password-reset token for the given email. * Returns the token (caller emails it), or null if email not found. */ public function generateResetToken(string $email): ?string { $email = trim($email); $stmt = $this->mysqli->prepare( "SELECT id FROM stream_members WHERE email = ? AND status = 'active' LIMIT 1" ); $stmt->bind_param('s', $email); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!$row) return null; $token = bin2hex(random_bytes(32)); $expires = date('Y-m-d H:i:s', time() + 3600); // 1-hour window $id = $row['id']; $stmt = $this->mysqli->prepare( "UPDATE stream_members SET reset_token = ?, reset_expires = ? WHERE id = ?" ); $stmt->bind_param('ssi', $token, $expires, $id); $stmt->execute(); $stmt->close(); return $token; } /** * Complete a password reset. * Returns true on success, false if token is invalid/expired. */ public function resetPassword(string $token, string $new_password): bool { if (strlen($new_password) < 8) return false; if (!ctype_xdigit($token)) return false; $now = date('Y-m-d H:i:s'); $stmt = $this->mysqli->prepare( "SELECT id FROM stream_members WHERE reset_token = ? AND reset_expires > ? LIMIT 1" ); $stmt->bind_param('ss', $token, $now); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!$row) return false; $hash = password_hash($new_password, PASSWORD_DEFAULT); $id = $row['id']; $stmt = $this->mysqli->prepare( "UPDATE stream_members SET password_hash = ?, reset_token = NULL, reset_expires = NULL WHERE id = ?" ); $stmt->bind_param('si', $hash, $id); $result = $stmt->execute(); $stmt->close(); return $result; } // ── Reputation / rating ─────────────────── /** * Give +1 or -1 reputation to $rated_id on behalf of $rater_id. * Tied to a specific post so one post = one vote per pair. * Returns ['ok' => true, 'new_score' => N] or ['ok' => false, 'error' => '...']. */ public function ratePost(int $rater_id, int $rated_id, int $post_id, int $points = 1): array { if ($rater_id === $rated_id) { return ['ok' => false, 'error' => "You can't rate your own posts."]; } $points = $points >= 0 ? 1 : -1; // Check for existing vote $stmt = $this->mysqli->prepare( "SELECT id FROM stream_member_ratings WHERE rater_id = ? AND rated_id = ? AND post_id = ?" ); $stmt->bind_param('iii', $rater_id, $rated_id, $post_id); $stmt->execute(); if ($stmt->get_result()->fetch_assoc()) { $stmt->close(); return ['ok' => false, 'error' => "You've already rated this post."]; } $stmt->close(); // Insert vote $stmt = $this->mysqli->prepare( "INSERT INTO stream_member_ratings (rater_id, rated_id, post_id, points) VALUES (?, ?, ?, ?)" ); $stmt->bind_param('iiii', $rater_id, $rated_id, $post_id, $points); if (!$stmt->execute()) { $stmt->close(); return ['ok' => false, 'error' => 'Could not record your rating.']; } $stmt->close(); // Update running total $stmt = $this->mysqli->prepare( "UPDATE stream_members SET rating_score = rating_score + ? WHERE id = ?" ); $stmt->bind_param('ii', $points, $rated_id); $stmt->execute(); $stmt->close(); // Return fresh score $stmt = $this->mysqli->prepare( "SELECT rating_score FROM stream_members WHERE id = ?" ); $stmt->bind_param('i', $rated_id); $stmt->execute(); $score = (int)($stmt->get_result()->fetch_assoc()['rating_score'] ?? 0); $stmt->close(); return ['ok' => true, 'new_score' => $score]; } // ── Profile ─────────────────────────────── public function getMemberById(int $id): ?array { $stmt = $this->mysqli->prepare( "SELECT id, username, display_name, email, bio, avatar_url, role, rating_score, status, created_at, last_login FROM stream_members WHERE id = ? LIMIT 1" ); $stmt->bind_param('i', $id); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); return $row ?: null; } public function updateProfile(int $id, array $data): bool { $display_name = clean_string($data['display_name'] ?? '', 100); $bio = clean_string($data['bio'] ?? '', 500); $avatar_url = clean_url($data['avatar_url'] ?? ''); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET display_name = ?, bio = ?, avatar_url = ? WHERE id = ?" ); $stmt->bind_param('sssi', $display_name, $bio, $avatar_url, $id); $result = $stmt->execute(); $stmt->close(); return $result; } public function changePassword(int $id, string $current_password, string $new_password): array { $stmt = $this->mysqli->prepare( "SELECT password_hash FROM stream_members WHERE id = ?" ); $stmt->bind_param('i', $id); $stmt->execute(); $row = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!$row || !password_verify($current_password, $row['password_hash'])) { return ['ok' => false, 'error' => 'Current password is incorrect.']; } if (strlen($new_password) < 8) { return ['ok' => false, 'error' => 'New password must be at least 8 characters.']; } $hash = password_hash($new_password, PASSWORD_DEFAULT); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET password_hash = ? WHERE id = ?" ); $stmt->bind_param('si', $hash, $id); $stmt->execute(); $stmt->close(); return ['ok' => true]; } // ── Admin helpers ───────────────────────── public function getAllMembers(int $limit = 100, int $offset = 0): array { $members = []; $stmt = $this->mysqli->prepare( "SELECT id, username, display_name, email, role, rating_score, status, created_at, last_login FROM stream_members ORDER BY created_at DESC LIMIT ? OFFSET ?" ); $stmt->bind_param('ii', $limit, $offset); $stmt->execute(); $result = $stmt->get_result(); while ($row = $result->fetch_assoc()) { $members[] = $row; } $stmt->close(); return $members; } public function setMemberRole(int $id, string $role): bool { $role = whitelist($role, ['member', 'moderator', 'admin'], 'member'); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET role = ? WHERE id = ?" ); $stmt->bind_param('si', $role, $id); $result = $stmt->execute(); $stmt->close(); return $result; } public function setMemberStatus(int $id, string $status): bool { $status = whitelist($status, ['active', 'suspended', 'pending'], 'active'); $stmt = $this->mysqli->prepare( "UPDATE stream_members SET status = ? WHERE id = ?" ); $stmt->bind_param('si', $status, $id); $result = $stmt->execute(); $stmt->close(); return $result; } // ── Rating badge helper ─────────────────── /** * Return a short HTML badge showing the member's rating tier and score. * Safe to echo directly into HTML — all values are hardcoded or int-cast. */ public static function ratingBadge(int $score): string { if ($score >= 500) { $tier = 'gold'; $label = 'Gold'; $color = '#b8860b'; } elseif ($score >= 100) { $tier = 'silver'; $label = 'Silver'; $color = '#708090'; } elseif ($score >= 20) { $tier = 'bronze'; $label = 'Bronze'; $color = '#8b4513'; } else { $tier = 'new'; $label = 'New'; $color = '#999'; } return '' . $label . ' • ' . $score . ''; } }