Language updates. New upload form. new classes.

This commit is contained in:
Cody Cook 2025-02-22 00:20:39 -08:00
commit 8f3061ab99
62 changed files with 3107 additions and 1883 deletions

75
classes/CDN.php Normal file
View file

@ -0,0 +1,75 @@
<?php
namespace DJMixHosting;
use Aws\S3\S3Client;
use Aws\Credentials\Credentials;
class CDN
{
protected $s3Client;
protected $bucket;
protected $config;
public function __construct(array $config)
{
$this->config = $config;
$this->bucket = $config['aws']['s3']['bucket'];
$credentials = new Credentials(
$config['aws']['s3']['access_key'],
$config['aws']['s3']['secret_key']
);
$this->s3Client = new S3Client([
'version' => 'latest',
'region' => $config['aws']['s3']['region'],
'endpoint' => $config['aws']['s3']['host'],
'credentials' => $credentials,
'use_path_style_endpoint' => true,
'profile' => null,
'use_aws_shared_config_files' => false,
]);
}
public function uploadFile(string $localPath, string $remotePath, string $mimeType, string $acl = 'private')
{
try {
$result = $this->s3Client->putObject([
'Bucket' => $this->bucket,
'Key' => $remotePath,
'SourceFile' => $localPath,
'ACL' => $acl,
'ContentType' => $mimeType,
]);
return $result;
} catch (\Exception $e) {
throw new \Exception("Error uploading file to CDN: " . $e->getMessage());
}
}
public function renameFile(string $oldRemotePath, string $newRemotePath)
{
// S3 does not support renaming directly. Copy then delete.
try {
$this->s3Client->copyObject([
'Bucket' => $this->bucket,
'CopySource' => "{$this->bucket}/{$oldRemotePath}",
'Key' => $newRemotePath,
]);
$this->s3Client->deleteObject([
'Bucket' => $this->bucket,
'Key' => $oldRemotePath,
]);
} catch (\Exception $e) {
throw new \Exception("Error renaming file on CDN: " . $e->getMessage());
}
}
public function clearCache(string $remotePath)
{
// If using CloudFront or another CDN in front of S3, implement invalidation logic here.
// This might involve calling the CloudFront API to invalidate the specific path.
}
}

73
classes/Email.php Normal file
View file

@ -0,0 +1,73 @@
<?php
namespace DJMixHosting;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
use Exception;
class Email {
private $config;
private $sesClient;
public function __construct(array $config) {
$this->config = $config;
$this->sesClient = new SesClient([
'version' => 'latest',
'region' => $config['aws']['ses']['region'],
'credentials' => [
'key' => $config['aws']['ses']['access_key'],
'secret' => $config['aws']['ses']['secret_key']
]
]);
}
/**
* Generic method to send an email.
*
* @param string $recipientEmail
* @param string $subject
* @param string $bodyText
* @throws Exception if email sending fails.
*/
public function sendEmail(string $recipientEmail, string $subject, string $bodyText): void {
$senderEmail = $this->config['aws']['ses']['sender_email'];
try {
$this->sesClient->sendEmail([
'Destination' => ['ToAddresses' => [$recipientEmail]],
'ReplyToAddresses' => [$senderEmail],
'Source' => $senderEmail,
'Message' => [
'Body' => [
'Text' => [
'Charset' => 'UTF-8',
'Data' => $bodyText,
],
],
'Subject' => [
'Charset' => 'UTF-8',
'Data' => $subject,
],
],
]);
} catch (AwsException $e) {
throw new Exception("Failed to send email: " . $e->getAwsErrorMessage());
}
}
/**
* Helper method to send a verification email.
*
* @param string $recipientEmail
* @param string $verificationCode
* @throws Exception if email sending fails.
*/
public function sendVerificationEmail(string $recipientEmail, string $verificationCode): void {
$subject = "Verify Your Email Address";
$verificationLink = $this->config['app']['url'] . "/verify_email.php?code={$verificationCode}";
$bodyText = "Please verify your email address by clicking the link below or by entering the code in your profile:\n\n";
$bodyText .= "{$verificationLink}\n\nYour verification code is: {$verificationCode}\nThis code will expire in 15 minutes.";
$this->sendEmail($recipientEmail, $subject, $bodyText);
}
}

View file

