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 {
$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;
}
}
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;
$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
];
}
// 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];
}
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);
}
}
}
// Getter methods for mix properties.
private function load_by_slug(): bool
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
{
$mix = $this->get_mix_by_slug();
if ($mix) {
if ($mix['title'] != "") {
return $this->build_mix($mix);
} else {
return false;
}
} else {
return false;
}
return "https://beta.utahsdjs.com/mix/{$this->slug}/download";
}
private function get_mix_by_slug()
public function getPageUrl(): 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");
return "https://beta.utahsdjs.com/mix/{$this->slug}";
}
$stmt->bind_param("s", $this->slug);
$stmt->execute();
$result = $stmt->get_result();
$mix = $result->fetch_assoc();
$stmt->close();
return $mix;
}
public function get_recorded()
{
return $this->recorded;
}
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.";
}
if ($this->file['size'] > $this->config['uploads']['max_file_size']) {
$this->errors[] = "File is too large.";
}
if (!in_array($this->fileExt, $this->config['uploads']['allowed_file_types'])) {
$this->errors[] = "File type not allowed.";
}
if (!in_array($this->file['type'], $this->config['uploads']['allowed_mime_type'])) {
$this->errors[] = "MIME type not allowed.";
}
return empty($this->errors);
}
public function getErrors(): array
{
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;
}
private function check_file_extension(): bool
{
if (!in_array($this->file_ext, $this->extensions)){
$this->errors[] = "Invalid file extension";
return false;
}
return true;
}
private function check_file_exists(): bool
{
if (file_exists($this->upload_dir . $this->file_name)){
$this->errors[] = "File already exists";
return false;
}
return true;
}
private function move_file(): bool
{
if (move_uploaded_file($this->file_tmp, $this->upload_dir . $this->uuid)){
$this->file_path = $this->upload_dir . $this->uuid;
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,25 +22,62 @@ 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);
// Set default values for optional fields.
$this->location = "";
$this->bio = "";
$this->created = date('Y-m-d H:i:s');
@ -44,14 +87,18 @@ Class 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; }
}

View file

