diff --git a/assets/profile_pics/head_emerald.png b/assets/profile_pics/head_emerald.png new file mode 100644 index 0000000..20cc767 Binary files /dev/null and b/assets/profile_pics/head_emerald.png differ diff --git a/classes/CDN.php b/classes/CDN.php index b316d2e..b374553 100644 --- a/classes/CDN.php +++ b/classes/CDN.php @@ -32,6 +32,9 @@ class CDN ]); } + /** + * @throws \Exception + */ public function uploadFile(string $localPath, string $remotePath, string $mimeType, string $acl = 'private') { try { @@ -48,6 +51,9 @@ class CDN } } + /** + * @throws \Exception + */ public function renameFile(string $oldRemotePath, string $newRemotePath) { // S3 does not support renaming directly. Copy then delete. diff --git a/classes/DJs.php b/classes/DJs.php index ad8a677..8f888dd 100644 --- a/classes/DJs.php +++ b/classes/DJs.php @@ -49,4 +49,18 @@ class DJs $stmt->close(); return $djs; } + + public function search(string $query, int $page = 1, int $resultsPerPage = 10): array { + $offset = ($page - 1) * $resultsPerPage; + $likeQuery = "%" . $query . "%"; + $stmt = $this->db->prepare("SELECT * FROM djs WHERE name LIKE ? OR bio LIKE ? LIMIT ?, ?"); + $stmt->bind_param("ssii", $likeQuery, $likeQuery, $offset, $resultsPerPage); + $stmt->execute(); + $result = $stmt->get_result(); + $djs = $result->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + return $djs; + } + + } diff --git a/classes/DownloadMix.php b/classes/DownloadMix.php index 894e70e..ca241b5 100644 --- a/classes/DownloadMix.php +++ b/classes/DownloadMix.php @@ -4,32 +4,32 @@ namespace DJMixHosting; class DownloadMix { - private $db; - private $mix; - private $ready = false; - private $name; - private $djs; - private $filename; - private $url; - private $mix_id; + private Database $db; + private Mix $mix; + private bool $ready = false; + private string $name; + private string $djs; + private string $filename; + private string $url; + private int $mix_id; private $content; - private $filesize = 0; - private $ext; + private int $filesize = 0; + private string $ext; public function __construct($mix, $db) { $this->db = $db; $this->mix = $mix; - $this->mix_id = $mix->get_id(); + $this->mix_id = $mix->getId(); $this->preDownload(); } - private function preDownload() + private function preDownload(): void { - $this->name = $this->mix->get_name(); - $buildDJs = $this->mix->get_djs(); - $this->url = $this->mix->get_url(); + $this->name = $this->mix->getName(); + $buildDJs = $this->mix->getDJs(); + $this->url = $this->mix->getUrl(); $this->djs = ''; $djCount = 0; foreach ($buildDJs as $dj) { @@ -42,7 +42,7 @@ class DownloadMix } - public function download() + public function download(): void { $this->loadDownload(); if (!$this->ready) { @@ -56,23 +56,24 @@ class DownloadMix } header("Content-Description: File Transfer"); header("Content-Type: application/octet-stream"); + header("Content-Length: " . $this->filesize); header("Content-Disposition: attachment; filename=\"" . $this->filename . "\""); echo $this->content; } } - private function loadDownload() + private function loadDownload(): void { $this->content = file_get_contents($this->url); $this->filesize = strlen($this->content); $this->ext = pathinfo(basename($this->url), PATHINFO_EXTENSION); - $this->filename = $this->djs . ' - ' . $this->name . ' (Downloaded from UtahsDJs.com).' . pathinfo(basename($this->url), PATHINFO_EXTENSION); + $this->filename = $this->djs . ' - ' . $this->name . ' (Downloaded from UtahsDJs.com).' . $this->ext; if ($this->filesize > 0) { $this->ready = true; } } - private function checkForMixDownloadCount() + private function checkForMixDownloadCount(): bool { $stmt = $this->db->prepare("SELECT * FROM mix_meta WHERE attribute = 'downloads' and mix_id = ?"); $stmt->bind_param('i', $this->mix_id); @@ -87,7 +88,7 @@ class DownloadMix } } - private function incrementMixDownloadCount() + private function incrementMixDownloadCount(): void { $stmt = $this->db->prepare("UPDATE mix_meta SET value = value + 1 WHERE attribute = 'downloads' and mix_id = ?"); $stmt->bind_param('i', $this->mix_id); @@ -95,7 +96,7 @@ class DownloadMix $stmt->close(); } - private function addMixDownloadCount() + private function addMixDownloadCount(): void { $stmt = $this->db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'downloads', 1)"); $stmt->bind_param('i', $this->mix_id); @@ -103,4 +104,9 @@ class DownloadMix $stmt->close(); } + public function getExt(): string + { + return $this->ext; + } + } \ No newline at end of file diff --git a/classes/Genres.php b/classes/Genres.php index 43c274d..691a811 100644 --- a/classes/Genres.php +++ b/classes/Genres.php @@ -52,4 +52,17 @@ class Genres $stmt->close(); return $genres; } + + public function search(string $query, int $page = 1, int $resultsPerPage = 10): array { + $offset = ($page - 1) * $resultsPerPage; + $likeQuery = "%" . $query . "%"; + $stmt = $this->db->prepare("SELECT * FROM genres WHERE name LIKE ? LIMIT ?, ?"); + $stmt->bind_param("sii", $likeQuery, $offset, $resultsPerPage); + $stmt->execute(); + $result = $stmt->get_result(); + $genres = $result->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + return $genres; + } + } diff --git a/classes/Mixes.php b/classes/Mixes.php new file mode 100644 index 0000000..add3211 --- /dev/null +++ b/classes/Mixes.php @@ -0,0 +1,73 @@ +db = $db; + // Automatically load all mixes upon instantiation. + if (!$this->load_all_mixes()) { + // Optionally, handle errors or fallback logic here. + return false; + } + return true; + } + + /** + * Load all mixes from the database. + * + * @return bool + */ + private function load_all_mixes(): bool + { + $mixes = $this->get_all_mixes(); + if ($mixes) { + $this->mixes = $mixes; + return true; + } + return false; + } + + /** + * Retrieve all mixes. + * + * @param string $order The sort order (ASC or DESC). + * @return array + */ + public function get_all_mixes(string $order = "ASC"): array + { + // Assuming your mix table has a column called "name" + $stmt = $this->db->prepare("SELECT * FROM mix ORDER BY title $order"); + $stmt->execute(); + $result = $stmt->get_result(); + $mixes = $result->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + return $mixes; + } + + /** + * Search mixes by name and description. + * + * @param string $query The search keyword. + * @param int $page The current page number. + * @param int $resultsPerPage The number of results per page. + * @return array + */ + public function search(string $query, int $page = 1, int $resultsPerPage = 10): array + { + $offset = ($page - 1) * $resultsPerPage; + $likeQuery = "%" . $query . "%"; + // Adjust the SQL if your mix table uses different column names (e.g., title instead of name) + $stmt = $this->db->prepare("SELECT * FROM mix WHERE title LIKE ? OR description LIKE ? LIMIT ?, ?"); + $stmt->bind_param("ssii", $likeQuery, $likeQuery, $offset, $resultsPerPage); + $stmt->execute(); + $result = $stmt->get_result(); + $mixes = $result->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + return $mixes; + } +} diff --git a/classes/Mixshows.php b/classes/Mixshows.php index 53bfd24..24c0e97 100644 --- a/classes/Mixshows.php +++ b/classes/Mixshows.php @@ -52,4 +52,17 @@ class Mixshows $stmt->close(); return $mixshows; } + + public function search(string $query, int $page = 1, int $resultsPerPage = 10): array { + $offset = ($page - 1) * $resultsPerPage; + $likeQuery = "%" . $query . "%"; + $stmt = $this->db->prepare("SELECT * FROM shows WHERE name LIKE ? OR description LIKE ? LIMIT ?, ?"); + $stmt->bind_param("ssii", $likeQuery, $likeQuery, $offset, $resultsPerPage); + $stmt->execute(); + $result = $stmt->get_result(); + $mixshows = $result->fetch_all(MYSQLI_ASSOC); + $stmt->close(); + return $mixshows; + } + } diff --git a/classes/RSS.php b/classes/RSS.php index e1e572e..8b00bd1 100644 --- a/classes/RSS.php +++ b/classes/RSS.php @@ -1,36 +1,64 @@ '; - private string $rss = ''; - - - private function itemMix($mix) - { - $output = new Mix($mix, $this->db); - - if ($output->get_recorded() != "") { - $pubdate = date('D, d M Y H:i:s O', strtotime($output->get_recorded())); - } elseif ($output->get_created() != "") { - $pubdate = date('D, d M Y H:i:s O', strtotime($output->get_created())); - } else { - $pubdate = date('D, d M Y H:i:s O', strtotime('2008-01-01 12:00:00')); - } - - echo ''; - echo '' . $output->get_name() . ''; - echo '' . $output->get_description() . ''; - echo '' . $output->get_url() . ''; - echo '' . $output->get_slug() . ''; - echo '' . $pubdate . ''; - echo ''; + public function __construct(string $title, string $link, string $description) { + $this->channelTitle = $title; + $this->channelLink = $link; + $this->channelDescription = $description; } -} \ No newline at end of file + /** + * Add an item to the RSS feed. + * + * @param string $title Item title. + * @param string $description Item description. + * @param string $link Item URL. + * @param string $pubDate A date/time string (accepted by strtotime). + */ + public function addItem(string $title, string $description, string $link, string $pubDate): void { + $this->items[] = [ + 'title' => htmlspecialchars($title), + 'description' => htmlspecialchars($description), + 'link' => $link, + 'guid' => $link, + 'pubDate' => date($this->pubDateFormat, strtotime($pubDate)) + ]; + } + + /** + * Generate the complete RSS XML. + * + * @return string The RSS XML string. + */ + public function generateXML(): string { + $xml = '' . "\n"; + $xml .= '' . "\n"; + $xml .= " \n"; + $xml .= " {$this->channelTitle}\n"; + $xml .= " {$this->channelLink}\n"; + $xml .= " {$this->channelDescription}\n"; + $xml .= " " . date($this->pubDateFormat) . "\n"; + // Optionally add additional channel tags here + + foreach ($this->items as $item) { + $xml .= " \n"; + $xml .= " {$item['title']}\n"; + $xml .= " {$item['description']}\n"; + $xml .= " {$item['link']}\n"; + $xml .= " {$item['guid']}\n"; + $xml .= " {$item['pubDate']}\n"; + $xml .= " \n"; + } + + $xml .= " \n"; + $xml .= ""; + return $xml; + } +} diff --git a/classes/User.php b/classes/User.php index 048d621..bd6f7c3 100644 --- a/classes/User.php +++ b/classes/User.php @@ -2,29 +2,27 @@ namespace DJMixHosting; +use DateMalformedStringException; use DateTime; use Exception; use Random\RandomException; -use Aws\Ses\SesClient; -use Aws\Exception\AwsException; class User { - private $db; - private $id; - private $username; - private $firstName; - private $lastName; - private $email; - private $location; - private $bio; - private $created; - private $updated; - private $verified; - private $role; - - private $img = ""; - private $api_key; + private Database $db; + private string $id; + private string $username; + private string $firstName; + private string $lastName; + private string $email; + private string $location; + private string $bio; + private string $created; + private string $updated; + private bool $verified; + private string $role; + private string $img = ""; + private string $api_key; public function __construct($db, $id = null) { $this->db = $db; @@ -36,7 +34,8 @@ class User { /** * Load user data from the database by id. */ - private function loadUserById($id) { + private function loadUserById($id): void + { $stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?"); $stmt->bind_param("i", $id); $stmt->execute(); @@ -56,9 +55,9 @@ class User { $this->updated = $user_data['lastupdated']; $this->role = $user_data['isAdmin'] ? 'admin' : 'user'; - // These fields are not in your table; assign defaults or remove them. - $this->location = ""; - $this->bio = ""; + // New fields loaded from the database + $this->location = $user_data['location'] ?? ""; + $this->bio = $user_data['bio'] ?? ""; } } @@ -69,7 +68,7 @@ class User { */ public function newUser(string $username, string $password, string $email, string $firstName, string $lastName): int { if ($this->check_existing_user($username, $email)) { - throw new \Random\RandomException("User already exists"); + throw new RandomException("User already exists"); } $this->username = $username; $this->email = $email; @@ -87,8 +86,8 @@ class User { $this->img = ""; $this->api_key = bin2hex(random_bytes(32)); - $stmt = $this->db->prepare("INSERT INTO users (username, password, email, firstName, lastName, img, emailVerified) VALUES (?, ?, ?, ?, ?, '', 0)"); - $stmt->bind_param("sssss", $this->username, $password_hashed, $this->email, $this->firstName, $this->lastName); + $stmt = $this->db->prepare("INSERT INTO users (username, password, email, firstName, lastName, img, emailVerified, apiKey, location, bio) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)"); + $stmt->bind_param("sssssssss", $this->username, $password_hashed, $this->email, $this->firstName, $this->lastName, $this->img, $this->api_key, $this->location, $this->bio); $stmt->execute(); $userId = $stmt->insert_id; $stmt->close(); @@ -97,8 +96,8 @@ class User { return $userId; } - - private function check_existing_user($username, $email) { + private function check_existing_user($username, $email): false|array|null + { $stmt = $this->db->prepare("SELECT * FROM users WHERE username = ? OR email = ?"); $stmt->bind_param("ss", $username, $email); $stmt->execute(); @@ -113,8 +112,10 @@ class User { * * Returns the user data array if successful. In case of failure, * a string error message is returned. + * @throws DateMalformedStringException */ - public function login($email, $password) { + public function login($email, $password): array|string + { // Retrieve user record by email $stmt = $this->db->prepare("SELECT * FROM users WHERE email = ?"); $stmt->bind_param("s", $email); @@ -161,6 +162,7 @@ class User { * Update (or create) a record in the login_attempts table for a failed attempt. * If attempts reach 3, set a lockout that doubles each time. * Returns the current number of attempts. + * @throws DateMalformedStringException */ private function updateFailedAttempt($email) { // Check for an existing record @@ -187,31 +189,27 @@ class User { $stmt = $this->db->prepare("UPDATE login_attempts SET attempts = ?, lockouts = ?, last_attempt = NOW(), lockout_until = ? WHERE email = ?"); $lockout_until_str = $lockout_until->format('Y-m-d H:i:s'); $stmt->bind_param("iiss", $attempts, $lockouts, $lockout_until_str, $email); - $stmt->execute(); - $stmt->close(); } else { $stmt = $this->db->prepare("UPDATE login_attempts SET attempts = ?, last_attempt = NOW() WHERE email = ?"); $stmt->bind_param("is", $attempts, $email); - $stmt->execute(); - $stmt->close(); } - return $attempts; } else { // Create a new record for this email $attempts = 1; $lockouts = 0; $stmt = $this->db->prepare("INSERT INTO login_attempts (email, attempts, lockouts, last_attempt) VALUES (?, ?, ?, NOW())"); $stmt->bind_param("sii", $email, $attempts, $lockouts); - $stmt->execute(); - $stmt->close(); - return $attempts; } + $stmt->execute(); + $stmt->close(); + return $attempts; } /** * Reset the login_attempts record for the given email. */ - private function resetLoginAttempts($email) { + private function resetLoginAttempts($email): void + { $stmt = $this->db->prepare("DELETE FROM login_attempts WHERE email = ?"); $stmt->bind_param("s", $email); $stmt->execute(); @@ -224,12 +222,12 @@ class User { * @param string $newEmail * @param array $config Configuration array for AWS SES and app settings. * @return string Success message. - * @throws \Exception on validation or email-sending failure. + * @throws Exception on validation or email-sending failure. */ public function updateEmail(string $newEmail, array $config): string { $newEmail = filter_var($newEmail, FILTER_VALIDATE_EMAIL); if (!$newEmail) { - throw new \Exception("Invalid email format."); + throw new Exception("Invalid email format."); } // Update email and mark as unverified. @@ -258,7 +256,11 @@ class User { return "Email updated. A verification email has been sent to your new address."; } - public function updateName($firstName, $lastName) { + /** + * @throws Exception + */ + public function updateName($firstName, $lastName): string + { // Update the user's name. $stmt = $this->db->prepare("UPDATE users SET firstName = ?, lastName = ? WHERE id = ?"); $stmt->bind_param("ssi", $firstName, $lastName, $this->id); @@ -274,7 +276,11 @@ class User { return "Name updated successfully."; } - public function updatePassword($currentPassword, $newPassword, $confirmPassword) { + /** + * @throws Exception + */ + public function updatePassword($currentPassword, $newPassword, $confirmPassword): string + { // Retrieve the current password hash. $stmt = $this->db->prepare("SELECT password FROM users WHERE id = ?"); $stmt->bind_param("i", $this->id); @@ -307,7 +313,11 @@ class User { return "Password updated successfully."; } - public function updateUsername($newUsername) { + /** + * @throws Exception + */ + public function updateUsername($newUsername): string + { // Validate username format. if (!preg_match('/^[a-zA-Z0-9_]{3,25}$/', $newUsername)) { throw new Exception("Invalid username format."); @@ -339,7 +349,7 @@ class User { * * @param string $verification_code The code submitted by the user. * @return string Success message. - * @throws \Exception If the code is invalid or expired. + * @throws Exception If the code is invalid or expired. */ public function verifyEmail(string $verification_code): string { // Look up the verification record for this user and code @@ -351,14 +361,14 @@ class User { $stmt->close(); if (!$record) { - throw new \Exception("Invalid verification code."); + throw new Exception("Invalid verification code."); } // Check if the verification code has expired - $current_time = new \DateTime(); - $expires_at = new \DateTime($record['expires_at']); + $current_time = new DateTime(); + $expires_at = new DateTime($record['expires_at']); if ($current_time > $expires_at) { - throw new \Exception("Verification code has expired. Please request a new one."); + throw new Exception("Verification code has expired. Please request a new one."); } // Update the user's record to mark the email as verified @@ -379,18 +389,50 @@ class User { return "Email verified successfully."; } + // New setters for location and bio + + /** + * @throws Exception + */ + public function setLocation(string $location): string { + $stmt = $this->db->prepare("UPDATE users SET location = ? WHERE id = ?"); + $stmt->bind_param("si", $location, $this->id); + if ($stmt->execute()) { + $this->location = $location; + $stmt->close(); + return "Location updated successfully."; + } + $stmt->close(); + throw new Exception("Failed to update location."); + } + + /** + * @throws Exception + */ + public function setBio(string $bio): string { + $stmt = $this->db->prepare("UPDATE users SET bio = ? WHERE id = ?"); + $stmt->bind_param("si", $bio, $this->id); + if ($stmt->execute()) { + $this->bio = $bio; + $stmt->close(); + return "Bio updated successfully."; + } + $stmt->close(); + throw new Exception("Failed to update bio."); + } + // Getter methods - public function getId() { return $this->id; } - public function getUsername() { return $this->username; } - public function getFirstName() { return $this->firstName; } - public function getLastName() { return $this->lastName; } - public function getEmail() { return $this->email; } - public function getLocation() { return $this->location; } - public function getBio() { return $this->bio; } - public function getCreated() { return $this->created; } - public function getUpdated() { return $this->updated; } - public function getVerified() { return $this->verified; } - public function getRole() { return $this->role; } - public function getImg() { return $this->img; } - public function getApiKey() { return $this->api_key; } + public function getId(): string { return $this->id; } + public function getUsername(): string { return $this->username; } + public function getFirstName(): string { return $this->firstName; } + public function getLastName(): string { return $this->lastName; } + public function getEmail(): string { return $this->email; } + public function getLocation(): string { return $this->location; } + public function getBio(): string { return $this->bio; } + public function getCreated(): string { return $this->created; } + public function getUpdated(): string { return $this->updated; } + public function getVerified(): string { return $this->verified; } + public function getRole(): string { return $this->role; } + public function getImg(): string { return $this->img; } + public function getApiKey(): string { return $this->api_key; } } diff --git a/composer.json b/composer.json index 97366ec..94aede3 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,8 @@ "php": ">=8.2.0", "phpunit/phpunit": ">=11", "yosymfony/toml": "*", - "ext-zip": "*" + "ext-zip": "*", + "ext-iconv": "*" }, "autoload": { "psr-4": { diff --git a/contact.php b/contact.php index 42f0cfc..43426fc 100644 --- a/contact.php +++ b/contact.php @@ -72,7 +72,7 @@ require_once 'includes/header.php';
- +
@@ -96,29 +96,29 @@ require_once 'includes/header.php';

-

We'd love to hear from you. Send us a message and we'll respond as soon as possible.

+

-
+
-

Send us a Message

+

- +
- +
- +
- +
diff --git a/dj.php b/dj.php index 2969b61..87886f6 100644 --- a/dj.php +++ b/dj.php @@ -138,13 +138,15 @@ require_once 'includes/header.php'; $mixes = $dj->getDJMixes(); if (!empty($mixes)) { // Add header row for the table-like layout - echo '
'; - echo '
' . $locale['mixName'] . '
'; - echo '
' . $locale['genres'] . '
'; - echo '
' . $locale['duration'] . '
'; - echo '
' . $locale['year'] . '
'; - echo '
'; - echo '
'; // Optional horizontal rule for separation + ?> +
+
+
+
+
+
+
+ document.addEventListener('DOMContentLoaded', function () { - var shareBtn = document.getElementById('shareBtn'); - var shareModal = new bootstrap.Modal(document.getElementById('shareModal')); - var copyLinkBtn = document.getElementById("copyLinkBtn"); - var urlToCopy = window.location.href + '?utm_source=website&utm_medium=share_modal&utm_campaign=sharing'; + const shareBtn = document.getElementById('shareBtn'); + const shareModal = new bootstrap.Modal(document.getElementById('shareModal')); + const copyLinkBtn = document.getElementById("copyLinkBtn"); + const urlToCopy = window.location.href + '?utm_source=website&utm_medium=share_modal&utm_campaign=sharing'; shareBtn.addEventListener('click', function () { shareModal.show(); diff --git a/forgot-password.php b/forgot-password.php index 71299ad..5de3e1f 100644 --- a/forgot-password.php +++ b/forgot-password.php @@ -7,81 +7,217 @@ use DJMixHosting\Database; use Aws\Ses\SesClient; use Aws\Exception\AwsException; -if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['email'])) { - $email = trim($_POST['email']); - if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { - $_SESSION['error'] = "Invalid email format."; +$db = new Database($config); + +// Ensure a CSRF token exists. +if (empty($_SESSION['csrf_token'])) { + $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); +} + +// Determine if we are in reset stage based on GET parameter. +$isResetStage = false; +$verification_code = ""; +if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['code'])) { + $verification_code = trim($_GET['code']); + $isResetStage = true; +} + +// Process POST submissions. +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + + // Validate the CSRF token. + if (!hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'] ?? '')) { + $_SESSION['error'] = "Invalid CSRF token."; header("Location: forgot-password.php"); exit; } - $db = new Database($config); - // Check if email exists in the system - $stmt = $db->prepare("SELECT id, username FROM users WHERE email = ?"); - $stmt->bind_param("s", $email); - $stmt->execute(); - $result = $stmt->get_result(); - $userData = $result->fetch_assoc(); - $stmt->close(); + // If a verification code is provided, we are in reset mode. + if (isset($_POST['verification_code']) && !empty($_POST['verification_code'])) { - // Always show a success message (to avoid disclosing which emails are registered) - $_SESSION['success'] = "If the email exists in our system, a password reset link has been sent."; + // Rate limiting for reset attempts. + if (!isset($_SESSION['attempts'])) { + $_SESSION['attempts'] = 0; + $_SESSION['first_attempt_time'] = time(); + } + $_SESSION['attempts']++; + if ($_SESSION['attempts'] > 5 && (time() - $_SESSION['first_attempt_time']) < 900) { // 15 minutes + $_SESSION['error'] = "Too many attempts. Please try again later."; + header("Location: forgot-password.php?code=" . urlencode($_POST['verification_code'])); + exit; + } + if (time() - $_SESSION['first_attempt_time'] >= 900) { + $_SESSION['attempts'] = 1; + $_SESSION['first_attempt_time'] = time(); + } - if ($userData) { - $user_id = $userData['id']; - // Generate a password reset verification code valid for 15 minutes - $verification_code = bin2hex(random_bytes(16)); - $expires_at = date("Y-m-d H:i:s", strtotime("+15 minutes")); + // Process the reset. + $verification_code = trim($_POST['verification_code']); + $username = trim($_POST['username'] ?? ''); + $new_password = $_POST['new_password'] ?? ''; + $confirm_password = $_POST['confirm_password'] ?? ''; - // Insert a record with purpose 'password_reset' - $stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at, purpose) VALUES (?, ?, ?, ?, 'password_reset')"); - $stmt->bind_param("isss", $user_id, $email, $verification_code, $expires_at); + if (empty($verification_code) || empty($username) || empty($new_password) || empty($confirm_password)) { + $_SESSION['error'] = $locale['allFieldsRequired']; + header("Location: forgot-password.php?code=" . urlencode($verification_code)); + exit; + } + if ($new_password !== $confirm_password) { + $_SESSION['error'] = $locale['passwordMismatch']; + header("Location: forgot-password.php?code=" . urlencode($verification_code)); + exit; + } + if (!validate_password($new_password)) { + $_SESSION['error'] = $locale['passwordRequirements']; + header("Location: forgot-password.php?code=" . urlencode($verification_code)); + exit; + } + + // Look up the password reset record. + $stmt = $db->prepare("SELECT * FROM email_verifications WHERE verification_code = ? AND purpose = 'password_reset'"); + $stmt->bind_param("s", $verification_code); + $stmt->execute(); + $result = $stmt->get_result(); + $record = $result->fetch_assoc(); + $stmt->close(); + + if (!$record) { + $_SESSION['error'] = $locale['resetExpiredInvalid']; + header("Location: forgot-password.php?code=" . urlencode($verification_code)); + exit; + } + + // Check expiration. + $current_time = new DateTime(); + $expires_at = new DateTime($record['expires_at']); + if ($current_time > $expires_at) { + $_SESSION['error'] = $locale['resetExpired']; + header("Location: forgot-password.php?code=" . urlencode($verification_code)); + exit; + } + + // Verify the username matches the record. + $stmt = $db->prepare("SELECT id, username FROM users WHERE id = ? AND username = ?"); + $stmt->bind_param("is", $record['user_id'], $username); + $stmt->execute(); + $userData = $stmt->get_result()->fetch_assoc(); + $stmt->close(); + + if (!$userData) { + $_SESSION['error'] = $locale['codeCredsInvalid']; + header("Location: forgot-password.php?code=" . urlencode($verification_code)); + exit; + } + + // Update the user's password. + $hashed_password = password_hash($new_password, PASSWORD_DEFAULT); + $stmt = $db->prepare("UPDATE users SET password = ? WHERE id = ?"); + $stmt->bind_param("si", $hashed_password, $userData['id']); $stmt->execute(); $stmt->close(); - // Send password reset email via AWS SES - $sesClient = new SesClient([ - 'version' => 'latest', - 'region' => $config['aws']['ses']['region'], - 'credentials' => [ - 'key' => $config['aws']['ses']['access_key'], - 'secret' => $config['aws']['ses']['secret_key'], - ] - ]); + // Remove the password reset record. + $stmt = $db->prepare("DELETE FROM email_verifications WHERE verification_code = ? AND purpose = 'password_reset'"); + $stmt->bind_param("s", $verification_code); + $stmt->execute(); + $stmt->close(); - $sender_email = $config['aws']['ses']['sender_email']; - $recipient_email = $email; - $subject = "Password Reset Request"; - $reset_link = $config['app']['url'] . "/password-reset.php?code={$verification_code}"; - $body_text = "You have requested to reset your password. Please click the link below to reset your password:\n\n"; - $body_text .= "{$reset_link}\n\nIf you did not request this, please ignore this email. This link will expire in 15 minutes."; + session_regenerate_id(true); + $_SESSION['success'] = $locale['passwordResetSuccess']; + header("Location: /login"); + exit; - try { - $result = $sesClient->sendEmail([ - 'Destination' => [ - 'ToAddresses' => [$recipient_email], - ], - 'ReplyToAddresses' => [$sender_email], - 'Source' => $sender_email, - 'Message' => [ - 'Body' => [ - 'Text' => [ + } else { + // Otherwise, we are processing a forgot password request. + $email = trim($_POST['email'] ?? ''); + if (empty($email)) { + $_SESSION['error'] = $locale['enterEmailAddressPlease']; + header("Location: /forgot-password"); + exit; + } + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + $_SESSION['error'] = $locale['emailInvalid']; + header("Location: /forgot-password"); + exit; + } + + // Check if email exists in the system. + $stmt = $db->prepare("SELECT id, username FROM users WHERE email = ?"); + $stmt->bind_param("s", $email); + $stmt->execute(); + $result = $stmt->get_result(); + $userData = $result->fetch_assoc(); + $stmt->close(); + + // Always display a success message (even if the email isn’t registered) to avoid disclosing registered emails. + $_SESSION['success'] = $locale['passwordResetSent']; + + if ($userData) { + $user_id = $userData['id']; + // Generate a password reset verification code valid for 15 minutes. + $verification_code = bin2hex(random_bytes(16)); + $expires_at = date("Y-m-d H:i:s", strtotime("+15 minutes")); + + // Insert a record for the password reset. + $stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at, purpose) VALUES (?, ?, ?, ?, 'password_reset')"); + $stmt->bind_param("isss", $user_id, $email, $verification_code, $expires_at); + $stmt->execute(); + $stmt->close(); + + // Send the password reset email via AWS SES. + $sesClient = new SesClient([ + 'version' => 'latest', + 'region' => $config['aws']['ses']['region'], + 'credentials' => [ + 'key' => $config['aws']['ses']['access_key'], + 'secret' => $config['aws']['ses']['secret_key'], + ] + ]); + + $sender_email = $config['aws']['ses']['sender_email']; + $recipient_email = $email; + $subject = "Password Reset Request"; + $reset_link = $config['app']['url'] . "/forgot-password.php?code={$verification_code}"; + $body_text = $locale['passwordResetRequested'] . "\n\n"; + $body_text .= "{$reset_link}\n\n"; + $body_text .= $locale['passwordResetUnrequested']; + + try { + $sesClient->sendEmail([ + 'Destination' => [ + 'ToAddresses' => [$recipient_email], + ], + 'ReplyToAddresses' => [$sender_email], + 'Source' => $sender_email, + 'Message' => [ + 'Body' => [ + 'Text' => [ + 'Charset' => 'UTF-8', + 'Data' => $body_text, + ], + ], + 'Subject' => [ 'Charset' => 'UTF-8', - 'Data' => $body_text, + 'Data' => $subject, ], ], - 'Subject' => [ - 'Charset' => 'UTF-8', - 'Data' => $subject, - ], - ], - ]); - } catch (AwsException $e) { - // Optionally log the error without disclosing details to the user. + ]); + } catch (AwsException $e) { + // Log the error as needed. + } } + header("Location: /forgot-password"); + exit; } - header("Location: forgot-password.php"); - exit; +} + +// Helper function to validate password strength. +function validate_password($password) { + if (strlen($password) < 8) return false; + if (!preg_match('/[A-Z]/', $password)) return false; + if (!preg_match('/[a-z]/', $password)) return false; + if (!preg_match('/[0-9]/', $password)) return false; + return true; } require_once 'includes/header.php'; @@ -92,27 +228,59 @@ require_once 'includes/header.php';
' . htmlspecialchars($_SESSION['error']) . '
'; + if (isset($_SESSION['error'])) { + echo ''; unset($_SESSION['error']); } - if(isset($_SESSION['success'])) { - echo ''; + if (isset($_SESSION['success'])) { + echo ''; unset($_SESSION['success']); } ?> -
-
-

Forgot Password

-
-
- - -
- -
+ + +
+
+

+
+ + +
+ + +
+
+ + +
+
+ + +
+ +
+
-
+ + +
+
+

+
+ +
+ + +
+ +
+
+
+
diff --git a/genre.php b/genre.php index 28d225f..aeb2aa0 100644 --- a/genre.php +++ b/genre.php @@ -52,10 +52,7 @@ require_once 'includes/header.php'; avatar -
get_name(); - ?>
- -

+
get_name();?>
@@ -110,11 +107,11 @@ require_once 'includes/header.php'; $output = new Mix($mix, $db); echo '
'; echo '

'; - echo ''; - echo $output->get_name(); + echo ''; + echo $output->getName(); echo ''; echo ' ‐ '; - $djs = $output->get_djs(); + $djs = $output->getDJs(); $djCount = 0; foreach ($djs as $dj) { echo ''; @@ -168,10 +165,10 @@ require_once 'includes/header.php'; - - +

- getTracklist() != []) { - echo "
"; - echo "
"; - echo "

" . $locale['tracklist'] . "

"; - echo "
    "; - $tracklist = $mix->getTracklist(); - foreach ($tracklist as $track) { - echo "
  • "; - echo $track; - echo "
  • "; - } - echo "
"; - echo "
"; - echo "
"; - } + - ?> + + + copyLinkBtn.onclick = function () { + navigator.clipboard.writeText(urlToCopy).then(function () { + alert(''); + shareModal.hide(); + }, function (err) { + alert(': ' + err); + }); + } + + window.hideModal = function () { + shareModal.hide(); + } + }); - + - + + audio.addEventListener('ended', function () { + playPauseIcon.removeClass('fa-pause').addClass('fa-play'); + }); + }); + + + +
+
+ +
-
+ + + - - - - - \ No newline at end of file +get_genres(); + $genres = $output->getGenres(); $genrelist = []; foreach ($genres as $genre) { @@ -110,8 +110,8 @@ require_once 'includes/header.php'; // Column for mix name and link echo '
'; echo '

'; - echo ''; - echo $output->get_name(); + echo ''; + echo $output->getName(); echo ''; echo '

'; echo '
'; // End column @@ -131,7 +131,7 @@ require_once 'includes/header.php'; // Column for duration echo '
'; echo '

'; - $duration = $output->get_duration(); + $duration = $output->getDuration(); echo $duration['t']; echo '

'; echo '
'; // End column @@ -140,9 +140,9 @@ require_once 'includes/header.php'; echo '
'; echo '

'; // date format should just be year - $date = $output->get_recorded(); + $date = $output->getRecorded(); if ($date == "") { - $date = $output->get_created(); + $date = $output->getCreated(); } echo date('Y', strtotime($date)); @@ -191,10 +191,10 @@ require_once 'includes/header.php';