@ -26,29 +26,29 @@ class HeaderMeta
$this->config = $config;
// detect if it is DJMixHosting\Mix, DJMixHosting\Genre, DJMixHosting\Mixshow, or DJMixHosting\DJ in $object
if ($object instanceof Mix) {
$djs = $object->get_djs();
$djs = $object->getDJs();
$djList = "";
foreach ($djs as $dj) {
$djList .= $dj->getName() . ", ";
}
$djList = rtrim($djList, ", ");
$genres = $object->get_genres();
$genres = $object->getGenres();
$genreNames = "";
$genreList = rtrim($genreNames, ", ");
$this->set_ogTitle($object->get_name());
$this->set_ogDescription($object->get_description());
$this->set_ogUrl($object->get_page_url());
$this->set_ogImage($object->get_cover());
$this->set_ogTitle($object->getName());
$this->set_ogDescription($object->getDescription());
$this->set_ogUrl($object->getPageUrl());
$this->set_ogImage($object->getCover());
$this->set_ogType("music.song");
$this->set_ogDurationSeconds($object->get_seconds());
$this->set_ogDurationSeconds($object->getSeconds());
$this->set_ogMusician($djList);
$this->set_ogAudio($object->get_download_url());
$this->set_ogAudio($object->getDownloadUrl());
$this->set_ogKeywords($genreList);
$this->set_ogCanonical($object->get_page_url());
$this->set_ogCanonical($object->getPageUrl());
$this->set_ogRobot("index, follow");
$this->set_ogNoindex("");
}

View file