@ -2,7 +2,7 @@
"name": "utahsdjs/utahsdjs2025",
"description": "description",
"minimum-stability": "dev",
"version": "0.0.1",
"version": "0.0.16",
"license": "proprietary",
"authors": [
{
@ -11,12 +11,13 @@
}
],
"require": {
"php": ">=8.2.0",
"yosymfony/toml": "*",
"ext-mysqli": "*",
"aws/aws-sdk-php": ">=3.339.15",
"ext-curl": "*",
"phpunit/phpunit": ">8.5.1.0",
"aws/aws-sdk-php": "*"
"ext-mysqli": "*",
"php": ">=8.2.0",
"phpunit/phpunit": ">=11",
"yosymfony/toml": "*",
"ext-zip": "*"
},
"autoload": {
"psr-4": {

1358
composer.lock generated

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -1,69 +1,128 @@
<?php
require_once 'includes/globals.php';
// Initialize auto-fill variables
$userName = '';
$userEmail = '';
// Check if user details are available in the session
if (isset($_SESSION['user'])) {
$userName = $_SESSION['user']['firstName'] . ' ' . $_SESSION['user']['lastName'];
$userEmail = $_SESSION['user']['email'];
}
require_once 'vendor/autoload.php';
use DJMixHosting\Telegram;
if ($_POST) {
$name = $_POST['name'];
$email = $_POST['email'];
$formName = $_POST['name'];
if ($formName != $userName) {
$name = $userName . " (Orig: " . $formName . ")";
} else {
$name = $formName;
}
$formEmail = $_POST['email'];
if ($formEmail != $userEmail) {
$email = $userEmail . " (Orig: " . $formEmail . ")";
} else {
$email = $userEmail;
}
$message = $_POST['message'];
$ipAddress = $_SERVER['REMOTE_ADDR'];
require_once 'classes/Telegram.php';
$telegram = new Telegram($config['notifications']['telegram']['token'], $config['notifications']['telegram']['chat_id']);
$result = $telegram->send_message("Name: $name\nEmail: $email\nMessage: $message");
$result = $telegram->send_message("Name: $name\nEmail: $email\nIP Address: $ipAddress\nMessage: $message");
}
$title = $locale['contactus'];
require_once 'includes/header.php';
if ($_POST) {
if ($result['ok']) {
echo '<div class="alert alert-success" role="alert">Message sent successfully</div>';
} else {
echo '<div class="alert alert-danger" role="alert">An error occurred while sending the message</div>';
}
}
?>
<section>
<div class="container py-5">
<div class="row">
<div class="col">
<nav aria-label="breadcrumb" class="bg-body-tertiary rounded-3 p-3 mb-4">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/"><?php echo $locale['home']; ?></a></li>
<li class="breadcrumb-item active"><a
href="/contact.php"><?php echo $locale['contactus']; ?></a></li>
<!-- Custom CSS -->
<style>
.contact-form {
border-radius: 15px;
box-shadow: 0 0 20px rgba(0,0,0,0.1);
padding: 2rem;
}
.form-control:focus {
border-color: #0d6efd;
box-shadow: 0 0 0 0.25rem rgba(13,110,253,.25);
}
.social-links a {
color: #6c757d;
margin: 0 10px;
transition: color 0.3s ease;
}
.social-links a:hover {
color: #0d6efd;
}
</style>
<?php if ($_POST): ?>
<div class="position-fixed top-0 start-50 translate-middle-x p-3" style="z-index: 1000">
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header <?php echo $result['ok'] ? 'bg-success' : 'bg-danger'; ?> text-white">
<strong class="me-auto">Notification</strong>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="toast" aria-label="Close"></button>
</div>
<div class="toast-body">
<?php echo $result['ok'] ? 'Message sent successfully!' : 'An error occurred while sending the message.'; ?>
</div>
</div>
</div>
<?php endif; ?>
<!-- Breadcrumb -->
<section>
<div class="container py-3">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/" class="text-decoration-none"><?php echo $locale['home']; ?></a></li>
<li class="breadcrumb-item active"><?php echo $locale['contactus']; ?></li>
</ol>
</nav>
</div>
</section>
<!-- Contact Section -->
<div class="container py-5">
<!-- Header -->
<div class="row mb-5 text-center">
<div class="col-lg-8 mx-auto">
<h1 class="display-4 mb-3"><?php echo $locale['contactus']; ?></h1>
<p class="lead text-muted">We'd love to hear from you. Send us a message and we'll respond as soon as possible.</p>
</div>
</div>
</section>
<div class="container py-5">
<div class="row">
<div class="col">
<h1><?php echo $locale['contactus']; ?></h1>
<p>For any inquiries, please complete the form below.</p>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<form action="contact.php" method="post">
<div class="mb-3">
<!-- Contact Form -->
<div class="col-lg-7 mb-4 mb-lg-0">
<div class="contact-form">
<h3 class="mb-4">Send us a Message</h3>
<form action="/contact" method="post">
<div class="mb-4">
<label for="name" class="form-label">Name</label>
<input type="text" class="form-control" id="name" name="name" required>
<input type="text" class="form-control form-control-lg" id="name" name="name" value="<?php echo htmlspecialchars($userName); ?>" required>
</div>
<div class="mb-3">
<div class="mb-4">
<label for="email" class="form-label">Email</label>
<input type="email" class="form-control" id="email" name="email" required>
<input type="email" class="form-control form-control-lg" id="email" name="email" value="<?php echo htmlspecialchars($userEmail); ?>" required>
</div>
<div class="mb-3">
<div class="mb-4">
<label for="message" class="form-label">Message</label>
<textarea class="form-control" id="message" name="message" rows="3" required></textarea>
<textarea class="form-control form-control-lg" id="message" name="message" rows="5" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
<button type="submit" class="btn btn-primary btn-lg px-5">Send Message</button>
</form>
</div>
</div>
</div>
<?php
require_once 'includes/footer.php';
</div>
<?php require_once 'includes/footer.php'; ?>

12
dj.php
View file

@ -151,7 +151,7 @@ require_once 'includes/header.php';
foreach ($mixes as $mix) {
$output = new Mix($mix['slug'], $db);
$genres = $output->get_genres();
$genres = $output->getGenres();
$genrelist = [];
foreach ($genres as $genre) {
@ -164,8 +164,8 @@ require_once 'includes/header.php';
// Column for mix name and link
echo '<div class="col-md text-truncate" >';
echo '<p class="mb-0 " >';
echo '<a title="' . $output->get_name() . '" href="/mix/' . $output->get_slug() . '">';
echo $output->get_name();
echo '<a title="' . $output->getName() . '" href="/mix/' . $output->getSlug() . '">';
echo $output->getName();
echo '</a>';
echo '</p>';
echo '</div>'; // End column
@ -185,7 +185,7 @@ require_once 'includes/header.php';
// Column for duration
echo '<div class="col-md">';
echo '<p class="mb-0">';
$duration = $output->get_duration();
$duration = $output->getDuration();
echo $duration['t'];
echo '</p>';
echo '</div>'; // End column
@ -194,9 +194,9 @@ require_once 'includes/header.php';
echo '<div class="col-md">';
echo '<p class="mb-0">';
// date format should just be year
$date = $output->get_recorded();
$date = $output->getRecorded();
if (is_null($date) || empty(trim($date))) {
$date = $output->get_created();
$date = $output->getCreated();
}
echo date('Y', strtotime($date));

View file

@ -1,19 +1,32 @@
<section>
<footer>
<div class="container py-5">
<!-- Navigation Links -->
<div class="row mb-3">
<div class="col text-center">
<p><?php
echo "&copy; 2008 - ";
echo date('Y');
echo " " . $locale['allrightsreserved'];
?></p>
<ul class="list-inline">
<li class="list-inline-item"><a href="/contact">Contact Us</a></li>
<li class="list-inline-item"><a href="/privacy">Privacy Policy</a></li>
<!-- Uncomment the following line once you have a Terms & Conditions page -->
<!-- <li class="list-inline-item"><a href="/terms">Terms & Conditions</a></li> -->
</ul>
</div>
</div>
<!-- Copyright Text -->
<div class="row mb-2">
<div class="col text-center">
<p><?php echo "&copy; 2008 - " . date('Y') . " " . $locale['allrightsreserved']; ?></p>
</div>
</div>
<!-- Software Credit -->
<div class="row">
<div class="col text-center">
Created using <a href="https://gitea.threefifteen.info/cody/dj_mix_hosting_software">DJ Mix Hosting Software</a>
<p>Created using <a href="https://gitea.threefifteen.info/cody/dj_mix_hosting_software">DJ Mix Hosting Software</a></p>
</div>
</div>
</div>
</section>
</footer>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/js/bootstrap.bundle.min.js" integrity="sha512-7Pi/otdlbbCR+LnW+F7PwFcSDJOuUJB3OxtEHbg4vSMvzvJjde4Po1v4BR9Gdc9aXNUNFVUY+SK51wWT8WF0Gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
</body>

View file

@ -4,24 +4,21 @@
require_once 'vendor/autoload.php';
use Yosymfony\Toml\Toml;
use DJMixHosting\SessionManager;
$config = Toml::ParseFile('includes/config.toml');
require_once 'functions/i18n.php';
require_once 'includes/sessions.php';
// Instead of including sessions.php, start the session via SessionManager:
SessionManager::start();
require_once 'includes/lang_loader.php';
$mixshowsPages = ["/mixshows", "/mixshows/", "/mixshows.php"];
$djsPages = ["/djs", "/djs/", "/djs.php"];
$genrePages = ["/genres", "/genres/", "/genres.php"];
$mixPages = ["/mix", "/mix/", "/mix.php"];
$specialStyle = array_merge($mixshowsPages, $djsPages, $genrePages);
/**
* @param int $count
* @param mixed $dj
@ -51,7 +48,6 @@ function social_line($social, $value): string
$icon = "";
$url = "";
$color = "#000000";
switch ($social) {
case "facebook":
$icon = "fa-brands fa-facebook";
@ -107,18 +103,15 @@ function social_line($social, $value): string
$name = "Website";
break;
}
return "
<li class='list-group-item d-flex justify-content-between bg-body-secondary align-items-center p-3' title='$name'>
<i class='fa $icon fa-lg' style='color: $color;' title='$name'></i>
<p class='mb-0'><a href='$url' title='$name' >$value</a>
</p>
<p class='mb-0'><a href='$url' title='$name'>$value</a></p>
</li>";
}
function box_line($title, $value): string
{
return "<hr>
<div class='row'>
<div class='col-sm-3'>
@ -128,5 +121,4 @@ function box_line($title, $value): string
<p class='text-muted mb-0'>$value</p>
</div>
</div>";
}

View file

@ -18,4 +18,3 @@ if (isset($mix)) {
echo $meta;
}
}

View file

@ -1,11 +1,14 @@
<?php require_once 'includes/header-security.php'; ?>
<!doctype html >
<html lang="<?php echo $lang ?>" <?php
// dark mode checker
if (isset($_SESSION['darkmode']) && $_SESSION['darkmode'] == 'true') {
echo 'data-bs-theme="dark"';
} ?>
<!doctype html>
<html lang="<?php echo $lang ?>">
<head>
<script>
(function() {
if (localStorage.getItem('darkmode') === 'true') {
document.documentElement.setAttribute('data-bs-theme', 'dark');
}
})();
</script>
<?php if (isset($config['seo']['google']) && $config['seo']['google']) {
require_once 'includes/google_tag_manager.php';
echo get_google_tag_manager_header($config['seo']['gtm']['key']);

View file

@ -11,7 +11,6 @@ if (isset($_GET['lang']) && array_key_exists($_GET['lang'], $languages)) {
}
$current_lang = $_SESSION['lang'] ?? $config['app']['locale'];
?>
<header class="navbar navbar-expand-md bg-body sticky-top shadow">
<div class="container-fluid">
<a class="navbar-brand pe-3" href="/"><?php echo htmlspecialchars($config['app']['name']); ?></a>
@ -22,55 +21,52 @@ $current_lang = $_SESSION['lang'] ?? $config['app']['locale'];
<div class="collapse navbar-collapse" id="navbarToggle">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link<?php
// if the page is genres.php, set active and aria-current="page"
if (basename($_SERVER['SCRIPT_NAME']) == 'index.php') {
echo current_list();
}
?>" href="/"><?php echo $locale['home']; ?></a>
<a class="nav-link<?php if (basename($_SERVER['SCRIPT_NAME']) == 'index.php') { echo current_list(); } ?>" href="/"><?php echo $locale['home']; ?></a>
</li>
<li class="nav-item">
<a class="nav-link<?php
if (in_array($_SERVER['SCRIPT_NAME'], $genrePages)) {
echo current_list();
} ?>" href="/genres"><?php echo $locale['genres']; ?></a>
<a class="nav-link<?php if (in_array($_SERVER['SCRIPT_NAME'], $genrePages)) { echo current_list(); } ?>" href="/genres"><?php echo $locale['genres']; ?></a>
</li>
<li class="nav-item">
<a class="nav-link<?php
if (in_array($_SERVER['SCRIPT_NAME'], $djsPages)) {
echo current_list();
} ?>" href="/djs"><?php echo $locale['djs']; ?></a>
<a class="nav-link<?php if (in_array($_SERVER['SCRIPT_NAME'], $djsPages)) { echo current_list(); } ?>" href="/djs"><?php echo $locale['djs']; ?></a>
</li>
<li class="nav-item">
<a class="nav-link<?php
if (in_array($_SERVER['SCRIPT_NAME'], $mixshowsPages)) {
echo current_list();
} ?>" href="/mixshows"><?php echo $locale['mixshows']; ?></a>
<a class="nav-link<?php if (in_array($_SERVER['SCRIPT_NAME'], $mixshowsPages)) { echo current_list(); } ?>" href="/mixshows"><?php echo $locale['mixshows']; ?></a>
</li>
</ul>
<?php
if (isset($_SESSION['darkmode']) && $_SESSION['darkmode'] == 'true') {
echo '<button class="btn btn-outline-dark" id="darkModeToggle" onclick="toggleDarkMode()">🌙</button>';
} else {
echo '<button class="btn btn-outline-light" id="darkModeToggle" onclick="toggleDarkMode()">🌞</button>';
}
?>
<div class="d-flex align-items-center">
<!-- Search form remains outside the dropdown -->
<form class="d-flex me-3" role="search">
<input class="form-control me-2" type="search" placeholder="<?php echo $locale['search']; ?>"
aria-label="<?php echo $locale['search']; ?>">
<button class="btn btn-outline-success" type="submit"><?php echo $locale['search']; ?></button>
</form>
<!-- Profile Widget Dropdown including language switcher -->
<div class="dropdown">
<label for="languageSelect" style="display: none">Language Selector</label>
<select class="form-select" id="languageSelect"
onchange="location = this.value;">
<a class="nav-link dropdown-toggle" href="#" id="profileDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-user"></i>
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="profileDropdown">
<?php if (isset($_SESSION['user'])): ?>
<li><a class="dropdown-item" href="/profile"><?php echo $locale['userProfile']; ?></a></li>
<li><a class="dropdown-item" href="/upload"><?php echo $locale['upload']; ?></a></li>
<li><a class="dropdown-item" href="/logout"><?php echo $locale['logout']; ?></a></li>
<li><hr class="dropdown-divider"></li>
<?php else: ?>
<li><a class="dropdown-item" href="/login"><?php echo $locale['login']; ?></a></li>
<li><hr class="dropdown-divider"></li>
<?php endif; ?>
<!-- Language Switcher -->
<li class="px-3 py-2">
<label for="languageSelect" class="visually-hidden">Language Selector</label>
<select class="form-select" id="languageSelect" onchange="location = this.value;">
<?php
if (isset($_SERVER["REQUIEST_URI"])) {
$currentUrl = strtok($_SERVER["REQUEST_URI"], '?') ?? '/';
} else {
$currentUrl = '/';
}
$queryParams = $_GET;
foreach ($languages as $key => $value) {
$queryParams['lang'] = $key;
@ -81,62 +77,49 @@ $current_lang = $_SESSION['lang'] ?? $config['app']['locale'];
}
?>
</select>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<button class="dropdown-item" id="darkModeToggle" onclick="toggleDarkMode()">
<?php echo (isset($_SESSION['darkmode']) && $_SESSION['darkmode'] == 'true') ? '🌙' : '🌞'; ?>
Toggle Dark Mode
</button>
</li>
</ul>
</div>
<?php
if (isset($_SESSION['user'])) {
echo '<a class="nav-link" href="/profile">' . $locale['userProfile'] . '</a>';
} else {
echo '<a class="nav-link" href="/login">' . $locale['login'] . '</a>';
}
?>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="<?php echo $locale['search']; ?>"
aria-label="<?php echo $locale['search']; ?>">
<button class="btn btn-outline-success" type="submit"><?php echo $locale['search']; ?></button>
</form>
</div>
</div>
</div>
<script>
document.getElementById('darkModeToggle').addEventListener('click', function() {
// Dark mode toggle script
function toggleDarkMode() {
var html = document.documentElement;
// Check the current theme
if (html.getAttribute('data-bs-theme') === 'dark') {
html.removeAttribute('data-bs-theme'); // Switch to light mode
this.innerHTML = '🌞'; // Update the button to reflect light mode
localStorage.setItem('darkmode', 'false');
updateDarkModeOnServer('false');
} else {
html.setAttribute('data-bs-theme', 'dark'); // Switch to dark mode
this.innerHTML = '🌙'; // Update the button to reflect dark mode
localStorage.setItem('darkmode', 'true');
updateDarkModeOnServer('true');
}
});
}
function updateDarkModeOnServer(value) {
// Create a new AJAX request
var xhr = new XMLHttpRequest();
xhr.open('POST', '/settings.php', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
// Define what happens on successful data submission
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 400) {
console.log('Dark mode setting updated on server');
}
};
// Define what happens in case of an error
xhr.onerror = function() {
console.error('An error occurred during the request.');
};
// Set the action and send the request
xhr.send('action=darkmode&value=' + value);
}
</script>
</header>

View file

@ -1,8 +0,0 @@
<?php
ob_start();
if (session_id() == '') {
session_set_cookie_params(0, '/', '', true, true);
session_start();
}

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'مرحبا بكم في موقعنا على الإنترنت!',
'description' => 'هذا وصف بالإنكليزية.',
'desc' => 'Description',
'desc' => 'الوصف',
'userProfile' => "الملف الشخصي للمستخدم",
'user' => 'المستخدم',
'home' => 'المنزل',
@ -14,17 +14,17 @@ return [
'login' => 'تسجيل الدخول',
'message' => 'رسالة',
'follow' => 'اتبع',
'djs' => 'DJs',
'djs' => 'دي جي',
"djNotFound" => "تعذر تحميل DJ; إما لم يتم العثور على DJ أو هذا DJ خاص.",
"notfound" => "لم يتم العثور على الصفحة",
"genre" => "النوع",
"genres" => "Genres",
"genres" => "أنواع الموسيقى",
"genreNotFound" => "لا يمكن تحميل النوع الجيد؛ إما لم يتم العثور على هذا النوع، أو كان فارغا، أو هذا النوع خاص.",
"mix-count" => "عدد المزيج",
"mixes" => "خلط",
"mix" => "خليط",
"mixNotFound" => "تعذر تحميل المزيج ؛ إما لم يتم العثور على المزيج ، أو كان فارغا، أو هذا المزيج خاص.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "تعذر تحميل الخليط؛ إما لم يتم العثور على الخليط، أو كان فارغاً، أو هذا الخليط خاص.",
"mixName" => "اسم المزيج",
"mixDescription" => "وصف المزيج",
"mixLength" => "مزيج الطول",
@ -54,18 +54,23 @@ return [
"plays" => "مشغلات",
"play" => "تشغيل",
"contactus" => "اتصل بنا",
"allrightsreserved" => "جميع الحقوق محفوظة.format@@0",
"mixshows" => "Mixshows",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"allrightsreserved" => "جميع الحقوق محفوظة.",
"mixshows" => "المزج",
"mixshow" => "عرض ميكس",
"mixshowName" => "اسم المزج",
"share" => "مشاركة",
"sahrethismix" => "شارك هذا المزيج",
"sharethismixshow" => "شارك هذا المزيج",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "السنة",
"sharetotwitter" => "شارك إلى X (تويتر سابقاً)",
"sharetofb" => "مشاركة على Facebook",
"sharetoig" => "مشاركة على Instagram",
"copyurl" => "نسخ الرابط",
"urlcopiedtoclipboard" => "تم نسخ عنوان URL إلى الحافظة",
"failedtocopyurl" => "فشل في نسخ الرابط",
"name" => "الاسم",
"username" => "اسم المستخدم",
"logout" => "تسجيل الخروج",
"upload" => "تحميل",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Vítejte na našich webových stránkách!',
'description' => 'To je popis v angličtině.',
'desc' => 'Description',
'desc' => 'L 343, 22.12.2009, s. 1).',
'userProfile' => "Profil uživatele",
'user' => 'Uživatel',
'home' => 'Domů',
@ -24,7 +24,7 @@ return [
"mixes" => "Směsi",
"mix" => "Míchat",
"mixNotFound" => "Nelze načíst mix; buď směs nebyla nalezena, byla prázdná, nebo je tato směs soukromá.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Nelze načíst mixshow; buď mixshow nebyl nalezen, byl prázdný, nebo je tento mixshow soukromý.",
"mixName" => "Směsný název",
"mixDescription" => "Smíchat popis",
"mixLength" => "Míchat délku",
@ -55,17 +55,22 @@ return [
"play" => "Hrát",
"contactus" => "Kontaktujte nás",
"allrightsreserved" => "Všechna práva vyhrazena.",
"mixshows" => "Mixshows",
"mixshows" => "Směšovače",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Název Mixshow",
"share" => "Sdílet",
"sahrethismix" => "Sdílet tuto skladbu",
"sharethismixshow" => "Sdílet tento mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "Rok",
"sharetotwitter" => "Sdílet na X (dříve Twitter)",
"sharetofb" => "Sdílet na Facebook",
"sharetoig" => "Sdílet na Instagramu",
"copyurl" => "Kopírovat URL",
"urlcopiedtoclipboard" => "URL zkopírováno do schránky",
"failedtocopyurl" => "Kopírování URL se nezdařilo",
"name" => "Název",
"username" => "Uživatelské jméno",
"logout" => "Odhlásit se",
"upload" => "Nahrát",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Velkommen til vores hjemmeside!',
'description' => 'Dette er en beskrivelse på engelsk.',
'desc' => 'Description',
'desc' => 'Varebeskrivelse',
'userProfile' => "Bruger Profil",
'user' => 'Bruger',
'home' => 'Hjem',
@ -24,7 +24,7 @@ return [
"mixes" => "Blandinger",
"mix" => "Bland",
"mixNotFound" => "Kunne ikke indlæse mix. Enten blev blandingen ikke fundet, var tom, eller også er dette miks privat.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Kunne ikke indlæse mixshow; enten blev mixshowet ikke fundet, var tomt, eller dette mixshow er privat.",
"mixName" => "Mix Navn",
"mixDescription" => "Bland Beskrivelse",
"mixLength" => "Bland Længde",
@ -57,15 +57,20 @@ return [
"allrightsreserved" => "Alle rettigheder forbeholdt.",
"mixshows" => "Mixshows",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Mixshow Navn",
"share" => "Del",
"sahrethismix" => "Del dette mix",
"sharethismixshow" => "Del dette mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "År",
"sharetotwitter" => "Del til X (tidligere Twitter)",
"sharetofb" => "Del på Facebook",
"sharetoig" => "Del på Instagram",
"copyurl" => "Kopiér URL",
"urlcopiedtoclipboard" => "URL kopieret til udklipsholder",
"failedtocopyurl" => "Kopiering af URL mislykkedes",
"name" => "Navn",
"username" => "Brugernavn",
"logout" => "Log Ud",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Willkommen auf unserer Website!',
'description' => 'Dies ist eine Beschreibung auf Englisch.',
'desc' => 'Description',
'desc' => 'Beschreibung',
'userProfile' => "Benutzerprofil",
'user' => 'Benutzer',
'home' => 'Zuhause',
@ -24,7 +24,7 @@ return [
"mixes" => "Mischungen",
"mix" => "Mix",
"mixNotFound" => "Mix konnte nicht geladen werden; entweder wurde der Mix nicht gefunden, war leer oder dieser Mix ist privat.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Mixshow konnte nicht geladen werden; entweder wurde die Mixshow nicht gefunden, war leer oder diese Mixshow ist privat.",
"mixName" => "Mix-Name",
"mixDescription" => "Mix-Beschreibung",
"mixLength" => "Mix-Länge",
@ -55,17 +55,22 @@ return [
"play" => "Abspielen",
"contactus" => "Kontaktiere uns",
"allrightsreserved" => "Alle Rechte vorbehalten.",
"mixshows" => "Mixshows",
"mixshows" => "Mix-Shows",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Mix-Show-Name",
"share" => "Teilen",
"sahrethismix" => "Diesen Mix teilen",
"sharethismixshow" => "Diese Mixshow teilen",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "Jahr",
"sharetotwitter" => "Auf X teilen (ehemals Twitter)",
"sharetofb" => "Auf Facebook teilen",
"sharetoig" => "Auf Instagram teilen",
"copyurl" => "URL kopieren",
"urlcopiedtoclipboard" => "URL in Zwischenablage kopiert",
"failedtocopyurl" => "Fehler beim Kopieren der URL",
"name" => "Name",
"username" => "Benutzername",
"logout" => "Abmelden",
"upload" => "Hochladen",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Καλώς ήλθατε στην ιστοσελίδα μας!',
'description' => 'Αυτή είναι μια περιγραφή στα αγγλικά.',
'desc' => 'Description',
'desc' => 'Περιγραφή',
'userProfile' => "Προφίλ Χρήστη",
'user' => 'Χρήστης',
'home' => 'Αρχική',
@ -24,7 +24,7 @@ return [
"mixes" => "Μείγματα",
"mix" => "Μείγμα",
"mixNotFound" => "Δεν ήταν δυνατή η φόρτωση του μείγματος. Είτε το μείγμα δεν βρέθηκε, ήταν άδειο, είτε αυτό το μίγμα είναι ιδιωτικό.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Δεν ήταν δυνατή η φόρτωση του mixshow. Είτε το mixshow δεν βρέθηκε, ήταν άδειο, είτε αυτό το mixshow είναι ιδιωτικό.",
"mixName" => "Όνομα Μεικτού",
"mixDescription" => "Περιγραφή Μεικτού",
"mixLength" => "Μήκος Μείγματος",
@ -55,17 +55,22 @@ return [
"play" => "Αναπαραγωγή",
"contactus" => "Επικοινωνήστε Μαζί Μας",
"allrightsreserved" => "Με επιφύλαξη παντός δικαιώματος.",
"mixshows" => "Mixshows",
"mixshows" => "Μείγματα",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Όνομα Mixshow",
"share" => "Κοινοποίηση",
"sahrethismix" => "Μοιραστείτε αυτό το μίγμα",
"sharethismixshow" => "Μοιραστείτε αυτό το μίγμα",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "Έτος",
"sharetotwitter" => "Μοιραστείτε στο X (πρώην Twitter)",
"sharetofb" => "Κοινοποίηση στο Facebook",
"sharetoig" => "Κοινοποίηση στο Instagram",
"copyurl" => "Αντιγραφή URL",
"urlcopiedtoclipboard" => "Το URL αντιγράφηκε στο πρόχειρο",
"failedtocopyurl" => "Αποτυχία αντιγραφής URL",
"name" => "Όνομα",
"username" => "Όνομα Χρήστη",
"logout" => "Αποσύνδεση",
"upload" => "Ανέβασμα",
];

View file

@ -71,4 +71,17 @@ return [
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
"loginToVerifyEmail" => "You must be logged in to verify your email.",
"loginToUploadMix" => "You must be logged in to upload a mix.",
"verificationCodeRequired" => "Verification code is required.",
"recordedDate" => "Recorded Date",
"noUploadedFileFound" => "No uploaded file found. Please upload a mix file.",
"mixTitleRequired" => "Mix title is required.",
"errorUploadCDN" => "Error uploading file to CDN: ",
"errorSavingMixDB" => "Error saving mix to database.",
"uploadedPendingApproval" => "Mix uploaded successfully and is pending approval.",
"uploadHeader1" => "Upload your mix to Utah's DJs"
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => '¡Bienvenido a nuestro sitio web!',
'description' => 'This is a description in Spanish.',
'desc' => 'Description',
'desc' => 'Descripción',
'userProfile' => "Perfil de usuario",
'user' => 'Usuario',
'home' => 'Inicio',
@ -24,7 +24,7 @@ return [
"mixes" => "Mezclas",
"mix" => "Mezcla",
"mixNotFound" => "No se pudo cargar la mezcla; o bien la mezcla no fue encontrada, estaba vacía, o esta mezcla es privada.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "No se pudo cargar mixshow; o bien el mixshow no fue encontrado, estaba vacío, o este mixshow es privado.",
"mixName" => "Mezclar nombre",
"mixDescription" => "Mezclar descripción",
"mixLength" => "Mezclar longitud",
@ -57,15 +57,20 @@ return [
"allrightsreserved" => "Todos los derechos reservados.",
"mixshows" => "Mixshows",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Nombre de Mixshow",
"share" => "Compartir",
"sahrethismix" => "Compartir esta mezcla",
"sharethismixshow" => "Compartir este mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "Año",
"sharetotwitter" => "Compartir en X (anteriormente Twitter)",
"sharetofb" => "Compartir en Facebook",
"sharetoig" => "Compartir en Instagram",
"copyurl" => "Copiar URL",
"urlcopiedtoclipboard" => "URL copiada al portapapeles",
"failedtocopyurl" => "Error al copiar la URL",
"name" => "Nombre",
"username" => "Usuario",
"logout" => "Cerrar sesión",
"upload" => "Subir",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Tervetuloa sivustollemme!',
'description' => 'Tämä on englanninkielinen kuvaus.',
'desc' => 'Description',
'desc' => 'Kuvaus',
'userProfile' => "Käyttäjän Profiili",
'user' => 'Käyttäjä',
'home' => 'Koti',
@ -24,7 +24,7 @@ return [
"mixes" => "Sekoitukset",
"mix" => "Sekoita",
"mixNotFound" => "Sekoitusta ei voitu ladata. Sekoitus ei löytynyt, se oli tyhjä, tai tämä sekoitus on yksityinen.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Sekoitusnäyttöä ei voitu ladata. Sekoitusnäyttöä ei löytynyt, se oli tyhjä, tai tämä mixshow on yksityinen.",
"mixName" => "Sekoita Nimi",
"mixDescription" => "Sekoita Kuvaus",
"mixLength" => "Sekoita Pituus",
@ -57,15 +57,20 @@ return [
"allrightsreserved" => "Kaikki oikeudet pidätetään.",
"mixshows" => "Mixshows",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Mixshow Nimi",
"share" => "Jaa",
"sahrethismix" => "Jaa tämä sekoitus",
"sharethismixshow" => "Jaa tämä mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "Vuosi",
"sharetotwitter" => "Jaa kohtaan X (entinen Twitter)",
"sharetofb" => "Jaa Facebookiin",
"sharetoig" => "Jaa Instagramiin",
"copyurl" => "Kopioi URL",
"urlcopiedtoclipboard" => "URL kopioitu leikepöydälle",
"failedtocopyurl" => "URL-osoitteen kopiointi epäonnistui",
"name" => "Nimi",
"username" => "Käyttäjätunnus",
"logout" => "Kirjaudu Ulos",
"upload" => "Lähetä",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Bienvenue sur notre site Web!',
'description' => 'Ceci est une description en français.',
'desc' => 'Description',
'desc' => 'Libellé',
'userProfile' => "Profil de l'utilisateur",
'user' => 'Utilisateur',
'home' => 'Domicile',
@ -24,7 +24,7 @@ return [
"mixes" => "Mixes",
"mix" => "Mélanger",
"mixNotFound" => "Impossible de charger le mixage; soit le mixage n'a pas été trouvé, soit le mixage est vide, soit ce mixage est privé.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Impossible de charger le mixshow ; soit le mixshow n'a pas été trouvé, soit le mixshow est vide, soit ce mixshow est privé.",
"mixName" => "Nom du mixage",
"mixDescription" => "Description du mixage",
"mixLength" => "Longueur du mixage",
@ -57,15 +57,20 @@ return [
"allrightsreserved" => "Tous droits réservés.",
"mixshows" => "Mixshows",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"mixshowName" => "Nom du Mixshow",
"share" => "Partager",
"sahrethismix" => "Partager ce mix",
"sharethismixshow" => "Partager ce mixshow",
"rss" => "Flux RSS",
"year" => "Année",
"sharetotwitter" => "Partager vers X (anciennement Twitter)",
"sharetofb" => "Partager sur Facebook",
"sharetoig" => "Partager sur Instagram",
"copyurl" => "Copier l'URL",
"urlcopiedtoclipboard" => "URL copiée dans le presse-papiers",
"failedtocopyurl" => "Échec de la copie de l'URL",
"name" => "Nom",
"username" => "Nom d'utilisateur",
"logout" => "Déconnexion",
"upload" => "Charger",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Benvenuto nel nostro sito Web!',
'description' => 'Questa è una descrizione in inglese.',
'desc' => 'Description',
'desc' => 'Descrizione',
'userProfile' => "Profilo Utente",
'user' => 'Utente',
'home' => 'Home',
@ -24,7 +24,7 @@ return [
"mixes" => "Miscele",
"mix" => "Miscela",
"mixNotFound" => "Impossibile caricare il mix; o il mix non è stato trovato, è vuoto, o questo mix è privato.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Impossibile caricare mixshow; o il mixshow non è stato trovato, è stato vuoto, o questo mixshow è privato.",
"mixName" => "Nome Mix",
"mixDescription" => "Descrizione Mix",
"mixLength" => "Miscela Lunghezza",
@ -55,17 +55,22 @@ return [
"play" => "Gioca",
"contactus" => "Contattaci",
"allrightsreserved" => "Tutti i diritti riservati.",
"mixshows" => "Mixshows",
"mixshows" => "Mixshow",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Nome Mixshow",
"share" => "Condividi",
"sahrethismix" => "Condividi questo mix",
"sharethismixshow" => "Condividi questo mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "Anno",
"sharetotwitter" => "Condividi su X (ex Twitter)",
"sharetofb" => "Condividi su Facebook",
"sharetoig" => "Condividi su Instagram",
"copyurl" => "Copia URL",
"urlcopiedtoclipboard" => "URL copiato negli appunti",
"failedtocopyurl" => "Impossibile copiare l'URL",
"name" => "Nome",
"username" => "Username",
"logout" => "Esci",
"upload" => "Carica",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => '私たちのウェブサイトへようこそ!',
'description' => 'これは英語の説明です。',
'desc' => 'Description',
'desc' => '説明',
'userProfile' => "ユーザープロフィール",
'user' => 'ユーザー',
'home' => 'ホーム',
@ -24,7 +24,7 @@ return [
"mixes" => "ミックス",
"mix" => "ミックス",
"mixNotFound" => "ミックスをロードできませんでした。ミックスが見つかりませんでした。空であるか、このミックスがプライベートです。",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "mixshowをロードできませんでした。mixshowが見つかりませんでした。空か、このmixshowはプライベートです。",
"mixName" => "ミックス名",
"mixDescription" => "ミックスの説明",
"mixLength" => "ミックス長さ",
@ -55,17 +55,22 @@ return [
"play" => "再生",
"contactus" => "お問い合わせ",
"allrightsreserved" => "All rights reserved.",
"mixshows" => "Mixshows",
"mixshows" => "ミックスショー",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"share" => "共有",
"sahrethismix" => "このミックスを共有",
"sharethismixshow" => "このミックスショーを共有",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "",
"sharetotwitter" => "X(旧 Twitter)で共有",
"sharetofb" => "Facebookで共有",
"sharetoig" => "Instagramで共有",
"copyurl" => "URLをコピー",
"urlcopiedtoclipboard" => "URL をクリップボードにコピーしました",
"failedtocopyurl" => "URLのコピーに失敗しました",
"name" => "名前",
"username" => "ユーザー名",
"logout" => "ログアウト",
"upload" => "アップロード",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Welkom op onze Website!',
'description' => 'Dit is een beschrijving in het Engels.',
'desc' => 'Description',
'desc' => 'Beschrijving',
'userProfile' => "Gebruikers Profiel",
'user' => 'Gebruiker',
'home' => 'Startpagina',
@ -24,7 +24,7 @@ return [
"mixes" => "Mixen",
"mix" => "Mengen",
"mixNotFound" => "Kon mixen niet laden; of de mix is niet gevonden, was leeg, of deze mix is privé.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Kon mixshow niet laden; of de mixshow is niet gevonden, was leeg, of deze mixshow is privé.",
"mixName" => "Mix Naam",
"mixDescription" => "Beschrijving mixen",
"mixLength" => "Mix Lengte",
@ -55,17 +55,22 @@ return [
"play" => "Afspelen",
"contactus" => "Contacteer ons",
"allrightsreserved" => "Alle rechten voorbehouden.format@@0",
"mixshows" => "Mixshows",
"mixshows" => "Mixseries",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Mixshow Naam",
"share" => "Delen",
"sahrethismix" => "Deel deze mix",
"sharethismixshow" => "Deel deze mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "jaar",
"sharetotwitter" => "Deel met X (voorheen Twitter)",
"sharetofb" => "Delen op Facebook",
"sharetoig" => "Delen op Instagram",
"copyurl" => "URL kopiëren",
"urlcopiedtoclipboard" => "URL gekopieerd naar klembord",
"failedtocopyurl" => "Kopiëren van URL mislukt",
"name" => "naam",
"username" => "Gebruikersnaam",
"logout" => "Afmelden",
"upload" => "Uploaden",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Velkommen til vår hjemmeside!',
'description' => 'Dette er en beskrivelse på engelsk.',
'desc' => 'Description',
'desc' => 'Beskrivelse',
'userProfile' => "Bruker profil",
'user' => 'Bruker',
'home' => 'Hjem',
@ -24,7 +24,7 @@ return [
"mixes" => "Mixes",
"mix" => "Bland",
"mixNotFound" => "Kunne ikke laste blanding, verken var blandingen tom, eller så var denne blandingen privat.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Kan ikke laste mixshow; verken ble ikke funnet, var tom, eller denne blandingen er privat.",
"mixName" => "Blandet navn",
"mixDescription" => "Bland beskrivelse",
"mixLength" => "Blandet lengde",
@ -57,15 +57,20 @@ return [
"allrightsreserved" => "Med enerett.",
"mixshows" => "Mixshows",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Mixshow navn",
"share" => "Del",
"sahrethismix" => "Del denne blandingen",
"sharethismixshow" => "Del denne blandingen",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "År",
"sharetotwitter" => "Del til X (tidligere Twitter)",
"sharetofb" => "Del på Facebook",
"sharetoig" => "Del på Instagram",
"copyurl" => "Kopier URL",
"urlcopiedtoclipboard" => "URL kopiert til utklippstavlen",
"failedtocopyurl" => "Klarte ikke å kopiere URL",
"name" => "Navn",
"username" => "Brukernavn",
"logout" => "Logg",
"upload" => "Last opp",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Witamy na naszej stronie internetowej!',
'description' => 'To jest opis w języku angielskim.',
'desc' => 'Description',
'desc' => 'Opis',
'userProfile' => "Profil użytkownika",
'user' => 'Użytkownik',
'home' => 'Strona główna',
@ -24,7 +24,7 @@ return [
"mixes" => "Mixy",
"mix" => "Mieszanina",
"mixNotFound" => "Nie można załadować miksy; albo mieszanina nie została znaleziona, była pusta lub ta miksy jest prywatna.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Nie można załadować mixshow; albo mixshow nie został znaleziony, był pusty, albo mixshow jest prywatny.",
"mixName" => "Nazwa mixu",
"mixDescription" => "Opis mieszaniny",
"mixLength" => "Długość mieszania",
@ -55,17 +55,22 @@ return [
"play" => "Odtwórz",
"contactus" => "Skontaktuj się z nami",
"allrightsreserved" => "Wszystkie prawa zastrzeżone.",
"mixshows" => "Mixshows",
"mixshows" => "Mieszanki",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Nazwa Mixshow",
"share" => "Udostępnij",
"sahrethismix" => "Udostępnij ten koszyk",
"sharethismixshow" => "Udostępnij ten mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "W związku z tym Komisja stwierdza, że środek 1 stanowi pomoc państwa w rozumieniu art. 107 ust. 1 Traktatu.",
"sharetotwitter" => "Udostępnij do X (dawniej Twitter)",
"sharetofb" => "Udostępnij na Facebooku",
"sharetoig" => "Udostępnij na Instagramie",
"copyurl" => "Kopiuj adres URL",
"urlcopiedtoclipboard" => "Adres URL skopiowany do schowka",
"failedtocopyurl" => "Nie udało się skopiować adresu URL",
"name" => "Nazwisko",
"username" => "Nazwa użytkownika",
"logout" => "Wyloguj się",
"upload" => "Prześlij",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Bem-vindo ao nosso site!',
'description' => 'Esta é uma descrição em inglês.',
'desc' => 'Description',
'desc' => 'Descrição:',
'userProfile' => "Informações do Perfil",
'user' => 'Usuário',
'home' => 'Residencial',
@ -24,7 +24,7 @@ return [
"mixes" => "Misturar",
"mix" => "Mistura",
"mixNotFound" => "Não foi possível carregar o mix; ou a mistura não foi encontrada, estava vazia, ou esta mistura é privada.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Não foi possível carregar o mixshow; ou o mixshow não foi encontrado, estava vazio ou este mixshow é privado.",
"mixName" => "Nome do mix",
"mixDescription" => "Descrição Misto",
"mixLength" => "Comprimento Misturado",
@ -55,17 +55,22 @@ return [
"play" => "Reproduzir",
"contactus" => "Entre em contato",
"allrightsreserved" => "Todos os direitos reservados.",
"mixshows" => "Mixshows",
"mixshows" => "Misturas",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"mixshowName" => "Nome do Mixshow",
"share" => "Compartilhar",
"sahrethismix" => "Compartilhe esta mistura",
"sharethismixshow" => "Compartilhar este mixshow",
"rss" => "Resposta",
"year" => "ano",
"sharetotwitter" => "Compartilhar para X (antigo Twitter)",
"sharetofb" => "Compartilhar no Facebook",
"sharetoig" => "Compartilhar com o Instagram",
"copyurl" => "Copiar URL",
"urlcopiedtoclipboard" => "URL copiado para área de transferência",
"failedtocopyurl" => "Falha ao copiar URL",
"name" => "Nome:",
"username" => "Usuário:",
"logout" => "Desconectar",
"upload" => "Transferir",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Bem-vindo ao nosso site!',
'description' => 'Esta é uma descrição em inglês.',
'desc' => 'Description',
'desc' => 'Descrição:',
'userProfile' => "Informações do Perfil",
'user' => 'Usuário',
'home' => 'Residencial',
@ -24,7 +24,7 @@ return [
"mixes" => "Misturar",
"mix" => "Mistura",
"mixNotFound" => "Não foi possível carregar o mix; ou a mistura não foi encontrada, estava vazia, ou esta mistura é privada.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Não foi possível carregar o mixshow; ou o mixshow não foi encontrado, estava vazio ou este mixshow é privado.",
"mixName" => "Nome do mix",
"mixDescription" => "Descrição Misto",
"mixLength" => "Comprimento Misturado",
@ -55,17 +55,22 @@ return [
"play" => "Reproduzir",
"contactus" => "Entre em contato",
"allrightsreserved" => "Todos os direitos reservados.",
"mixshows" => "Mixshows",
"mixshows" => "Misturas",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"mixshowName" => "Nome do Mixshow",
"share" => "Compartilhar",
"sahrethismix" => "Compartilhe esta mistura",
"sharethismixshow" => "Compartilhar este mixshow",
"rss" => "Resposta",
"year" => "ano",
"sharetotwitter" => "Compartilhar para X (antigo Twitter)",
"sharetofb" => "Compartilhar no Facebook",
"sharetoig" => "Compartilhar com o Instagram",
"copyurl" => "Copiar URL",
"urlcopiedtoclipboard" => "URL copiado para área de transferência",
"failedtocopyurl" => "Falha ao copiar URL",
"name" => "Nome:",
"username" => "Usuário:",
"logout" => "Desconectar",
"upload" => "Transferir",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Bun venit pe site-ul nostru!',
'description' => 'Aceasta este o descriere în engleză.',
'desc' => 'Description',
'desc' => 'Descriere',
'userProfile' => "Profil utilizator",
'user' => 'Utilizator',
'home' => 'Acasă',
@ -24,7 +24,7 @@ return [
"mixes" => "Mixuri",
"mix" => "Amestecă",
"mixNotFound" => "Nu s-a putut încărca mixul; fie mixul nu a fost găsit, a fost gol, fie acest mix este privat.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Nu s-a putut încărca mixshow-ul; fie emisiunea mixtă nu a fost găsită, a fost goală, fie acest spectacol mixt este privat.",
"mixName" => "Nume mix",
"mixDescription" => "Descriere mixtă",
"mixLength" => "Amestecă lungimea",
@ -55,17 +55,22 @@ return [
"play" => "Redare",
"contactus" => "Contactează-ne",
"allrightsreserved" => "Toate drepturile rezervate.",
"mixshows" => "Mixshows",
"mixshows" => "Amestec",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Nume Mixshow",
"share" => "Distribuie",
"sahrethismix" => "Distribuie acest mix",
"sharethismixshow" => "Distribuie acest spectacol mixt",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "An",
"sharetotwitter" => "Distribuie pe X (anterior Twitter)",
"sharetofb" => "Distribuie pe Facebook",
"sharetoig" => "Distribuie pe Instagram",
"copyurl" => "Copiază URL-ul",
"urlcopiedtoclipboard" => "URL copiat în clipboard",
"failedtocopyurl" => "Copierea adresei URL a eșuat",
"name" => "Nume",
"username" => "Nume",
"logout" => "Deconectare",
"upload" => "Incarca",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Добро пожаловать на наш сайт!',
'description' => 'Это описание на английском языке.',
'desc' => 'Description',
'desc' => 'Описание',
'userProfile' => "Профиль пользователя",
'user' => 'Пользователь',
'home' => 'Домашний',
@ -24,7 +24,7 @@ return [
"mixes" => "Миксы",
"mix" => "Микс",
"mixNotFound" => "Не удалось загрузить смесь; либо смесь не найдена, либо эта смесь является приватной.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Не удалось загрузить mixshow; либо mixshow не найден, либо эта mixshow является приватной.",
"mixName" => "Название микса",
"mixDescription" => "Описание смеси",
"mixLength" => "Длина микса",
@ -55,17 +55,22 @@ return [
"play" => "Играть",
"contactus" => "Свяжитесь с нами",
"allrightsreserved" => "Все права защищены.",
"mixshows" => "Mixshows",
"mixshows" => "Смешанные шоу",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"mixshowName" => "Название Mixshow",
"share" => "Поделиться",
"sahrethismix" => "Поделиться этой смесью",
"sharethismixshow" => "Поделиться этой смесительной шоу",
"rss" => "RSS-лента",
"year" => "Год",
"sharetotwitter" => "Поделиться с X (ранее Twitter)",
"sharetofb" => "Поделиться на Facebook",
"sharetoig" => "Поделиться в Instagram",
"copyurl" => "Копировать URL",
"urlcopiedtoclipboard" => "URL скопирован в буфер обмена",
"failedtocopyurl" => "Не удалось скопировать URL",
"name" => "Наименование",
"username" => "Имя пользователя",
"logout" => "Выйти",
"upload" => "Выгрузить",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Välkommen till vår webbplats!',
'description' => 'Detta är en beskrivning på engelska.',
'desc' => 'Description',
'desc' => 'Beskrivning',
'userProfile' => "Användarprofil",
'user' => 'Användare',
'home' => 'Hem',
@ -24,7 +24,7 @@ return [
"mixes" => "Blandningar",
"mix" => "Blanda",
"mixNotFound" => "Kunde inte ladda mix; antingen hittades blandningen inte, var tom, eller denna blandning är privat.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Kunde inte ladda mixshow; antingen hittades inte mixshowen, var tom, eller så är denna mixshow privat.",
"mixName" => "Blanda namn",
"mixDescription" => "Blanda beskrivning",
"mixLength" => "Blanda längd",
@ -55,17 +55,22 @@ return [
"play" => "Spela",
"contactus" => "Kontakta oss",
"allrightsreserved" => "Alla rättigheter reserverade.",
"mixshows" => "Mixshows",
"mixshows" => "Blandningsserier",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Mixseriens namn",
"share" => "Dela",
"sahrethismix" => "Dela denna mix",
"sharethismixshow" => "Dela denna mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "År",
"sharetotwitter" => "Dela till X (tidigare Twitter)",
"sharetofb" => "Dela på Facebook",
"sharetoig" => "Dela på Instagram",
"copyurl" => "Kopiera URL",
"urlcopiedtoclipboard" => "URL kopierad till urklipp",
"failedtocopyurl" => "Kunde inte kopiera URL",
"name" => "Namn",
"username" => "Användarnamn",
"logout" => "Utloggning",
"upload" => "Ladda upp",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => 'Ласкаво просимо на наш сайт!',
'description' => 'Це опис англійською.',
'desc' => 'Description',
'desc' => 'Опис',
'userProfile' => "Профіль користувача",
'user' => 'Користувач',
'home' => 'Домашній екран',
@ -24,7 +24,7 @@ return [
"mixes" => "Змішати",
"mix" => "Змішати",
"mixNotFound" => "Не вдалось завантажити мікс; або суміш не знайдена, була порожня, або ця суміш приватна.",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "Не вдалося завантажити шоу, або не знайдене змішування, було порожнім, або це змішання є приватним.",
"mixName" => "Змішане ім'я",
"mixDescription" => "Змішати опис",
"mixLength" => "Довжина мікса",
@ -55,17 +55,22 @@ return [
"play" => "Відтворити",
"contactus" => "Зв’язатись з нами",
"allrightsreserved" => "Усі права захищені.",
"mixshows" => "Mixshows",
"mixshows" => "Змішані серіали",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "Змішане ім'я",
"share" => "Поділитись",
"sahrethismix" => "Поділитися цим мікшуванням",
"sharethismixshow" => "Поділитися цим мікшалом",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "Рік",
"sharetotwitter" => "Поділитись до X (раніше Twitter)",
"sharetofb" => "Поділитись у Facebook",
"sharetoig" => "Поділитися в Instagram",
"copyurl" => "Копіювати посилання",
"urlcopiedtoclipboard" => "URL скопійовано до буферу обміну",
"failedtocopyurl" => "Не вдалося скопіювати URL-адресу",
"name" => "Ім'я",
"username" => "Ім'я користувача",
"logout" => "Вихід із системи",
"upload" => "Вивантажити",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -2,7 +2,7 @@
return [
'welcome' => '欢迎来到我们的网站!',
'description' => '这是英文描述。',
'desc' => 'Description',
'desc' => '描述',
'userProfile' => "用户资料",
'user' => '用户',
'home' => '首页',
@ -24,7 +24,7 @@ return [
"mixes" => "混音器",
"mix" => "混音器",
"mixNotFound" => "无法加载混合物;混合物未找到, 为空, 或者这种混合是私有的。",
"mixshowNotFound" => "Could not load mixshow; either the mixshow wasn't found, was empty, or this mixshow is private.",
"mixshowNotFound" => "无法加载mixshow要么找不到mixshow要么是空的要么这个mixshow是私有的。",
"mixName" => "混合名称",
"mixDescription" => "混合描述",
"mixLength" => "混合长度",
@ -55,17 +55,22 @@ return [
"play" => "播放",
"contactus" => "联系我们",
"allrightsreserved" => "版权所有。",
"mixshows" => "Mixshows",
"mixshows" => "混合显示",
"mixshow" => "Mixshow",
"mixshowName" => "Mixshow Name",
"share" => "Share",
"sahrethismix" => "Share this mix",
"sharethismixshow" => "Share this mixshow",
"mixshowName" => "混合节目名称",
"share" => "分享",
"sahrethismix" => "分享这个组合",
"sharethismixshow" => "分享此 mixshow",
"rss" => "RSS",
"year" => "Year",
"sharetotwitter" => "Share to X (formerly Twitter)",
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"year" => "年份",
"sharetotwitter" => "分享到 X (旧的 Twitter)",
"sharetofb" => "分享到 Facebook",
"sharetoig" => "分享到 Instagram",
"copyurl" => "复制 URL",
"urlcopiedtoclipboard" => "URL 已复制到剪贴板",
"failedtocopyurl" => "无法复制 URL",
"name" => "名称",
"username" => "用户名",
"logout" => "注销",
"upload" => "上传",
];

View file

@ -67,5 +67,10 @@ return [
"sharetofb" => "Share to Facebook",
"sharetoig" => "Share to Instagram",
"copyurl" => "Copy URL",
"urlcopiedtoclipboard" => "URL copied to clipboard",
"failedtocopyurl" => "Failed to copy URL",
"name" => "Name",
"username" => "Username",
"logout" => "Logout",
"upload" => "Upload",
];

View file

@ -1,15 +1,16 @@
<?php
require_once 'includes/globals.php';
if (isset($_SESSION['user'])) {
header("Location: /profile");
exit;
}
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use DJMixHosting\User;
use DJMixHosting\SessionManager;
// If a user is already logged in, redirect to profile.
if (SessionManager::getUser()) {
header("Location: /profile");
exit;
}
// Generate a CSRF token if one is not set
if (!isset($_SESSION['csrf_token'])) {
@ -19,9 +20,7 @@ if (!isset($_SESSION['csrf_token'])) {
$title = $locale['home'];
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if (isset($_POST['email'], $_POST['password'], $_POST['csrf_token'])) {
// Check the CSRF token
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) {
$_SESSION['error'] = $locale['message'] . ": Invalid form submission. Please try again.";
@ -32,32 +31,42 @@ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$db = new Database($config);
$user = new User($db);
$result = $user->login($email, $password);
if ($result === true) {
// Successful login, redirect to profile page
// If login() returns an array, the login was successful.
if (is_array($result)) {
SessionManager::setUser([
'id' => $result['id'],
'email' => $result['email'],
'username' => $result['username'],
'firstName' => $result['firstName'],
'lastName' => $result['lastName'],
'role' => $result['isAdmin'] ? 'admin' : 'user'
]);
header("Location: profile.php");
exit;
} else {
// Set error message from login method (includes lockout messages)
// Login failed; $result contains an error message.
$_SESSION['error'] = $result;
}
}
}
}
require_once 'includes/header.php';
?>
<section class="login-section py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<?php
if (isset($_SESSION['error'])) {
if (isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">
' . htmlspecialchars($_SESSION['error']) . '
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>';
unset($_SESSION['error']);
}
?>
}
?>
<section class="login-section py-5">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-5">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="text-center mb-4">Login</h3>
@ -97,30 +106,21 @@ require_once 'includes/header.php';
</section>
<style>
.login-section {
min-height: calc(100vh - 200px); /* Adjust based on your header/footer height */
}
.form-control:focus {
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.15);
}
#togglePassword:hover {
background-color: #f8f9fa;
}
#togglePassword:focus {
box-shadow: none;
}
.input-group .form-control:focus {
border-right: 1px solid #86b7fe;
}
.input-group .btn {
border-left: 0;
}
</style>
<script>

