Compare commits

...

2 Commits

3 changed files with 178 additions and 130 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@ jilo.db
jilo-web.db
packaging/deb-package/
packaging/rpm-package/
/public_html/uploads/avatars/

View File

@ -473,6 +473,20 @@ class User {
$newFileName = md5(time() . $fileName) . '.' . $fileExtension;
$dest_path = $avatars_path . $newFileName;
// ensure avatars directory exists
if (!is_dir($avatars_path)) {
if (!mkdir($avatars_path, 0755, true)) {
$_SESSION['error'] .= 'Unable to create avatars directory. ';
return false;
}
}
// check if directory is writable
if (!is_writable($avatars_path)) {
$_SESSION['error'] .= 'Avatars directory is not writable. ';
return false;
}
// move the file to avatars folder
if (move_uploaded_file($fileTmpPath, $dest_path)) {
try {
@ -486,24 +500,50 @@ class User {
':user_id' => $userId
]);
// all went OK
$_SESSION['notice'] .= 'Avatar updated successfully. ';
$_SESSION['notice'] = 'Avatar updated successfully. ';
return true;
} catch (Exception $e) {
$_SESSION['error'] .= 'Database error updating avatar. ';
return $e->getMessage();
}
} else {
$_SESSION['error'] .= 'Error moving the uploaded file. ';
$_SESSION['error'] = 'Error moving the uploaded file. Please check directory permissions. ';
}
} else {
$_SESSION['error'] .= 'Invalid avatar file type. ';
$_SESSION['error'] = 'Invalid avatar file type. Only JPG, PNG, and JPEG are allowed. ';
}
} else {
$_SESSION['error'] .= 'Error uploading the avatar file. ';
// Handle different upload errors
switch ($avatar_file['error']) {
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$_SESSION['error'] = 'Avatar file is too large. Maximum size is 500KB. ';
break;
case UPLOAD_ERR_PARTIAL:
$_SESSION['error'] = 'Avatar file was only partially uploaded. ';
break;
case UPLOAD_ERR_NO_FILE:
$_SESSION['error'] = 'No avatar file was uploaded. ';
break;
case UPLOAD_ERR_NO_TMP_DIR:
$_SESSION['error'] = 'Missing temporary folder for file upload. ';
break;
case UPLOAD_ERR_CANT_WRITE:
$_SESSION['error'] = 'Failed to write avatar file to disk. ';
break;
case UPLOAD_ERR_EXTENSION:
$_SESSION['error'] = 'File upload stopped by extension. ';
break;
default:
$_SESSION['error'] = 'Unknown upload error occurred. ';
break;
}
}
} catch (Exception $e) {
$_SESSION['error'] = 'An error occurred while processing the avatar: ' . $e->getMessage();
return $e->getMessage();
}
return false;
}
/**

View File

@ -1,15 +1,12 @@
<!-- user profile -->
<div class="tm-profile-card mx-auto">
<div class="tm-profile-header">
<div>
<p class="tm-profile-eyebrow">Account</p>
<h2 class="tm-profile-title">Profile of <?= htmlspecialchars($userDetails[0]['username']) ?></h2>
<p class="tm-profile-subtitle">Update your personal details, avatar, and access rights in one streamlined view.</p>
<!-- user profile -->
<div class="action-card">
<div class="action-card-header">
<p class="action-eyebrow">Account</p>
<h2 class="action-title">Profile of <?= htmlspecialchars($userDetails[0]['username']) ?></h2>
<p class="action-subtitle">Update your personal details, avatar, and access rights in one streamlined view.</p>
</div>
</div>
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data" class="tm-profile-form" novalidate>
<div class="action-card-body">
<form method="POST" action="<?= htmlspecialchars($app_root) ?>?page=profile" enctype="multipart/form-data" class="action-form" novalidate>
<?php include CSRF_TOKEN_INCLUDE; ?>
<div class="row g-4 align-items-start">
<div class="col-lg-4">
@ -39,20 +36,25 @@
<h3 class="tm-profile-section-title">Personal info</h3>
<div class="row g-3">
<div class="col-md-6">
<label for="name" class="form-label">Full name</label>
<input class="form-control" type="text" name="name" id="name" value="<?= htmlspecialchars($userDetails[0]['name'] ?? '') ?>" autofocus />
<div class="action-form-group">
<label for="name" class="action-form-label">Full name</label>
<input class="form-control action-form-control" type="text" name="name" id="name" value="<?= htmlspecialchars($userDetails[0]['name'] ?? '') ?>" autofocus />
</div>
</div>
<div class="col-md-6">
<label for="email" class="form-label">Email address</label>
<input class="form-control" type="text" name="email" id="email" value="<?= htmlspecialchars($userDetails[0]['email'] ?? '') ?>" />
<div class="action-form-group">
<label for="email" class="action-form-label">Email address</label>
<input class="form-control action-form-control" type="text" name="email" id="email" value="<?= htmlspecialchars($userDetails[0]['email'] ?? '') ?>" />
</div>
</div>
</div>
</div>
<div class="tm-profile-section">
<h3 class="tm-profile-section-title">Timezone</h3>
<label for="timezone" class="form-label">Preferred timezone</label>
<select class="form-control" name="timezone" id="timezone">
<div class="action-form-group">
<label for="timezone" class="action-form-label">Preferred timezone</label>
<select class="form-control action-form-control" name="timezone" id="timezone">
<?php foreach ($allTimezones as $timezone) { ?>
<option value="<?= htmlspecialchars($timezone) ?>" <?= $timezone === $userTimezone ? 'selected' : '' ?>>
<?= htmlspecialchars($timezone) ?>&nbsp;&nbsp;(<?= htmlspecialchars(getUTCOffset($timezone)) ?>)
@ -60,10 +62,13 @@
<?php } ?>
</select>
</div>
</div>
<div class="tm-profile-section">
<h3 class="tm-profile-section-title">Bio</h3>
<textarea class="form-control" name="bio" rows="6" placeholder="Share something about yourself, your role, or preferences."><?= htmlspecialchars($userDetails[0]['bio'] ?? '') ?></textarea>
<div class="action-form-group">
<textarea class="form-control action-form-control" name="bio" rows="6" placeholder="Share something about yourself, your role, or preferences."><?= htmlspecialchars($userDetails[0]['bio'] ?? '') ?></textarea>
</div>
</div>
<div class="tm-profile-section">
@ -86,9 +91,9 @@
</div>
</div>
<div class="tm-profile-actions">
<a href="<?= htmlspecialchars($app_root) ?>?page=profile" class="btn btn-light tm-contact-back">Cancel</a>
<button type="submit" class="btn btn-primary tm-contact-submit">Save changes</button>
<div class="action-actions">
<a href="<?= htmlspecialchars($app_root) ?>?page=profile" class="btn btn-light">Cancel</a>
<button type="submit" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
@ -116,22 +121,23 @@
</div>
</div>
</div>
</div>
<!-- /user profile -->
</div>
<!-- /user profile -->
<script>
// Preview the uploaded avatar
document.getElementById('avatar-upload').addEventListener('change', function(event) {
document.addEventListener('DOMContentLoaded', function() {
// Preview the uploaded avatar
document.getElementById('avatar-upload').addEventListener('change', function(event) {
const reader = new FileReader();
reader.onload = function() {
document.querySelector('.avatar-img').src = reader.result;
};
reader.readAsDataURL(event.target.files[0]);
});
});
// Avatar file size and type control
document.getElementById('avatar-upload').addEventListener('change', function() {
// Avatar file size and type control
document.getElementById('avatar-upload').addEventListener('change', function() {
const maxFileSize = 500 * 1024; // 500 KB in bytes
const currentAvatar = '<?= htmlspecialchars($app_root) . htmlspecialchars($avatar) ?>'; // current avatar
const file = this.files[0];
@ -144,12 +150,13 @@ document.getElementById('avatar-upload').addEventListener('change', function() {
document.querySelector('.avatar-img').src = currentAvatar;
}
}
});
});
// Submitting the avatar deletion confirmation modal form
document.getElementById('confirm-delete').addEventListener('click', function(event) {
// Submitting the avatar deletion confirmation modal form
document.getElementById('confirm-delete').addEventListener('click', function(event) {
event.preventDefault(); // Prevent the outer form from submitting
document.getElementById('remove-avatar-form').submit();
});
});
// Function to detect user's timezone and select it in the dropdown