username = $username; $this->password = $password; } public function getUsername(): string { return $this->username; } public function getPassword(): string { return $this->password; } } class User { private int $id; private ?array $spaces; private string $username; private ?DateTime $createdAt; private ?DateTime $lastActive; private bool $isActive; private ?string $profilePicture; private ?string $bio; private ?string $website; private int $role; public function __construct(int $id = -1, ?array $spaces, string $username, ?string $profilePicture = null, ?string $bio = null, ?string $website = null, int $role = 3) { // Basic XSS prevention on construction (can be enhanced) $username = htmlspecialchars($username, ENT_QUOTES, 'UTF-8'); $bio = htmlspecialchars($bio ?? '', ENT_QUOTES, 'UTF-8'); $website = htmlspecialchars($website ?? '', ENT_QUOTES, 'UTF-8'); $profilePicture = htmlspecialchars($profilePicture ?? 'default-profile.png', ENT_QUOTES, 'UTF-8'); // Basic SQL injection prevention (should primarily rely on prepared statements) if (preg_match('/[\'";\-\_]/', $username) || preg_match('/[\'";\-\_]/', $bio) || preg_match('/[\'";\-\_]/', $website) || preg_match('/[\'";\-\_]/', $profilePicture)) { throw new InvalidArgumentException('Invalid characters in user data.'); } $this->username = $username; $this->profilePicture = $profilePicture; $this->bio = $bio; $this->website = $website; $this->createdAt = new DateTime(); $this->lastActive = null; $this->isActive = false; $this->role = $role; $this->spaces = null; if ($id == -1) { $this->id = randomId(0); $stmt = 'INSERT INTO user (userId, username, profilePicture, bio, website) VALUES (?, ?, ?, ?, ?)'; exec_stmt($stmt, 'issss', $this->id, $this->username, $this->profilePicture, $this->bio, $this->website); } else $this->id = $id; } public static function exists(int $id): bool { $stmt = 'SELECT count(id) FROM user WHERE id = ?'; $result = exec_stmt($stmt, 'i', $id)->fetch_assoc(); if ($result['count(id)'] == 1) return true; else if ($result['count(id)'] > 1) return true; else return false; } public static function retrieveFromDB(int $userId): ?User { $stmt = 'SELECT id, username, profilePicture, bio, website FROM user WHERE id = ?'; $user = exec_stmt($stmt, 'i', $userId)->fetch_assoc(); if ($user) { return new User($user['id'], null, $user['username'], $user['profilePicture'], $user['bio'], $user['website']); } else { return null; } } public static function getSignupHTML() { $html = '

Sign Up

No file selected.
'; return $html; } public static function getLoginHTML() { $html = '

Log In

'; return $html; } public static function login(LoginCredentials $credentials): ?User { $stmt = 'SELECT id, username, email, passwordHash, role, profilePicture, bio, website FROM user WHERE username = ?'; $user = exec_stmt($stmt, 's', $credentials->getUsername())->fetch_assoc(); if (!password_verify($credentials->getPassword(), $user['passwordHash'])) return null; else return new User($user['id'], null, $user['username'], $user['profilePicture'], $user['bio'], $user['website']); } public static function logout(): void { foreach (array_keys($_SESSION) as $key) { unset($_SESSION[$key]); } // Consider destroying the session cookie as well: session_destroy(); header('Location: .'); // Redirect to the homepage or login page exit(); } public function getHomeSpace() { // TODO: Need to implement. Requires fleshing out additional space types. return Space::retrieveFromDB(1, 1); } public function getSpaces() { $stmt = 'SELECT * FROM spaceMembers WHERE user = ?'; $spaces = []; $rawSpaces = exec_stmt($stmt, 'i', $this->id); while ($row = $rawSpaces->fetch_assoc()) { $spaces[] = Space::retrieveFromDB($row['space'], $row['user']); } $this->spaces = $spaces; return $spaces; } public function getRole(): int { return $this->role; } public function getId(): int { return $this->id; } public function getUsername(): string { return $this->username; } public function getCreatedAt(): ?DateTime { return $this->createdAt; } public function getLastActive(): ?DateTime { return $this->lastActive; } public function isActive(): bool { return $this->isActive; } public function getProfilePicture(): ?string { return $this->profilePicture; } public function getBio(): ?string { return $this->bio; } public function getWebsite(): ?string { return $this->website; } public function setLastActive(?DateTime $lastActive): void { $this->lastActive = $lastActive; } public function setIsActive(bool $isActive): void { $this->isActive = $isActive; } public function toggleActive(?mysqli $db): bool { if (!$db) { error_log('Database connection not provided for toggleActive.'); return false; } $this->isActive = !$this->isActive; $stmt = $db->prepare('UPDATE user SET is_active = ? WHERE user_id = ?'); if ($stmt) { $stmt->bind_param('ii', (int) $this->isActive, $this->id); $result = $stmt->execute(); $stmt->close(); return $result; } else { error_log('Error preparing statement: ' . $db->error); return false; } } public function updateUsername(string $newUsername, ?mysqli $db): bool { $newUsername = htmlspecialchars($newUsername, ENT_QUOTES, 'UTF-8'); if (preg_match('/[\'";\-\_]/', $newUsername)) { throw new InvalidArgumentException('Invalid characters in username.'); } if (!$db) { error_log('Database connection not provided for updateUsername.'); return false; } $stmt = $db->prepare('UPDATE user SET username = ? WHERE user_id = ?'); if ($stmt) { $stmt->bind_param('si', $newUsername, $this->id); $result = $stmt->execute(); $stmt->close(); if ($result) { $this->username = $newUsername; return true; } } else { error_log('Error preparing statement: ' . $db->error); } return false; } public function updatePassword(string $newPassword, ?mysqli $db): bool { $newPasswordHash = hash('sha256', $newPassword); if (!$db) { error_log('Database connection not provided for updatePassword.'); return false; } $stmt = $db->prepare('UPDATE user SET password_hash = ? WHERE user_id = ?'); if ($stmt) { $stmt->bind_param('si', $newPasswordHash, $this->id); $result = $stmt->execute(); $stmt->close(); return $result; } else { error_log('Error preparing statement: ' . $db->error); } return false; } public function updateProfilePicture(?string $newProfilePicture, ?mysqli $db): bool { $newProfilePicture = htmlspecialchars($newProfilePicture ?? 'default-profile.png', ENT_QUOTES, 'UTF-8'); if (preg_match('/[\'";\-\_]/', $newProfilePicture)) { throw new InvalidArgumentException('Invalid characters in profile picture filename.'); } if (!$db) { error_log('Database connection not provided for updateProfilePicture.'); return false; } $stmt = $db->prepare('UPDATE user SET profile_picture = ? WHERE user_id = ?'); if ($stmt) { $stmt->bind_param('si', $newProfilePicture, $this->id); $result = $stmt->execute(); $stmt->close(); if ($result) { $this->profilePicture = $newProfilePicture; return true; } } else { error_log('Error preparing statement: ' . $db->error); } return false; } public function followUser(int $followingUserId, bool $notify = false, ?mysqli $db): bool { if (!$db) { error_log('Database connection not provided for followUser.'); return false; } $stmt = $db->prepare('INSERT INTO follows (follower_user_id, following_user_id, notify_user) VALUES (?, ?, ?)'); if ($stmt) { $stmt->bind_param('iii', $this->id, $followingUserId, (int) $notify); $result = $stmt->execute(); $stmt->close(); return $result; } else { error_log('Error preparing statement: ' . $db->error); return false; } } public function getFollowers(?mysqli $db): array { if (!$db) { error_log('Database connection not provided for getFollowers.'); return []; } $stmt = $db->prepare('SELECT u.user_id, u.username, u.profile_picture, u.bio, u.website FROM follows f JOIN user u ON f.follower_user_id = u.user_id WHERE f.following_user_id = ?'); if ($stmt) { $stmt->bind_param('i', $this->id); $stmt->execute(); $result = $stmt->get_result(); $followers = []; while ($row = $result->fetch_assoc()) { $followers[] = new User( (int) $row['user_id'], $row['username'], $row['profile_picture'], $row['bio'], $row['website'] ); } $stmt->close(); return $followers; } else { error_log('Error preparing statement: ' . $db->error); return []; } } public function getFollowing(?mysqli $db): array { if (!$db) { error_log('Database connection not provided for getFollowing.'); return []; } $stmt = $db->prepare('SELECT u.user_id, u.username, u.profile_picture, u.bio, u.website FROM follows f JOIN user u ON f.following_user_id = u.user_id WHERE f.follower_user_id = ?'); if ($stmt) { $stmt->bind_param('i', $this->id); $stmt->execute(); $result = $stmt->get_result(); $following = []; while ($row = $result->fetch_assoc()) { $following[] = new User( (int) $row['user_id'], $row['username'], $row['profile_picture'], $row['bio'], $row['website'] ); } $stmt->close(); return $following; } else { error_log('Error preparing statement: ' . $db->error); return []; } } } class Creator extends User {}