14
logout.php Normal file
View file

@ -0,0 +1,14 @@
<?php
require_once 'includes/globals.php';
use DJMixHosting\SessionManager;
// Start the session (if not already started)
SessionManager::start();
// Destroy the session using the SessionManager
SessionManager::destroy();
// Redirect to the homepage
header("Location: /");
exit();

102
mix.php
View file

@ -13,9 +13,9 @@ $db = new Database($config);
$mixFound = false;
if (isset($_GET['mix']) && $_GET['mix'] != "") {
$mix = new Mix($_GET['mix'], $db);
if ($mix->get_name() != "") {
if ($mix->getName() != "") {
$mixFound = true;
$title = $mix->get_name();
$title = $mix->getName();
} else {
$title = $locale['notfound'];
}
@ -42,8 +42,8 @@ require_once 'includes/header.php'; ?>
<li class="breadcrumb-item"><?php echo $locale['mixes']; ?></li>
<li class="breadcrumb-item active"
aria-current="page"><?php
if (isset($mix) && $mix->get_name() != "") {
echo $mix->get_name();
if (isset($mix) && $mix->getName() != "") {
echo $mix->getName();
} else {
echo $locale['notfound'];
}
@ -59,14 +59,14 @@ require_once 'includes/header.php'; ?>
<div class="card mb-4">
<div class="card-body bg-body-secondary text-center">
<?php
if ($mix->get_img() != "") {
echo "<img src='" . $mix->get_img() . "' alt='avatar' class='img-fluid' style='width: 150px;'>";
if ($mix->getCover() != "") {
echo "<img src='" . $mix->getCover() . "' alt='avatar' class='img-fluid' style='width: 150px;'>";
} ?>
<h1 class="my-3 fs-4"><?php echo $mix->get_name();
<h1 class="my-3 fs-4"><?php echo $mix->getName();
?></h1>
<?php
if ($mix->get_description() != "") {
echo "<h2 class='text-muted mb-4 fs-6'>" . $mix->get_description() . "</h2>";
if ($mix->getDescription() != "") {
echo "<h2 class='text-muted mb-4 fs-6'>" . $mix->getDescription() . "</h2>";
}
?>
</p>
@ -75,12 +75,12 @@ require_once 'includes/header.php'; ?>
<div class="card mb-4">
<div class="card-body bg-body-secondary text-center">
<?php
if ($mix->is_download_only()) {
echo "<a href='/mix/" . $mix->get_slug() . "/download" . "' class='btn btn-primary w-100 mb-2'>" . $locale['download'] . "</a>";
if ($mix->isDownloadOnly()) {
echo "<a href='/mix/" . $mix->getSlug() . "/download" . "' class='btn btn-primary w-100 mb-2'>" . $locale['download'] . "</a>";
} else {
?>
<div id="audio-player">
<audio id="audio" src="<?php echo $mix->get_url(); ?>"></audio>
<audio id="audio" src="<?php echo $mix->getUrl(); ?>"></audio>
<div class="player-controls">
<button id="play-pause-btn">
<i class="fas fa-play" style="font-size: 12px;"></i>
@ -101,10 +101,10 @@ require_once 'includes/header.php'; ?>
<?php echo $locale['share']; ?>
</button>
<?php if (!$mix->is_download_only()) : ?>
<?php if (!$mix->isDownloadOnly()) : ?>
<a href="<?php
echo "/mix/" . $mix->get_slug() . "/download";
echo "/mix/" . $mix->getSlug() . "/download";
?>"
class="btn btn-primary w-100 mb-2"><?php echo $locale['download']; ?></a>
@ -121,7 +121,7 @@ require_once 'includes/header.php'; ?>
<p class="mb-0"><?php echo $locale['mixname'] ?></p>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0"><?php echo $mix->get_name(); ?></p>
<p class="text-muted mb-0"><?php echo $mix->getName(); ?></p>
</div>
</div>
<hr>
@ -134,7 +134,7 @@ require_once 'includes/header.php'; ?>
<?php
// loop through the $mix['djs'] array and output them in comma separated format
$djs = $mix->get_djs();
$djs = $mix->getDJs();
$djCount = count($djs);
$i = 0;
foreach ($djs as $dj) {
@ -153,7 +153,7 @@ require_once 'includes/header.php'; ?>
</div>
</div>
<?php
$genres = $mix->get_genres();
$genres = $mix->getGenres();
$genreCount = count($genres);
if ($genreCount > 0) {
?>
@ -181,7 +181,7 @@ require_once 'includes/header.php'; ?>
</div><?php } ?>
<?php
$mixshows = $mix->get_mixshow();
$mixshows = $mix->getMixshow();
$mixshowsCount = count($mixshows);
if ($mixshowsCount > 0) {
?>
@ -215,7 +215,7 @@ require_once 'includes/header.php'; ?>
<div class="col-sm-9">
<p class="text-muted mb-0">
<?php
$time = $mix->get_duration();
$time = $mix->getDuration();
// Decide the correct singular or plural term
$hour_text = $time['h'] == 1 ? $locale['hour'] : $locale['hours'];
$minute_text = $time['m'] == 1 ? $locale['minute'] : $locale['minutes'];
@ -245,7 +245,7 @@ require_once 'includes/header.php'; ?>
</div>
</div>
<?php if ($mix->get_recorded() != ""): ?>
<?php if ($mix->getRecorded() != ""): ?>
<hr>
<div class="row">
<div class="col-sm-3">
@ -253,11 +253,11 @@ require_once 'includes/header.php'; ?>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">
<?php echo $mix->get_recorded(); ?>
<?php echo $mix->getRecorded(); ?>
</div>
</div>
<?php endif; ?>
<?php if ($mix->get_created() != ""): ?>
<?php if ($mix->getCreated() != ""): ?>
<hr>
<div class="row">
<div class="col-sm-3">
@ -265,7 +265,7 @@ require_once 'includes/header.php'; ?>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">
<?php echo $mix->get_created(); ?>
<?php echo $mix->getCreated(); ?>
</div>
</div>
<?php endif; ?>
@ -276,7 +276,7 @@ require_once 'includes/header.php'; ?>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">
<?php echo $mix->get_downloads(); ?>
<?php echo $mix->getDownloads(); ?>
</div>
</div>
<hr>
@ -286,10 +286,10 @@ require_once 'includes/header.php'; ?>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">
<?php echo $mix->get_plays(); ?>
<?php echo $mix->getPlaycount(); ?>
</div>
</div>
<?php if ($mix->get_updated() != ""): ?>
<?php if ($mix->getUpdated() != ""): ?>
<hr>
<div class="row">
<div class="col-sm-3">
@ -297,7 +297,7 @@ require_once 'includes/header.php'; ?>
</div>
<div class="col-sm-9">
<p class="text-muted mb-0">
<?php echo $mix->get_updated(); ?>
<?php echo $mix->getUpdated(); ?>
</div>
</div>
<script>
@ -307,9 +307,9 @@ require_once 'includes/header.php'; ?>
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: '<?php echo addslashes($mix->get_name()); ?>',
title: '<?php echo addslashes($mix->getName()); ?>',
artist: '<?php
$djs = $mix->get_djs();
$djs = $mix->getDJs();
$djCount = count($djs);
$i = 0;
$djnamelist = [];
@ -319,15 +319,15 @@ require_once 'includes/header.php'; ?>
}
echo addslashes(implode(", ", $djnamelist));?>',
album: '<?php echo addslashes($mix->get_name()); ?>',
album: '<?php echo addslashes($mix->getName()); ?>',
artwork: [
{
src: '<?php echo $mix->get_cover('small'); ?>',
src: '<?php echo $mix->getCover('small'); ?>',
sizes: '96x96',
type: 'image/jpeg'
},
{
src: '<?php echo $mix->get_cover('large'); ?>',
src: '<?php echo $mix->getCover('large'); ?>',
sizes: '128x128',
type: 'image/jpeg'
}
@ -359,12 +359,12 @@ require_once 'includes/header.php'; ?>
</div>
</div>
<?php
if ($mix->get_tracklist() != []) {
if ($mix->getTracklist() != []) {
echo "<div class='card mb-4 bg-body-secondary'>";
echo "<div class='card-body '>";
echo "<p class='mb-4'><span class='text-primary font-italic me-1'>" . $locale['tracklist'] . "</span></p>";
echo "<ul class='list-group list-group-flush rounded-3 bg-body-secondary'>";
$tracklist = $mix->get_tracklist();
$tracklist = $mix->getTracklist();
foreach ($tracklist as $track) {
echo "<li class='list-group-item bg-body-secondary d-flex justify-content-between align-items-center'>";
echo $track;
@ -443,7 +443,7 @@ require_once 'includes/header.php'; ?>
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({mix: '<?php echo $mix->get_id(); ?>'}),
body: JSON.stringify({mix: '<?php echo $mix->getId(); ?>'}),
})
.then(response => response.json())
.then(data => {
@ -461,52 +461,52 @@ require_once 'includes/header.php'; ?>
{
"@context": "https://schema.org",
"@type": "MusicRecording",
"name": "<?php echo $mix->get_name(); ?>",
"name": "<?php echo $mix->getName(); ?>",
"byArtist": {
"@type": "MusicGroup",
"name": "<?php
$djs = $mix->get_djs();
$djs = $mix->getDJs();
echo $djs[0]->getName();
?>",
"image": "<?php echo $djs[0]->getImg(); ?>"
},
"inAlbum": {
"@type": "MusicAlbum",
"name": "<?php echo $mix->get_name(); ?>"
"name": "<?php echo $mix->getName(); ?>"
},
"genre": "<?php
$genre = new Genre($mix->get_genres()[0], $db);
$genre = new Genre($mix->getGenres()[0], $db);
echo $genre->get_name();
?>",
"url": "<?php echo "https://utahsdjs.com/mix/" . $mix->get_slug(); ?>",
"image": "<?php echo $mix->get_cover(); ?>",
"url": "<?php echo "https://utahsdjs.com/mix/" . $mix->getSlug(); ?>",
"image": "<?php echo $mix->getCover(); ?>",
"duration": "<?php echo $mix->get_duration()['S']; ?>",
"duration": "<?php echo $mix->getDuration()['S']; ?>",
<?php
// if recorded is empty, use created; if created is empty, use 2008-01-01;
if (empty($mix->get_recorded())) {
if (empty($mix->get_created())) {
if (empty($mix->getRecorded())) {
if (empty($mix->getCreated())) {
$recorded = '2008-01-01 00:00:00';
} else {
$recorded = $mix->get_created();
$recorded = $mix->getCreated();
}
} else {
$recorded = $mix->get_recorded();
$recorded = $mix->getRecorded();
} ?>
"datePublished": "<?php echo $recorded; ?>",
"description": "<?php
if (empty($mix->get_description())) {
$description = 'Listen to ' . $mix->get_name() . ' on Utah\'s DJs.';
if (empty($mix->getDescription())) {
$description = 'Listen to ' . $mix->getName() . ' on Utah\'s DJs.';
}
echo $mix->get_description(); ?>",
echo $mix->getDescription(); ?>",
"interactionStatistic": {
"@type": "InteractionCounter",
"interactionType": "https://schema.org/ListenAction",
"userInteractionCount": "<?php echo $mix->get_plays() + $mix->get_downloads() ?>",
"url": "<?php echo "https://utahsdjs.com/mix/" . $mix->get_slug() . "/download"; ?>"
"userInteractionCount": "<?php echo $mix->getPlaycount() + $mix->getDownloads() ?>",
"url": "<?php echo "https://utahsdjs.com/mix/" . $mix->getSlug() . "/download"; ?>"
}
}

BIN
phing-latest.phar Normal file

Binary file not shown.

View file

@ -1,43 +1,116 @@
<?php
// profile.php
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
// Make sure the user is authenticated; otherwise redirect to login.
if (!isset($_SESSION['user'])) {
use DJMixHosting\Database;
use DJMixHosting\User;
use DJMixHosting\SessionManager;
// Ensure the session is started and the user is authenticated.
SessionManager::start();
if (!SessionManager::getUser()) {
header("Location: login.php");
exit;
}
use DJMixHosting\Database;
$db = new Database($config);
$userId = SessionManager::getUser()['id'];
// Instantiate the User object; this loads user details internally.
$user = new User($db, $userId);
// Retrieve the full user record from the database
$userId = $_SESSION['user']['id'];
$stmt = $db->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
$userData = $result->fetch_assoc();
$stmt->close();
// Determine if editing should be disabled (if email not verified)
$editingDisabled = (int)$userData['emailVerified'] !== 1; // Assuming 1 means verified
// Optionally, set a flag for showing an alert to verify email.
$alertMessage = "";
if ($editingDisabled) {
$alertMessage = "Please verify your email to enable profile editing.";
// Handle POST requests for profile updates.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
try {
if (!isset($_POST['action'])) {
throw new Exception("No action specified.");
}
$action = $_POST['action'];
switch ($action) {
case 'updateUsername':
$newUsername = trim($_POST['new_username'] ?? '');
$message = $user->updateUsername($newUsername);
$_SESSION['success'] = $message;
break;
case 'updateEmail':
$newEmail = trim($_POST['new_email'] ?? '');
$message = $user->updateEmail($newEmail, $config);
$_SESSION['success'] = $message;
break;
case 'updateName':
$firstName = trim($_POST['first_name'] ?? '');
$lastName = trim($_POST['last_name'] ?? '');
$message = $user->updateName($firstName, $lastName);
$_SESSION['success'] = $message;
break;
case 'updatePassword':
$currentPassword = $_POST['current_password'] ?? '';
$newPassword = $_POST['new_password'] ?? '';
$confirmPassword = $_POST['confirm_password'] ?? '';
$message = $user->updatePassword($currentPassword, $newPassword, $confirmPassword);
$_SESSION['success'] = $message;
break;
// Optionally, handle other actions such as profile picture update.
default:
throw new Exception("Invalid action.");
}
} catch (Exception $e) {
$_SESSION['error'] = $e->getMessage();
}
header("Location: profile.php");
exit;
}
// Retrieve user data using getter methods.
$userData = [
'id' => $user->getId(),
'username' => $user->getUsername(),
'firstName' => $user->getFirstName(),
'lastName' => $user->getLastName(),
'email' => $user->getEmail(),
'img' => $user->getImg(),
'emailVerified' => $user->getVerified(),
];
// Determine if editing should be disabled (e.g., if email not verified)
$editingDisabled = ((int)$userData['emailVerified'] !== 1);
$alertMessage = $editingDisabled ? "Please verify your email to enable profile editing." : "";
require_once 'includes/header.php';
?>
<section>
<div class="container py-5">
<div class="row">
<div class="col">
<nav aria-label="breadcrumb" class="bg-body-tertiary rounded-3 p-3 mb-4">
<ol class="breadcrumb mb-0">
<li class="breadcrumb-item"><a href="/"><?php echo $locale['home']; ?></a></li>
<li class="breadcrumb-item active" aria-current="page">Profile</li>
</ol>
</nav>
</div>
</div>
<!-- Display Flash Alerts -->
<?php if (isset($_SESSION['error'])): ?>
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<?php echo htmlspecialchars($_SESSION['error']); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['error']); ?>
<?php endif; ?>
<?php if (isset($_SESSION['success'])): ?>
<div class="alert alert-success alert-dismissible fade show" role="alert">
<?php echo htmlspecialchars($_SESSION['success']); ?>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<?php unset($_SESSION['success']); ?>
<?php endif; ?>
<section class="container py-5">
<?php if (!empty($alertMessage)): ?>
<div class="alert alert-warning" role="alert">
<?php echo $alertMessage; ?>
<?php echo htmlspecialchars($alertMessage); ?>
</div>
<?php endif; ?>
@ -49,7 +122,7 @@ require_once 'includes/header.php';
<img src="<?php echo htmlspecialchars($userData['img'] ?: 'default_profile.png'); ?>"
alt="avatar"
class="rounded-circle img-fluid" style="width: 150px;">
<!-- Remove username from here -->
<br/>
<button type="button" class="btn btn-sm btn-secondary mb-2"
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#profilePictureModal">
@ -74,8 +147,7 @@ require_once 'includes/header.php';
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#emailModal">
Change
</button>
<?php if (!$userData['emailVerified']): ?>
</button><?php if (!$userData['emailVerified']): ?>
<button type="button" class="btn btn-sm btn-primary"
data-bs-toggle="modal" data-bs-target="#verifyEmailModal">
Verify
@ -88,7 +160,7 @@ require_once 'includes/header.php';
<button type="button" class="btn btn-sm btn-secondary"
<?php echo ($editingDisabled) ? 'disabled' : ''; ?>
data-bs-toggle="modal" data-bs-target="#nameModal">
Change Name
Change
</button>
</div>
<div class="list-group-item d-flex justify-content-between align-items-center">
@ -110,22 +182,36 @@ require_once 'includes/header.php';
<p>Followed DJs and recent ratings will appear here once implemented.</p>
</div>
</div>
<?php
// If the user is an admin, display a new box with session output.
$currentUser = SessionManager::getUser();
if (isset($currentUser['role']) && $currentUser['role'] === 'admin'):
?>
<div class="card mb-4">
<div class="card-body bg-body-secondary">
<h5>Admin Session Output</h5>
<pre id="adminSessionOutput"><?php echo htmlspecialchars(print_r($_SESSION, true)); ?></pre>
</div>
</div>
<?php endif; ?>
</div>
</div>
</section>
<!-- Modals -->
<!-- 1. Profile Picture Modal -->
<div class="modal fade" id="profilePictureModal" tabindex="-1" aria-labelledby="profilePictureModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_profile_picture.php" method="post" enctype="multipart/form-data">
<form action="profile.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="action" value="updateProfilePicture">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="profilePictureModalLabel">Change Profile Picture</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- CSRF token can be added here as hidden input if needed -->
<!-- CSRF token can be added here as needed -->
<div class="mb-3">
<label for="profilePicture" class="form-label">Select new profile picture</label>
<input type="file" class="form-control" id="profilePicture" name="profile_picture" accept="image/*" required>
@ -143,7 +229,8 @@ require_once 'includes/header.php';
<!-- 2. Username Modal -->
<div class="modal fade" id="usernameModal" tabindex="-1" aria-labelledby="usernameModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_username.php" method="post">
<form action="profile.php" method="post">
<input type="hidden" name="action" value="updateUsername">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="usernameModalLabel">Change Username</h5>
@ -167,7 +254,8 @@ require_once 'includes/header.php';
<!-- 3. Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_email.php" method="post">
<form action="profile.php" method="post">
<input type="hidden" name="action" value="updateEmail">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="emailModalLabel">Change Email</h5>
@ -215,10 +303,11 @@ require_once 'includes/header.php';
</div>
</div>
<!-- 5. Name Modal (First & Last Name) -->
<!-- 5. Name Modal -->
<div class="modal fade" id="nameModal" tabindex="-1" aria-labelledby="nameModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_name.php" method="post">
<form action="profile.php" method="post">
<input type="hidden" name="action" value="updateName">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="nameModalLabel">Change Your Name</h5>
@ -246,7 +335,8 @@ require_once 'includes/header.php';
<!-- 6. Password Modal -->
<div class="modal fade" id="passwordModal" tabindex="-1" aria-labelledby="passwordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<form action="update_password.php" method="post">
<form action="profile.php" method="post">
<input type="hidden" name="action" value="updatePassword">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="passwordModalLabel">Change Password</h5>

View file

@ -1,19 +1,20 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use DJMixHosting\User;
use DJMixHosting\SessionManager;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
// If the user is already logged in, redirect them
if(isset($_SESSION['user'])) {
if (SessionManager::getUser()) {
header("Location: profile.php");
exit;
}
if($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// Gather form fields
$username = trim($_POST['username'] ?? '');
$email = trim($_POST['email'] ?? '');
@ -24,68 +25,55 @@ if($_SERVER['REQUEST_METHOD'] == 'POST') {
// Basic validation
$errors = [];
if(empty($username) || empty($email) || empty($password) || empty($confirm_password) || empty($first_name) || empty($last_name)) {
if (empty($username) || empty($email) || empty($password) || empty($confirm_password) || empty($first_name) || empty($last_name)) {
$errors[] = "All fields are required.";
}
if($password !== $confirm_password) {
if ($password !== $confirm_password) {
$errors[] = "Passwords do not match.";
}
if(!filter_var($email, FILTER_VALIDATE_EMAIL)) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$errors[] = "Invalid email format.";
}
if(!preg_match('/^[a-zA-Z0-9_]{3,25}$/', $username)) {
if (!preg_match('/^[a-zA-Z0-9_]{3,25}$/', $username)) {
$errors[] = "Invalid username format.";
}
if(!empty($errors)) {
if (!empty($errors)) {
$_SESSION['error'] = implode(" ", $errors);
header("Location: register.php");
exit;
}
$db = new Database($config);
$user = new User($db);
// Check if username or email already exists
$stmt = $db->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->bind_param("ss", $username, $email);
$stmt->execute();
$result = $stmt->get_result();
if($result->num_rows > 0) {
$_SESSION['error'] = "Username or email already exists.";
try {
// Delegate registration to the User class
$user_id = $user->newUser($username, $password, $email, $first_name, $last_name);
} catch (\Exception $e) {
$_SESSION['error'] = $e->getMessage();
header("Location: register.php");
exit;
}
$stmt->close();
// Insert the new user record. (Assuming columns firstName and lastName exist.)
$hashed_password = password_hash($password, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO users (username, password, email, firstName, lastName, img, emailVerified) VALUES (?, ?, ?, ?, ?, '', 0)");
$stmt->bind_param("sssss", $username, $hashed_password, $email, $first_name, $last_name);
if(!$stmt->execute()){
$_SESSION['error'] = "Registration failed. Please try again.";
header("Location: register.php");
exit;
}
$user_id = $stmt->insert_id;
$stmt->close();
// Log the user in
$_SESSION['user'] = [
// Log the user in using SessionManager
SessionManager::setUser([
'id' => $user_id,
'username' => $username,
'email' => $email
];
]);
// Trigger email verification: generate a 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 record with purpose 'email_verification'
// Insert verification record (with purpose 'email_verification')
$stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at, purpose) VALUES (?, ?, ?, ?, 'email_verification')");
$stmt->bind_param("isss", $user_id, $email, $verification_code, $expires_at);
$stmt->execute();
$stmt->close();
// Send verification email via AWS SES using config settings
// Send verification email via AWS SES
$sesClient = new SesClient([
'version' => 'latest',
'region' => $config['aws']['ses']['region'],
@ -103,10 +91,8 @@ if($_SERVER['REQUEST_METHOD'] == 'POST') {
$body_text .= "{$verification_link}\n\nYour verification code is: {$verification_code}\nThis code will expire in 15 minutes.";
try {
$result = $sesClient->sendEmail([
'Destination' => [
'ToAddresses' => [$recipient_email],
],
$sesClient->sendEmail([
'Destination' => ['ToAddresses' => [$recipient_email]],
'ReplyToAddresses' => [$sender_email],
'Source' => $sender_email,
'Message' => [
@ -139,8 +125,10 @@ require_once 'includes/header.php';
<div class="row justify-content-center">
<div class="col-lg-6">
<?php
if(isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">' . htmlspecialchars($_SESSION['error']) . '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
if (isset($_SESSION['error'])) {
echo '<div class="alert alert-danger alert-dismissible fade show mb-4" role="alert">'
. htmlspecialchars($_SESSION['error'])
. '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>';
unset($_SESSION['error']);
}
?>

View file

@ -1,97 +0,0 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use Aws\Ses\SesClient;
use Aws\Exception\AwsException;
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header("Location: profile.php");
exit;
}
if (!isset($_POST['new_email']) || empty($_POST['new_email'])) {
$_SESSION['error'] = "New email address is required.";
header("Location: profile.php");
exit;
}
$new_email = filter_var($_POST['new_email'], FILTER_VALIDATE_EMAIL);
if (!$new_email) {
$_SESSION['error'] = "Invalid email format.";
header("Location: profile.php");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Update the user's email and mark it as unverified
$stmt = $db->prepare("UPDATE users SET email = ?, emailVerified = 0 WHERE id = ?");
$stmt->bind_param("si", $new_email, $userId);
$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 (using REPLACE to update any existing record for this user and email)
$stmt = $db->prepare("REPLACE INTO email_verifications (user_id, email, verification_code, expires_at) VALUES (?, ?, ?, ?)");
$stmt->bind_param("isss", $userId, $new_email, $verification_code, $expires_at);
$stmt->execute();
$stmt->close();
// Send email using AWS SES with config settings
$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 = $new_email;
$subject = "Verify Your Email Address";
// Construct a verification link. Users can click this link to auto-submit the code.
$verification_link = $config['app']['url'] . "/verify_email.php?code={$verification_code}";
$body_text = "Please verify your email address by clicking the link below or by entering the code in your profile:\n\n";
$body_text .= "{$verification_link}\n\nYour verification code is: {$verification_code}\nThis code will expire in 15 minutes.";
try {
$result = $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' => $subject,
],
],
]);
$_SESSION['success'] = "Email updated. A verification email has been sent to your new address.";
} catch (AwsException $e) {
$_SESSION['error'] = "Failed to send verification email: " . $e->getAwsErrorMessage();
}
header("Location: profile.php");
exit;

View file

@ -1,43 +0,0 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
// Ensure the user is logged in
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
// Check that both first and last names were provided
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_POST['first_name']) || empty($_POST['last_name'])) {
$_SESSION['error'] = "Both first name and last name are required.";
header("Location: profile.php");
exit;
}
$firstName = trim($_POST['first_name']);
$lastName = trim($_POST['last_name']);
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Update the user's first and last name in the database
$stmt = $db->prepare("UPDATE users SET firstName = ?, lastName = ? WHERE id = ?");
$stmt->bind_param("ssi", $firstName, $lastName, $userId);
if (!$stmt->execute()) {
$_SESSION['error'] = "Failed to update name. Please try again.";
header("Location: profile.php");
exit;
}
$stmt->close();
// Optionally update session data if you store these fields there
$_SESSION['user']['firstName'] = $firstName;
$_SESSION['user']['lastName'] = $lastName;
$_SESSION['success'] = "Name updated successfully.";
header("Location: profile.php");
exit;

View file

@ -1,84 +0,0 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
// Ensure the user is authenticated.
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
// Check for required POST fields.
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || empty($_POST['current_password']) || empty($_POST['new_password']) || empty($_POST['confirm_password'])) {
$_SESSION['error'] = "All password fields are required.";
header("Location: profile.php");
exit;
}
$current_password = $_POST['current_password'];
$new_password = $_POST['new_password'];
$confirm_password = $_POST['confirm_password'];
// Validate that the new password meets the requirements:
// - 8 to 32 characters
// - at least one uppercase letter
// - at least one lowercase letter
// - at least one number
// - at least one symbol
$pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,32}$/';
if (!preg_match($pattern, $new_password)) {
$_SESSION['error'] = "New password must be 8-32 characters and include at least one uppercase letter, one lowercase letter, one number, and one symbol.";
header("Location: profile.php");
exit;
}
// Verify that new password and confirmation match.
if ($new_password !== $confirm_password) {
$_SESSION['error'] = "New password and confirmation do not match.";
header("Location: profile.php");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Retrieve the current password hash from the database.
$stmt = $db->prepare("SELECT password FROM users WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$result = $stmt->get_result();
$userData = $result->fetch_assoc();
$stmt->close();
if (!$userData) {
$_SESSION['error'] = "User not found.";
header("Location: profile.php");
exit;
}
// Verify that the current password is correct.
if (!password_verify($current_password, $userData['password'])) {
$_SESSION['error'] = "Current password is incorrect.";
header("Location: profile.php");
exit;
}
// Hash the new password.
$hashed_new_password = password_hash($new_password, PASSWORD_DEFAULT);
// Update the user's password in the database.
$stmt = $db->prepare("UPDATE users SET password = ? WHERE id = ?");
$stmt->bind_param("si", $hashed_new_password, $userId);
if (!$stmt->execute()) {
$_SESSION['error'] = "Failed to update password. Please try again.";
header("Location: profile.php");
exit;
}
$stmt->close();
$_SESSION['success'] = "Password updated successfully.";
header("Location: profile.php");
exit;

View file

@ -1,53 +0,0 @@
<?php
session_start();
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
if (!isset($_SESSION['user'])) {
header("Location: login.php");
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST' || !isset($_POST['new_username']) || empty($_POST['new_username'])) {
$_SESSION['error'] = "New username is required.";
header("Location: profile.php");
exit;
}
$new_username = trim($_POST['new_username']);
// Validate username (for example, only alphanumeric and underscores, 3-25 characters)
if (!preg_match('/^[a-zA-Z0-9_]{3,25}$/', $new_username)) {
$_SESSION['error'] = "Invalid username format.";
header("Location: profile.php");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Check if the new username already exists (excluding the current user)
$stmt = $db->prepare("SELECT id FROM users WHERE username = ? AND id != ?");
$stmt->bind_param("si", $new_username, $userId);
$stmt->execute();
$result = $stmt->get_result();
if ($result->num_rows > 0) {
$_SESSION['error'] = "Username already taken.";
header("Location: profile.php");
exit;
}
$stmt->close();
// Update the username in the database
$stmt = $db->prepare("UPDATE users SET username = ? WHERE id = ?");
$stmt->bind_param("si", $new_username, $userId);
$stmt->execute();
$stmt->close();
// Update session data
$_SESSION['user']['username'] = $new_username;
$_SESSION['success'] = "Username updated successfully.";
header("Location: profile.php");
exit;

View file

@ -1,149 +1,305 @@
<?php
// upload.php - Step 1: File upload and immediate processing
// upload.php - One Page Upload and Details Submission
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Upload;
use DJMixHosting\CDN;
use DJMixHosting\Database;
use DJMixHosting\Genres;
use DJMixHosting\DJs;
// Ensure user is authenticated
if (!isset($_SESSION['user'])) {
header("Location: login.php");
$_SESSION['error'] = $locale['loginToUploadMix'];
header("Location: /login");
exit;
}
// Process the form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['mix_file'])) {
// Get upload config from $config
$tmpPath = rtrim($config['uploads']['tmp_path'], '/') . '/';
$maxFileSize = $config['uploads']['max_file_size'];
$allowedMimeTypes = $config['uploads']['allowed_mime_type'];
$allowedFileTypes = $config['uploads']['allowed_file_types'];
// If the form was submitted, check which step we're processing via an "action" field.
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Step 1: Handle file upload
if (isset($_POST['action']) && $_POST['action'] === 'upload_file') {
if (isset($_FILES['mix_file'])) {
$upload = new Upload($_FILES['mix_file'], $config);
$file = $_FILES['mix_file'];
// Basic file validations
if ($file['error'] !== UPLOAD_ERR_OK) {
$_SESSION['error'] = "File upload error.";
header("Location: upload.php");
// Validate the file
if (!$upload->validate()) {
$_SESSION['error'] = implode(", ", $upload->getErrors());
header("Location: /upload");
exit;
}
if ($file['size'] > $maxFileSize) {
$_SESSION['error'] = "File is too large.";
header("Location: upload.php");
// Move the file to the temporary directory
if (!$upload->moveFile()) {
$_SESSION['error'] = implode(", ", $upload->getErrors());
header("Location: /upload");
exit;
}
// Get file extension and mime type
$ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!in_array($ext, $allowedFileTypes)) {
$_SESSION['error'] = "File type not allowed.";
header("Location: upload.php");
exit;
}
if (!in_array($file['type'], $allowedMimeTypes)) {
$_SESSION['error'] = "MIME type not allowed.";
header("Location: upload.php");
exit;
}
// Create a unique temporary filename
$uniqueName = uniqid() . "." . $ext;
$destination = $tmpPath . $uniqueName;
if (!move_uploaded_file($file['tmp_name'], $destination)) {
$_SESSION['error'] = "Failed to save uploaded file.";
header("Location: upload.php");
exit;
}
// Process the file: if mp3, extract metadata; if zip, extract each mp3's metadata.
$uploadTask = [];
$uploadTask['original_name'] = $file['name'];
$uploadTask['local_path'] = $destination;
$uploadTask['size'] = $file['size'];
$uploadTask['ext'] = $ext;
if ($ext === "mp3") {
// Process MP3 file using shell_exec calls (ensure you sanitize arguments)
$escapedFile = escapeshellarg($destination);
$artist = trim(shell_exec("eyed3 --query='%a%' $escapedFile"));
$title = trim(shell_exec("eyed3 --query='%t%' $escapedFile"));
$duration = trim(shell_exec("mp3info -p \"%S\" $escapedFile"));
// You can extract additional info as needed
$uploadTask['file_type'] = 'mp3';
$uploadTask['metadata'] = [
'artist' => $artist,
'title' => $title,
'duration' => $duration, // in seconds
];
$uploadTask['mediaplayer'] = 1;
} elseif ($ext === "zip") {
// Process ZIP file using ZipArchive
$zip = new ZipArchive;
if ($zip->open($destination) === true) {
$totalDuration = 0;
$tracklist = [];
for ($i = 0; $i < $zip->numFiles; $i++) {
$entryName = $zip->getNameIndex($i);
$entryExt = strtolower(pathinfo($entryName, PATHINFO_EXTENSION));
if ($entryExt === "mp3") {
// Extract the MP3 temporarily to process metadata
$tempDir = $tmpPath . 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,
];
// Mark ZIPs as download only (no mediaplayer)
$uploadTask['mediaplayer'] = 0;
} else {
$_SESSION['error'] = "Failed to open ZIP file.";
header("Location: upload.php");
exit;
}
} else {
$_SESSION['error'] = "Unsupported file type.";
header("Location: upload.php");
exit;
}
// Save the upload task details in session so step 2 can use them.
// Process the file (extract metadata for MP3/ZIP)
$uploadTask = $upload->process();
$_SESSION['upload_task'] = $uploadTask;
header("Location: upload_details.php");
// Reload the page so the details form can be shown
header("Location: /upload");
exit;
}
}
}
// Step 2: Handle mix details submission
elseif (isset($_POST['action']) && $_POST['action'] === 'submit_details') {
if (!isset($_SESSION['upload_task'])) {
$_SESSION['error'] = $locale['noUploadedFileFound'];
header("Location: /upload");
exit;
}
require_once 'includes/header.php';
$uploadTask = $_SESSION['upload_task'];
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$recorded = trim($_POST['recorded'] ?? '');
$selectedGenres = $_POST['genres'] ?? []; // array of genre IDs
$dj1 = $_POST['dj1'] ?? 0;
$dj2 = $_POST['dj2'] ?? 0;
$dj3 = $_POST['dj3'] ?? 0;
// Basic validation: title is required
if (empty($title)) {
$_SESSION['error'] = $locale['mixTitleRequired'];
header("Location: /upload");
exit;
}
// Simple slugify function
function slugify($text) {
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
$text = preg_replace('~[^-\w]+~', '', $text);
$text = trim($text, '-');
$text = preg_replace('~-+~', '-', $text);
$text = strtolower($text);
return empty($text) ? 'n-a' : $text;
}
$slug = slugify($title);
// Upload the file to the CDN
$cdn = new CDN($config);
$remotePath = "temp/mixes/" . uniqid() . "_" . basename($uploadTask['local_path']);
$mimeType = ($uploadTask['ext'] === 'mp3') ? 'audio/mpeg' : 'application/zip';
try {
$cdn->uploadFile($uploadTask['local_path'], $remotePath, $mimeType, 'private');
} catch (Exception $e) {
$_SESSION['error'] = $locale['errorUploadCDN'] . $e->getMessage();
header("Location: /upload");
exit;
}
// Insert the mix record into the database
$db = new Database($config);
$stmt = $db->prepare("INSERT INTO mix (title, slug, description, cover, url, seconds, mediaplayer, dj1, dj2, dj3, pending, recorded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)");
if ($uploadTask['file_type'] === 'mp3') {
$seconds = (int)$uploadTask['metadata']['duration'];
$mediaplayer = 0;
} elseif ($uploadTask['file_type'] === 'zip') {
$seconds = (int)$uploadTask['metadata']['total_duration'];
$mediaplayer = 1;
} else {
$seconds = 0;
$mediaplayer = 1;
}
$url = $remotePath;
$dj1 = (int)$dj1;
$dj2 = (int)$dj2;
$dj3 = (int)$dj3;
$cover = "";
$stmt->bind_param("sssssiiiiss", $title, $slug, $description, $cover, $url, $seconds, $mediaplayer, $dj1, $dj2, $dj3, $recorded);
if (!$stmt->execute()) {
$_SESSION['error'] = $locale['errorSavingMixDB'];
header("Location: /upload");
exit;
}
$mixId = $stmt->insert_id;
$stmt->close();
// Insert mix_meta entries for genres and tracklist
foreach ($selectedGenres as $genreId) {
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'genre', ?)");
$stmt->bind_param("is", $mixId, $genreId);
$stmt->execute();
$stmt->close();
}
if ($uploadTask['file_type'] === 'mp3' && !empty($uploadTask['metadata']['title'])) {
$tracklist = $uploadTask['metadata']['title'];
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'tracklist', ?)");
$stmt->bind_param("is", $mixId, $tracklist);
$stmt->execute();
$stmt->close();
} elseif ($uploadTask['file_type'] === 'zip' && !empty($uploadTask['metadata']['tracklist'])) {
$tracklist = implode("\n", $uploadTask['metadata']['tracklist']);
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'tracklist', ?)");
$stmt->bind_param("is", $mixId, $tracklist);
$stmt->execute();
$stmt->close();
}
// Cleanup: delete the local temporary file and clear session upload task
unlink($uploadTask['local_path']);
unset($_SESSION['upload_task']);
$_SESSION['success'] = $locale['uploadedPendingApproval'];
header("Location: profile.php");
exit;
}
}
?>
<?php require_once 'includes/header.php'; ?>
<section class="upload-section py-5">
<div class="container">
<h2 class="mb-4">Upload a New Mix</h2>
<?php
// Display any error messages
if (isset($_SESSION['error'])) {
echo '<div class="alert alert-danger">' . htmlspecialchars($_SESSION['error']) . '</div>';
unset($_SESSION['error']);
}
// If no file has been uploaded, show the file upload form.
if (!isset($_SESSION['upload_task'])):
?>
<form action="upload.php" method="post" enctype="multipart/form-data">
<div class="card">
<div class="card-body">
<h3 class="card-title mb-4"><?php echo $locale['uploadHeader1'];?></h3>
<!-- Info Alert -->
<div class="alert alert-info">
<h4 class="alert-heading">Important Upload Information</h4>
<p>Utah's DJs is primarily an archival project dedicated to preserving the history and culture of EDM DJs in Utah. Your uploads contribute to this historical record.</p>
</div>
<!-- Requirements -->
<h4 class="mb-3">Before You Upload</h4>
<ul class="list-group list-group-flush mb-4">
<li class="list-group-item">
<i class="fas fa-check-circle text-success me-2"></i>
Verify that all DJs involved are listed in our database. If a DJ is not listed, they must be added and approved before uploading.
</li>
<li class="list-group-item">
<i class="fas fa-check-circle text-success me-2"></i>
Check that appropriate genres are available for your mix. New genres require approval before they can be used.
</li>
<li class="list-group-item">
<i class="fas fa-info-circle text-primary me-2"></i>
You can submit new DJ or genre requests through your profile settings.
</li>
</ul>
<!-- Process Steps -->
<h4 class="mb-3">Upload Process</h4>
<ol class="list-group list-group-numbered mb-4">
<li class="list-group-item">Upload your mix file (MP3 or ZIP format)</li>
<li class="list-group-item">Enter mix details, including title, description, and recording date</li>
<li class="list-group-item">Select relevant genres and DJs</li>
<li class="list-group-item">Submit for review</li>
</ol>
<!-- Upload Form -->
<div class="card mt-4">
<div class="card-body">
<h4 class="card-title mb-4">Upload Your Mix</h4>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="hidden" name="action" value="upload_file">
<div class="mb-3">
<label for="mix_file" class="form-label">Select Mix File (MP3 or ZIP)</label>
<input type="file" class="form-control" id="mix_file" name="mix_file" accept=".mp3,.zip" required>
<div class="form-text">Maximum file size: 500MB</div>
</div>
<!-- Optionally, add album art upload here later -->
<button type="submit" class="btn btn-primary">Upload File</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-upload me-2"></i>Upload File
</button>
</form>
</div>
</div>
</div>
</div>
<?php
// If an upload task exists, show the mix details form.
else:
$uploadTask = $_SESSION['upload_task'];
// Load available genres and DJs for the form
$db = new Database($config);
$genresObj = new Genres($db);
$allGenres = $genresObj->get_all_genres();
$djsObj = new DJs($db);
$allDJs = $djsObj->get_all_djs();
?>
<div class="card">
<div class="card-body">
<h3 class="card-title mb-4">Enter Mix Details</h3>
<div class="card mb-4">
<div class="card-body">
<h5>File Summary</h5>
<p><strong>Original Name:</strong> <?php echo htmlspecialchars($uploadTask['original_name']); ?></p>
<p><strong>File Type:</strong> <?php echo htmlspecialchars(strtoupper($uploadTask['file_type'])); ?></p>
<p><strong>Size:</strong> <?php echo round($uploadTask['size'] / 1024, 2); ?> KB</p>
<?php if ($uploadTask['file_type'] === 'mp3'): ?>
<p><strong>Duration:</strong> <?php echo htmlspecialchars($uploadTask['metadata']['duration']); ?> seconds</p>
<?php elseif ($uploadTask['file_type'] === 'zip'): ?>
<p><strong>Total Duration:</strong> <?php echo htmlspecialchars($uploadTask['metadata']['total_duration']); ?> seconds</p>
<?php endif; ?>
</div>
</div>
<form action="/upload" method="post" class="needs-validation" novalidate>
<input type="hidden" name="action" value="submit_details">
<div class="mb-3">
<label for="title" class="form-label">Mix Title</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Mix Description</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="recorded" class="form-label">Recorded Date</label>
<input type="date" class="form-control" id="recorded" name="recorded" required>
</div>
<div class="mb-3">
<label for="genres" class="form-label">Select Genres (type to search)</label>
<select class="form-select" id="genres" name="genres[]" multiple required>
<?php foreach ($allGenres as $genre): ?>
<option value="<?php echo htmlspecialchars($genre['id']); ?>"><?php echo htmlspecialchars($genre['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Select DJs (Maximum 3)</label>
<div class="row">
<div class="col">
<select class="form-select" name="dj1" required>
<option value="">Select DJ 1</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col">
<select class="form-select" name="dj2">
<option value="">Select DJ 2 (Optional)</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col">
<select class="form-select" name="dj3">
<option value="">Select DJ 3 (Optional)</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Submit Mix</button>
</form>
</div>
</div>
<?php endif; ?>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>

View file

@ -1,245 +0,0 @@
<?php
// upload_details.php - Step 2: Enter mix details and finalize upload
require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use DJMixHosting\Genres;
use DJMixHosting\DJs;
use Aws\Credentials\Credentials;
// Ensure user is authenticated and an upload task exists
if (!isset($_SESSION['user']) || !isset($_SESSION['upload_task'])) {
header("Location: upload.php");
exit;
}
$uploadTask = $_SESSION['upload_task'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Get form fields
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$recorded = trim($_POST['recorded'] ?? '');
$selectedGenres = $_POST['genres'] ?? []; // an array of genre IDs or names
$dj1 = $_POST['dj1'] ?? 0;
$dj2 = $_POST['dj2'] ?? 0;
$dj3 = $_POST['dj3'] ?? 0;
// Basic validation
if (empty($title)) {
$_SESSION['error'] = "Mix title is required.";
header("Location: upload_details.php");
exit;
}
// Generate a slug from the title
function slugify($text) {
// Replace non-letter or digits by -
$text = preg_replace('~[^\pL\d]+~u', '-', $text);
// Transliterate
$text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
// Remove unwanted characters
$text = preg_replace('~[^-\w]+~', '', $text);
// Trim
$text = trim($text, '-');
// Remove duplicate -
$text = preg_replace('~-+~', '-', $text);
// Lowercase
$text = strtolower($text);
return empty($text) ? 'n-a' : $text;
}
$slug = slugify($title);
$credentials = new Credentials(
$config['aws']['s3']['access_key'],
$config['aws']['s3']['secret_key']
);
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['aws']['s3']['region'],
'endpoint' => $config['aws']['s3']['host'],
'credentials' => $credentials,
'use_path_style_endpoint' => true,
'profile' => null, // disable reading ~/.aws/config
'use_aws_shared_config_files' => false,
]);
// Determine remote file path for now, store in a temporary folder on Spaces
$remotePath = "temp/mixes/" . uniqid() . "_" . basename($uploadTask['local_path']);
// Determine MIME type from file extension
$mimeType = ($uploadTask['ext'] === 'mp3') ? 'audio/mpeg' : 'application/zip';
try {
$s3Client->putObject([
'Bucket' => $config['aws']['s3']['bucket'],
'Key' => $remotePath,
'SourceFile' => $uploadTask['local_path'],
'ACL' => 'private', // file remains hidden until approved
'ContentType' => $mimeType,
]);
} catch (Exception $e) {
$_SESSION['error'] = "Error uploading file to CDN: " . $e->getMessage();
header("Location: upload_details.php");
exit;
}
// Now insert a new mix record into the database with pending status.
$db = new Database($config);
$stmt = $db->prepare("INSERT INTO mix (title, slug, description, cover, url, seconds, mediaplayer, dj1, dj2, dj3, pending, recorded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)");
// Determine the duration and mediaplayer flag from the upload task:
// For an MP3 file, set mediaplayer to 0; for a ZIP file, set it to 1.
$seconds = 0;
if ($uploadTask['file_type'] === 'mp3') {
$seconds = (int)$uploadTask['metadata']['duration'];
$mediaplayer = 0; // MP3 gets a player
} elseif ($uploadTask['file_type'] === 'zip') {
$seconds = (int)$uploadTask['metadata']['total_duration'];
$mediaplayer = 1; // ZIPs are download only, so flag them as 1
}
// The URL can be constructed as per your CDN structure; for now, we store the remote path.
$url = $remotePath;
// For DJs, cast to integer (0 if none selected)
$dj1 = (int)$dj1;
$dj2 = (int)$dj2;
$dj3 = (int)$dj3;
$cover = "";
$stmt->bind_param("sssssiiiiss", $title, $slug, $description, $cover, $url, $seconds, $mediaplayer, $dj1, $dj2, $dj3, $recorded);
if (!$stmt->execute()) {
$_SESSION['error'] = "Error saving mix to database.";
header("Location: upload_details.php");
exit;
}
$mixId = $stmt->insert_id;
$stmt->close();
// Insert mix_meta entries for genres and tracklist
// For genres, assume $selectedGenres is an array of genre IDs (or names, if new genres need to be added)
foreach ($selectedGenres as $genreId) {
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'genre', ?)");
$stmt->bind_param("is", $mixId, $genreId);
$stmt->execute();
$stmt->close();
}
// If the file was mp3 and metadata includes a track title or tracklist, insert that as well:
if ($uploadTask['file_type'] === 'mp3' && !empty($uploadTask['metadata']['title'])) {
$tracklist = $uploadTask['metadata']['title'];
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'tracklist', ?)");
$stmt->bind_param("is", $mixId, $tracklist);
$stmt->execute();
$stmt->close();
} elseif ($uploadTask['file_type'] === 'zip' && !empty($uploadTask['metadata']['tracklist'])) {
// If multiple tracks, you might store them as a newline-separated string
$tracklist = implode("\n", $uploadTask['metadata']['tracklist']);
$stmt = $db->prepare("INSERT INTO mix_meta (mix_id, attribute, value) VALUES (?, 'tracklist', ?)");
$stmt->bind_param("is", $mixId, $tracklist);
$stmt->execute();
$stmt->close();
}
// Cleanup: delete the local temporary file and clear session task
unlink($uploadTask['local_path']);
unset($_SESSION['upload_task']);
$_SESSION['success'] = "Mix uploaded successfully and is pending approval.";
header("Location: profile.php");
exit;
}
require_once 'includes/header.php';
// Load available genres and DJs for the form
$db = new Database($config);
$genresObj = new DJMixHosting\Genres($db);
$allGenres = $genresObj->get_all_genres();
$djsObj = new DJMixHosting\DJs($db);
$allDJs = $djsObj->get_all_djs();
?>
<section class="upload-details-section py-5">
<div class="container">
<h2 class="mb-4">Enter Mix Details</h2>
<?php
if(isset($_SESSION['error'])) {
echo '<div class="alert alert-danger">' . htmlspecialchars($_SESSION['error']) . '</div>';
unset($_SESSION['error']);
}
?>
<div class="card mb-4">
<div class="card-body">
<h5>File Summary</h5>
<p><strong>Original Name:</strong> <?php echo htmlspecialchars($uploadTask['original_name']); ?></p>
<p><strong>File Type:</strong> <?php echo htmlspecialchars(strtoupper($uploadTask['file_type'])); ?></p>
<p><strong>Size:</strong> <?php echo round($uploadTask['size'] / 1024, 2); ?> KB</p>
<?php if ($uploadTask['file_type'] === 'mp3'): ?>
<p><strong>Duration:</strong> <?php echo htmlspecialchars($uploadTask['metadata']['duration']); ?> seconds</p>
<?php elseif ($uploadTask['file_type'] === 'zip'): ?>
<p><strong>Total Duration:</strong> <?php echo htmlspecialchars($uploadTask['metadata']['total_duration']); ?> seconds</p>
<?php endif; ?>
</div>
</div>
<form action="upload_details.php" method="post" class="needs-validation" novalidate>
<div class="mb-3">
<label for="title" class="form-label">Mix Title</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Mix Description</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="recorded" class="form-label">Recorded Date</label>
<input type="date" class="form-control" id="recorded" name="recorded" required>
</div>
<div class="mb-3">
<label for="genres" class="form-label">Select Genres (type to search)</label>
<select class="form-select" id="genres" name="genres[]" multiple required>
<?php foreach ($allGenres as $genre): ?>
<option value="<?php echo htmlspecialchars($genre['id']); ?>"><?php echo htmlspecialchars($genre['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="mb-3">
<label class="form-label">Select DJs (Maximum 3)</label>
<div class="row">
<div class="col">
<select class="form-select" name="dj1" required>
<option value="">Select DJ 1</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col">
<select class="form-select" name="dj2">
<option value="">Select DJ 2 (Optional)</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col">
<select class="form-select" name="dj3">
<option value="">Select DJ 3 (Optional)</option>
<?php foreach ($allDJs as $dj): ?>
<option value="<?php echo htmlspecialchars($dj['id']); ?>"><?php echo htmlspecialchars($dj['name']); ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Submit Mix</button>
</form>
</div>
</section>
<?php require_once 'includes/footer.php'; ?>

View file

@ -4,15 +4,20 @@ require_once 'includes/globals.php';
require_once 'vendor/autoload.php';
use DJMixHosting\Database;
use DJMixHosting\User;
if (!isset($_SESSION['user'])) {
header("Location: login.php");
$_SESSION['error'] = $locale['loginToVerifyEmail'];
header("Location: /login");
exit;
}
$db = new Database($config);
$userId = $_SESSION['user']['id'];
// Create a User instance
$user = new User($db, $userId);
// Retrieve the verification code from GET or POST
$verification_code = "";
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['verification_code'])) {
@ -20,46 +25,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['verification_code']))
} elseif (isset($_GET['code'])) {
$verification_code = trim($_GET['code']);
} else {
$_SESSION['error'] = "Verification code is required.";
header("Location: profile.php");
$_SESSION['error'] = $locale['verificationCodeRequired'];
header("Location: /profile");
exit;
}
// Look up the email verification record for this user and code
$stmt = $db->prepare("SELECT * FROM email_verifications WHERE user_id = ? AND verification_code = ?");
$stmt->bind_param("is", $userId, $verification_code);
$stmt->execute();
$result = $stmt->get_result();
$record = $result->fetch_assoc();
$stmt->close();
if (!$record) {
$_SESSION['error'] = "Invalid verification code.";
header("Location: profile.php");
exit;
// Attempt to verify the email
try {
$message = $user->verifyEmail($verification_code);
$_SESSION['success'] = $message;
} catch (\Exception $e) {
$_SESSION['error'] = $e->getMessage();
}
// Check if the verification code has expired
$current_time = new DateTime();
$expires_at = new DateTime($record['expires_at']);
if ($current_time > $expires_at) {
$_SESSION['error'] = "Verification code has expired. Please request a new one.";
header("Location: profile.php");
exit;
}
// Verification successful: update the user's record
$stmt = $db->prepare("UPDATE users SET emailVerified = 1 WHERE id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$stmt->close();
// Remove the verification record for cleanup
$stmt = $db->prepare("DELETE FROM email_verifications WHERE user_id = ?");
$stmt->bind_param("i", $userId);
$stmt->execute();
$stmt->close();
$_SESSION['success'] = "Email verified successfully.";
header("Location: profile.php");
header("Location: /profile");
exit;