@ -2,343 +2,277 @@
namespace DJMixHosting;
use DateTime;
use Exception;
class Mix
{
private $id = -1;
private $enabled = false;
private $name = "";
private $slug = "";
private $db = null;
private $description = "";
private $cover = "";
private $url = "";
private $seconds = 0;
private $download_only = true;
private $djs = [];
private $genres = [];
private $recorded;
private $downloads = 0;
private $created;
private $updated;
private $playcount = 0;
private int $id = -1;
private bool $enabled = false;
private string $name = "";
private string $slug = "";
private $db;
private string $description = "";
private string $cover = "";
private string $url = "";
private int $seconds = 0;
private bool $download_only = true;
private array $djs = [];
private array $genres = [];
private ?string $recorded = null;
private int $downloads = 0;
private ?string $created = null;
private ?string $updated = null;
private int $playcount = 0;
private $tracklist = [];
private $loadDJs = true;
private $related_mixes = [];
private $duration = [];
private $mixshow = [];
private bool $loadDJs = true;
private array $related_mixes = [];
private array $duration = [];
private array $mixshow = [];
public function __construct($value, $db, $loadDJs = true)
/**
* Construct a Mix object using either an ID or slug.
*
* @param mixed $value The mix identifier (ID or slug).
* @param mixed $db Database connection.
* @param bool $loadDJs Whether to load DJ objects.
*/
public function __construct($value, $db, bool $loadDJs = true)
{
$this->db = $db;
// echo the type of value
$this->loadDJs = $loadDJs;
if (ctype_digit((string)$value)) {
$this->id = (int)$value;
return $this->load_by_id();
$loaded = $this->loadById();
} else {
$this->slug = $value;
return $this->load_by_slug();
$loaded = $this->loadBySlug();
}
if (!$loaded) {
throw new Exception("Mix not found.");
}
}
private function load_by_id(): bool
/**
* Loads mix data by its ID.
*/
private function loadById(): bool
{
$mix = $this->get_mix_by_id();
if ($mix && $mix['title'] != "") {
return $this->build_mix($mix);
} else {
return false;
$mix = $this->getMixById();
if ($mix && !empty($mix['title'])) {
return $this->buildMix($mix);
}
return false;
}
private function get_mix_by_id()
/**
* Loads mix data by its slug.
*/
private function loadBySlug(): bool
{
// Check if current user is admin
$isAdmin = (isset($_SESSION['user']) && isset($_SESSION['user']['role']) && $_SESSION['user']['role'] === 'admin');
if ($isAdmin) {
$stmt = $this->db->prepare("SELECT * FROM mix WHERE id = ?");
} else {
// Only return approved mixes (pending = 0) for non-admins
$stmt = $this->db->prepare("SELECT * FROM mix WHERE id = ? AND pending = 0");
$mix = $this->getMixBySlug();
if ($mix && !empty($mix['title'])) {
return $this->buildMix($mix);
}
return false;
}
/**
* Retrieve mix data by ID.
*/
private function getMixById(): ?array
{
// If the current user is admin, show all mixes;
// Otherwise, only return mixes with pending = 0.
$isAdmin = isset($_SESSION['user']) &&
isset($_SESSION['user']['role']) &&
$_SESSION['user']['role'] === 'admin';
$query = $isAdmin
? "SELECT * FROM mix WHERE id = ?"
: "SELECT * FROM mix WHERE id = ? AND pending = 0";
$stmt = $this->db->prepare($query);
$stmt->bind_param("i", $this->id);
$stmt->execute();
$result = $stmt->get_result();
$mix = $result->fetch_assoc();
$stmt->close();
return $mix;
return $mix ?: null;
}
/**
* @param $mix
* @return true
* Retrieve mix data by slug.
*/
private function build_mix($mix): bool
private function getMixBySlug(): ?array
{
$this->id = $mix['id'];
$this->name = $mix['title'];
$this->slug = $mix['slug'];
$this->description = $mix['description'];
$this->cover = $this->legacyFix($mix['cover']);
$this->url = $this->legacyFix($mix['url']);
$this->seconds = $mix['seconds'];
$this->duration = $this->configure_duration();
$this->download_only = $mix['mediaplayer'];
$this->recorded = $mix['recorded'];
$this->created = $mix['created'];
$this->updated = $mix['lastupdated'];
$this->enabled = $mix['pending'];
$isAdmin = isset($_SESSION['user']) &&
isset($_SESSION['user']['role']) &&
$_SESSION['user']['role'] === 'admin';
$query = $isAdmin
? "SELECT * FROM mix WHERE slug = ?"
: "SELECT * FROM mix WHERE slug = ? AND pending = 0";
$stmt = $this->db->prepare($query);
$stmt->bind_param("s", $this->slug);
$stmt->execute();
$result = $stmt->get_result();
$mix = $result->fetch_assoc();
$stmt->close();
return $mix ?: null;
}
/**
* Build the mix object from database data.
*/
private function buildMix(array $mix): bool
{
$this->id = (int)$mix['id'];
$this->name = $mix['title'] ?? "";
$this->slug = $mix['slug'] ?? "";
$this->description = $mix['description'] ?? "";
$this->cover = $this->legacyFix($mix['cover'] ?? "");
$this->url = $this->legacyFix($mix['url'] ?? "");
$this->seconds = (int)($mix['seconds'] ?? 0);
$this->duration = $this->configureDuration();
$this->download_only = (bool)($mix['mediaplayer'] ?? true);
$this->recorded = $mix['recorded'] ?? null;
$this->created = $mix['created'] ?? null;
$this->updated = $mix['lastupdated'] ?? null;
$this->enabled = (bool)($mix['pending'] ?? false);
if ($this->loadDJs) {
require_once 'DJ.php';
$this->djs[] = new DJ($mix['dj1'], $this->db);
if ($mix['dj2'] != null) {
if (!empty($mix['dj2'])) {
$this->djs[] = new DJ($mix['dj2'], $this->db);
}
if ($mix['dj3'] != null) {
if (!empty($mix['dj3'])) {
$this->djs[] = new DJ($mix['dj3'], $this->db);
}
$this->djs = array_filter($this->djs);
}
$this->load_mix_meta();
$this->tracklist = $this->evaluate_tracklist();
$this->loadMixMeta();
$this->tracklist = $this->evaluateTracklist();
return true;
}
private function legacyFix(mixed $item)
/**
* Fix legacy URL paths.
*/
private function legacyFix(string $item): string
{
if (str_starts_with($item, "/djs/")) {
return "https://cdn.utahsdjs.com" . substr($item, 4);
} else {
return $item;
}
return $item;
}
private function configure_duration(): array
/**
* Configure a formatted duration based on seconds.
*/
private function configureDuration(): array
{
$seconds = $this->seconds;
$hours = floor($seconds / 3600);
$minutes = floor(($seconds / 60) % 60);
$seconds = $seconds % 60;
// for 't', we need to show it as 01:02:03
if ($hours < 10) {
$hours0 = "0" . $hours;
} else {
$hours0 = $hours;
}
if ($minutes < 10) {
$minutes0 = "0" . $minutes;
} else {
$minutes0 = $minutes;
}
if ($seconds < 10) {
$seconds0 = "0" . $seconds;
} else {
$seconds0 = $seconds;
}
// if hours is 0, we don't need to show it
$time = $hours > 0 ? $hours0 . ":" . $minutes0 . ":" . $seconds0 : $minutes0 . ":" . $seconds0;
return ['h' => $hours, 'm' => $minutes, 's' => $seconds, 't' => $time, 'S' => $this->seconds];
$secs = $seconds % 60;
$time = ($hours > 0)
? sprintf("%02d:%02d:%02d", $hours, $minutes, $secs)
: sprintf("%02d:%02d", $minutes, $secs);
return [
'h' => $hours,
'm' => $minutes,
's' => $secs,
't' => $time,
'S' => $this->seconds
];
}
private function load_mix_meta(): void
/**
* Load mix meta data.
*/
private function loadMixMeta(): void
{
$stmt = $this->db->prepare("SELECT attribute,value FROM mix_meta WHERE mix_id = ?");
$stmt = $this->db->prepare("SELECT attribute, value FROM mix_meta WHERE mix_id = ?");
$stmt->bind_param("i", $this->id);
$stmt->execute();
$result = $stmt->get_result();
$meta = $result->fetch_all(MYSQLI_ASSOC);
$stmt->close();
foreach ($meta as $key => $value) {
if ($value['attribute'] == "genre") {
$this->genres[] = $value['value'];
unset($meta[$key]);
foreach ($meta as $entry) {
switch ($entry['attribute']) {
case "genre":
$this->genres[] = $entry['value'];
break;
case "related":
$this->related_mixes[] = $entry['value'];
break;
case "playcount":
$this->playcount = (int)$entry['value'];
break;
case "downloads":
$this->downloads = (int)$entry['value'];
break;
case "tracklist":
$this->tracklist = $entry['value'];
break;
case "mixshow":
$this->mixshow[] = $entry['value'];
break;
default:
// Handle additional meta attributes if needed.
break;
}
if ($value['attribute'] == "related") {
$this->related_mixes[] = $value['value'];
unset($meta[$key]);
}
if ($value['attribute'] == "playcount") {
$this->playcount = $value['value'];
unset($meta[$key]);
}
if ($value['attribute'] == "downloads") {
$this->downloads = $value['value'];
unset($meta[$key]);
}
if ($value['attribute'] == "tracklist") {
$this->tracklist = $value['value'];
unset($meta[$key]);
}
if ($value['attribute'] == "mixshow") {
$this->mixshow[] = $value['value'];
unset($meta[$key]);
}
}
}
private function evaluate_tracklist()
/**
* Evaluate the tracklist data.
*/
private function evaluateTracklist()
{
if (empty($this->tracklist)) {
return [];
} else {
// if the first item in the array is also an array, then return it
if (is_array($this->tracklist)) {
return $this->tracklist;
} else {
return explode("\n", (string)$this->tracklist);
}
}
}
private function load_by_slug(): bool
{
$mix = $this->get_mix_by_slug();
if ($mix) {
if ($mix['title'] != "") {
return $this->build_mix($mix);
} else {
return false;
}
} else {
return false;
if (is_array($this->tracklist)) {
return $this->tracklist;
}
return explode("\n", (string)$this->tracklist);
}
// Getter methods for mix properties.
private function get_mix_by_slug()
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
public function getSlug(): string { return $this->slug; }
public function getDescription(): string { return $this->description; }
public function getCover(): string { return $this->cover; }
public function getUrl(): string { return $this->url; }
public function getSeconds(): int { return $this->seconds; }
public function isDownloadOnly(): bool { return $this->download_only; }
public function getDJs(): array { return $this->djs; }
public function getGenres(): array { return $this->genres; }
public function getRecorded(): ?string { return $this->recorded; }
public function getDownloads(): int { return $this->downloads; }
public function getCreated(): ?string { return $this->created; }
public function getUpdated(): ?string { return $this->updated; }
public function getPlaycount(): int { return $this->playcount; }
public function getTracklist(): array { return $this->evaluateTracklist(); }
public function getRelatedMixes(): array { return $this->related_mixes; }
public function getDuration(): array { return $this->duration; }
public function getMixshow(): array { return $this->mixshow; }
public function getDownloadUrl(): string
{
// Check if current user is admin
$isAdmin = (isset($_SESSION['user']) && isset($_SESSION['user']['role']) && $_SESSION['user']['role'] === 'admin');
if ($isAdmin) {
$stmt = $this->db->prepare("SELECT * FROM mix WHERE slug = ?");
} else {
$stmt = $this->db->prepare("SELECT * FROM mix WHERE slug = ? AND pending = 0");
}
$stmt->bind_param("s", $this->slug);
$stmt->execute();
$result = $stmt->get_result();
$mix = $result->fetch_assoc();
$stmt->close();
return $mix;
return "https://beta.utahsdjs.com/mix/{$this->slug}/download";
}
public function get_recorded()
public function getPageUrl(): string
{
return $this->recorded;
return "https://beta.utahsdjs.com/mix/{$this->slug}";
}
public function get_created()
{
return $this->created;
}
public function get_updated()
{
return $this->updated;
}
public function get_img()
{
return $this->cover;
}
public function get_djs()
{
return $this->djs;
}
public function get_description()
{
return $this->description;
}
public function get_tracklist(): array
{
return $this->tracklist;
}
public function get_genres()
{
return $this->genres;
}
public function get_seconds(): int
{
return $this->seconds;
}
public function is_download_only(): bool
{
return $this->download_only;
}
public function get_url(): string
{
return $this->url;
}
public function get_cover()
{
return $this->cover;
}
public function get_downloads(): int
{
return $this->downloads;
}
public function get_plays(): int
{
return $this->playcount;
}
public function get_id(): int
{
return $this->id;
}
public function get_name(): string
{
return $this->name;
}
public function get_slug(): string
{
return $this->slug;
}
public function get_duration(): array
{
return $this->duration;
}
public function get_mixshow(): array
{
return $this->mixshow;
}
public function get_download_url()
{
return "https://beta.utahsdjs.com/mix/" . "$this->slug" . "/download";
}
public function get_page_url()
{
return "https://beta.utahsdjs.com/mix/" . "$this->slug";
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace DJMixHosting;
class SessionManager {
/**
* Ensure the session is started.
*/
public static function start() {
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
}
/**
* Set user data in session.
*/
public static function setUser(array $userData) {
$_SESSION['user'] = $userData;
}
/**
* Retrieve the current user data from session.
*/
public static function getUser() {
return $_SESSION['user'] ?? null;
}
/**
* Destroy the session.
*/
public static function destroy() {
if (session_status() !== PHP_SESSION_NONE) {
session_unset();
session_destroy();
}
}
}

View file

@ -4,87 +4,125 @@ namespace DJMixHosting;
class Upload
{
private $file;
private $file_name;
private $file_size;
private $file_tmp;
private $file_type;
private $file_ext;
private $file_path;
private $extensions = array("mp3", "zip");
private $upload_dir = "uploads/";
private $errors = array();
private $ok = false;
private $config;
private $uuid = "";
protected $file;
protected $config;
protected $errors = [];
protected $uploadDir;
protected $uuid;
protected $fileExt;
protected $localPath;
public function __construct($file, $config)
public function __construct(array $file, array $config)
{
$this->file = $file;
$this->file_name = $file['name'];
$this->file_size = $file['size'];
$this->file_tmp = $file['tmp_name'];
$this->file_type = $file['type'];
$this->config = $config;
$ext = explode('.', $file['name']);
$this->file_ext = strtolower(end($ext));
$this->uploadDir = rtrim($config['uploads']['tmp_path'], '/') . '/';
$this->uuid = uniqid();
$this->fileExt = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
}
private function check_file_size(): bool
public function validate(): bool
{
if ($this->file_size > $this->config['uploads']['max_file_size']){
$this->errors[] = "File size is too large";
return false;
if ($this->file['error'] !== UPLOAD_ERR_OK) {
$this->errors[] = "File upload error.";
}
return true;
}
private function check_file_extension(): bool
{
if (!in_array($this->file_ext, $this->extensions)){
$this->errors[] = "Invalid file extension";
return false;
if ($this->file['size'] > $this->config['uploads']['max_file_size']) {
$this->errors[] = "File is too large.";
}
return true;
}
private function check_file_exists(): bool
{
if (file_exists($this->upload_dir . $this->file_name)){
$this->errors[] = "File already exists";
return false;
if (!in_array($this->fileExt, $this->config['uploads']['allowed_file_types'])) {
$this->errors[] = "File type not allowed.";
}
return true;
if (!in_array($this->file['type'], $this->config['uploads']['allowed_mime_type'])) {
$this->errors[] = "MIME type not allowed.";
}
return empty($this->errors);
}
private function move_file(): bool
public function getErrors(): array
{
if (move_uploaded_file($this->file_tmp, $this->upload_dir . $this->uuid)){
$this->file_path = $this->upload_dir . $this->uuid;
return $this->errors;
}
public function moveFile(): bool
{
$destination = $this->uploadDir . $this->uuid . "." . $this->fileExt;
if (move_uploaded_file($this->file['tmp_name'], $destination)) {
$this->localPath = $destination;
return true;
}
$this->errors[] = "Failed to move uploaded file.";
return false;
}
public function dump_all()
public function process(): array
{
$array = array(
"file" => $this->file,
"file_name" => $this->file_name,
"file_size" => $this->file_size,
"file_tmp" => $this->file_tmp,
"file_type" => $this->file_type,
"file_ext" => $this->file_ext,
"file_path" => $this->file_path,
"extensions" => $this->extensions,
"upload_dir" => $this->upload_dir,
"errors" => $this->errors,
"ok" => $this->ok,
"uuid" => $this->uuid
);
// Assuming moveFile() has been called
$uploadTask = [
'original_name' => $this->file['name'],
'local_path' => $this->localPath,
'size' => $this->file['size'],
'ext' => $this->fileExt,
];
if ($this->fileExt === "mp3") {
$escapedFile = escapeshellarg($this->localPath);
$artist = trim(shell_exec("eyed3 --query='%a%' $escapedFile"));
$title = trim(shell_exec("eyed3 --query='%t%' $escapedFile"));
$duration = trim(shell_exec("mp3info -p \"%S\" $escapedFile"));
$uploadTask['file_type'] = 'mp3';
$uploadTask['metadata'] = [
'artist' => $artist,
'title' => $title,
'duration' => $duration,
];
$uploadTask['mediaplayer'] = 0;
} elseif ($this->fileExt === "zip") {
$zip = new \ZipArchive;
if ($zip->open($this->localPath) === true) {
$totalDuration = 0;
$tracklist = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$entryExt = strtolower(pathinfo($entryName, PATHINFO_EXTENSION));
if ($entryExt === "mp3") {
$tempDir = $this->uploadDir . uniqid('zip_');
mkdir($tempDir);
$tempFilePath = $tempDir . '/' . basename($entryName);
$zip->extractTo($tempDir, $entryName);
$escapedFile = escapeshellarg($tempFilePath);
$title = trim(shell_exec("eyed3 --query='%t%' $escapedFile"));
$duration = trim(shell_exec("mp3info -p \"%S\" $escapedFile"));
$tracklist[] = $title ?: basename($entryName);
$totalDuration += (int)$duration;
unlink($tempFilePath);
rmdir($tempDir);
}
}
$zip->close();
$uploadTask['file_type'] = 'zip';
$uploadTask['metadata'] = [
'tracklist' => $tracklist,
'total_duration' => $totalDuration,
];
$uploadTask['mediaplayer'] = 1;
} else {
$this->errors[] = "Failed to open ZIP file.";
}
} else {
$this->errors[] = "Unsupported file type.";
}
return $uploadTask;
}
public function uploadToCDN(CDN $cdn): string
{
// Create a remote path e.g., under a temp folder with a unique name
$remotePath = "temp/mixes/" . uniqid() . "_" . basename($this->localPath);
$mimeType = ($this->fileExt === 'mp3') ? 'audio/mpeg' : 'application/zip';
$cdn->uploadFile($this->localPath, $remotePath, $mimeType);
return $remotePath;
}
}

View file

@ -2,13 +2,19 @@
namespace DJMixHosting;
use DateTime;
use Exception;
use Random\RandomException;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
Class User{
class User {
private $db;
private $id;
private $username;
private $firstName;
private $lastName;
private $email;
private $location;
private $bio;
@ -16,42 +22,83 @@ Class User{
private $updated;
private $verified;
private $role;
private $img;
private $img = "";
private $api_key;
public function __construct($db){
public function __construct($db, $id = null) {
$this->db = $db;
if ($id) {
$this->loadUserById($id);
}
}
/**
* @throws RandomException
* Load user data from the database by id.
*/
public function newUser($username, $password, $email){
if ($this->check_existing_user($username, $email)){
throw new RandomException("User already exists");
private function loadUserById($id) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $id);
$stmt->execute();
$user_data = $stmt->get_result()->fetch_assoc();
$stmt->close();
if ($user_data) {
$this->id = $user_data['id'];
$this->username = $user_data['username'];
$this->firstName = $user_data['firstName'];
$this->lastName = $user_data['lastName'];
$this->email = $user_data['email'];
$this->verified = $user_data['emailVerified'];
$this->img = $user_data['img'];
$this->api_key = $user_data['apiKey'];
$this->created = $user_data['created'];
$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 = "";
}
}
/**
* Register a new user.
*
* @throws RandomException if the user already exists.
*/
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");
}
$this->username = $username;
$this->email = $email;
$password2 = password_hash($password, PASSWORD_DEFAULT);
$this->password = $password2;
$this->firstName = $firstName;
$this->lastName = $lastName;
$password_hashed = password_hash($password, PASSWORD_DEFAULT);
$this->location = "";
$this->bio = "";
$this->created = date('Y-m-d H:i:s');
$this->updated = date('Y-m-d H:i:s');
$this->verified = 0;
$this->role = "user";
$this->img = "";
$this->api_key = bin2hex(random_bytes(32));
// Set default values for optional fields.
$this->location = "";
$this->bio = "";
$this->created = date('Y-m-d H:i:s');
$this->updated = date('Y-m-d H:i:s');
$this->verified = 0;
$this->role = "user";
$this->img = "";
$this->api_key = bin2hex(random_bytes(32));
$stmt = $this->db->prepare("INSERT INTO users (username, password, email, img) VALUES (?, ?, ?, ?)");
$stmt->bind_param("ssss", $this->username, $this->password, $this->email, $this->img);
$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->execute();
$userId = $stmt->insert_id;
$stmt->close();
$this->id = $userId;
return $userId;
}
private function check_existing_user($username, $email){
private function check_existing_user($username, $email) {
$stmt = $this->db->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
$stmt->bind_param("ss", $username, $email);
$stmt->execute();
@ -61,8 +108,13 @@ Class User{
return $user;
}
public function login($email, $password)
{
/**
* Login a user by email and password.
*
* Returns the user data array if successful. In case of failure,
* a string error message is returned.
*/
public function login($email, $password) {
// Retrieve user record by email
$stmt = $this->db->prepare("SELECT * FROM users WHERE email = ?");
$stmt->bind_param("s", $email);
@ -78,10 +130,10 @@ Class User{
$attempt_data = $stmt->get_result()->fetch_assoc();
$stmt->close();
$current_time = new \DateTime();
$current_time = new DateTime();
if ($attempt_data && !empty($attempt_data['lockout_until'])) {
$lockout_until = new \DateTime($attempt_data['lockout_until']);
$lockout_until = new DateTime($attempt_data['lockout_until']);
if ($current_time < $lockout_until) {
return "Account locked until " . $lockout_until->format('Y-m-d H:i:s') . ". Please try again later.";
}
@ -95,15 +147,10 @@ Class User{
// Verify the password using password_verify
if (password_verify($password, $user_data['password'])) {
// Successful login clear login attempts and set session variables
// Successful login clear login attempts.
$this->resetLoginAttempts($email);
$_SESSION['user'] = [
'id' => $user_data['id'],
'email' => $user_data['email'],
'username' => $user_data['username'],
'role' => $user_data['isAdmin'] ? 'admin' : 'user'
];
return true;
// Return the user data for further session handling
return $user_data;
} else {
$attempts = $this->updateFailedAttempt($email);
return "Invalid email or password. Attempt $attempts of 3.";
@ -115,8 +162,7 @@ Class User{
* If attempts reach 3, set a lockout that doubles each time.
* Returns the current number of attempts.
*/
private function updateFailedAttempt($email)
{
private function updateFailedAttempt($email) {
// Check for an existing record
$stmt = $this->db->prepare("SELECT * FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
@ -124,7 +170,7 @@ Class User{
$record = $stmt->get_result()->fetch_assoc();
$stmt->close();
$current_time = new \DateTime();
$current_time = new DateTime();
if ($record) {
$attempts = $record['attempts'] + 1;
@ -165,13 +211,186 @@ Class User{
/**
* Reset the login_attempts record for the given email.
*/
private function resetLoginAttempts($email)
{
private function resetLoginAttempts($email) {
$stmt = $this->db->prepare("DELETE FROM login_attempts WHERE email = ?");
$stmt->bind_param("s", $email);
$stmt->execute();
$stmt->close();
}
/**
* Update the user's email address.
*
* @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.
*/
public function updateEmail(string $newEmail, array $config): string {
$newEmail = filter_var($newEmail, FILTER_VALIDATE_EMAIL);
if (!$newEmail) {
throw new \Exception("Invalid email format.");
}
}
// Update email and mark as unverified.
$stmt = $this->db->prepare("UPDATE users SET email = ?, emailVerified = 0 WHERE id = ?");
$stmt->bind_param("si", $newEmail, $this->id);
$stmt->execute();
$stmt->close();
// Generate verification code and expiry (15 minutes from now)
$verification_code = bin2hex(random_bytes(16));
$expires_at = date("Y-m-d H:i:s", strtotime("+15 minutes"));
// Store the verification record.
$stmt = $this->db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at) VALUES (?, ?, ?, ?)");
$stmt->bind_param("isss", $this->id, $newEmail, $verification_code, $expires_at);
$stmt->execute();
$stmt->close();
// Use the new Email class to send the verification email.
$emailObj = new Email($config);
$emailObj->sendVerificationEmail($newEmail, $verification_code);
// Update the class properties.
$this->email = $newEmail;
$this->verified = 0;
return "Email updated. A verification email has been sent to your new address.";
}
public function updateName($firstName, $lastName) {
// Update the user's name.
$stmt = $this->db->prepare("UPDATE users SET firstName = ?, lastName = ? WHERE id = ?");
$stmt->bind_param("ssi", $firstName, $lastName, $this->id);
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Failed to update name. Please try again.");
}
$stmt->close();
// Optionally update class properties.
$this->firstName = $firstName;
$this->lastName = $lastName;
return "Name updated successfully.";
}
public function updatePassword($currentPassword, $newPassword, $confirmPassword) {
// Retrieve the current password hash.
$stmt = $this->db->prepare("SELECT password FROM users WHERE id = ?");
$stmt->bind_param("i", $this->id);
$stmt->execute();
$userData = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$userData || !password_verify($currentPassword, $userData['password'])) {
throw new Exception("Current password is incorrect.");
}
if ($newPassword !== $confirmPassword) {
throw new Exception("New password and confirmation do not match.");
}
// Validate the new password.
$pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,32}$/';
if (!preg_match($pattern, $newPassword)) {
throw new Exception("New password must be 8-32 characters and include at least one uppercase letter, one lowercase letter, one number, and one symbol.");
}
$hashed_new_password = password_hash($newPassword, PASSWORD_DEFAULT);
$stmt = $this->db->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->bind_param("si", $hashed_new_password, $this->id);
if (!$stmt->execute()) {
$stmt->close();
throw new Exception("Failed to update password. Please try again.");
}
$stmt->close();
return "Password updated successfully.";
}
public function updateUsername($newUsername) {
// Validate username format.
if (!preg_match('/^[a-zA-Z0-9_]{3,25}$/', $newUsername)) {
throw new Exception("Invalid username format.");
}
// Check if the new username already exists for another user.
$stmt = $this->db->prepare("SELECT id FROM users WHERE username = ? AND id != ?");
$stmt->bind_param("si", $newUsername, $this->id);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$stmt->close();
throw new Exception("Username already taken.");
}
$stmt->close();
// Update the username.
$stmt = $this->db->prepare("UPDATE users SET username = ? WHERE id = ?");
$stmt->bind_param("si", $newUsername, $this->id);
$stmt->execute();
$stmt->close();
$this->username = $newUsername;
return "Username updated successfully.";
}
/**
* Verify the user's email using the provided verification code.
*
* @param string $verification_code The code submitted by the user.
* @return string Success message.
* @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
$stmt = $this->db->prepare("SELECT * FROM email_verifications WHERE user_id = ? AND verification_code = ?");
$stmt->bind_param("is", $this->id, $verification_code);
$stmt->execute();
$result = $stmt->get_result();
$record = $result->fetch_assoc();
$stmt->close();
if (!$record) {
throw new \Exception("Invalid verification code.");
}
// Check if the verification code has expired
$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.");
}
// Update the user's record to mark the email as verified
$stmt = $this->db->prepare("UPDATE users SET emailVerified = 1 WHERE id = ?");
$stmt->bind_param("i", $this->id);
$stmt->execute();
$stmt->close();
// Remove the verification record to clean up
$stmt = $this->db->prepare("DELETE FROM email_verifications WHERE user_id = ?");
$stmt->bind_param("i", $this->id);
$stmt->execute();
$stmt->close();
// Update the object property
$this->verified = 1;
return "Email verified successfully.";
}
// 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; }
}