Release V1.7

This commit is contained in:
Flatlogic Bot 2026-03-15 00:44:20 +00:00
parent 9c3e061f45
commit d77f94966d
3 changed files with 121 additions and 161 deletions

View File

@ -5,69 +5,9 @@ require_once 'includes/permissions.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? '';
$user_id = $_SESSION['user_id'];
if ($action === 'update_tags') {
$thread_id = $_POST['thread_id'] ?? 0;
$tag_ids = $_POST['tag_ids'] ?? [];
if (is_string($tag_ids)) {
$tag_ids = array_filter(explode(',', $tag_ids));
}
if (!$thread_id) {
echo json_encode(['success' => false, 'error' => 'Missing thread_id']);
exit;
}
// Verify permission
$stmt = db()->prepare("SELECT t.*, c.server_id FROM forum_threads t JOIN channels c ON t.channel_id = c.id WHERE t.id = ?");
$stmt->execute([$thread_id]);
$thread = $stmt->fetch();
if (!$thread) {
echo json_encode(['success' => false, 'error' => 'Thread not found']);
exit;
}
$stmtServer = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmtServer->execute([$thread['server_id']]);
$server = $stmtServer->fetch();
$is_admin = Permissions::hasPermission($user_id, $thread['server_id'], Permissions::ADMINISTRATOR) ||
Permissions::hasPermission($user_id, $thread['server_id'], Permissions::MANAGE_SERVER) ||
Permissions::hasPermission($user_id, $thread['server_id'], Permissions::MANAGE_MESSAGES) ||
$server['owner_id'] == $user_id;
if ($thread['user_id'] != $user_id && !$is_admin) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
try {
db()->beginTransaction();
// Delete old tags
$stmt = db()->prepare("DELETE FROM thread_tags WHERE thread_id = ?");
$stmt->execute([$thread_id]);
// Insert new tags
if (!empty($tag_ids)) {
$stmtTag = db()->prepare("INSERT INTO thread_tags (thread_id, tag_id) VALUES (?, ?)");
foreach ($tag_ids as $tag_id) {
if ($tag_id) $stmtTag->execute([$thread_id, $tag_id]);
}
}
db()->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
$channel_id = $_POST['channel_id'] ?? 0;
$title = $_POST['title'] ?? '';
$user_id = $_SESSION['user_id'];
if (!$channel_id || !$title) {
echo json_encode(['success' => false, 'error' => 'Missing data']);
@ -105,7 +45,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || $_SERVER['REQUEST_METHOD'] === 'DELETE' || (isset($_GET['action']) && in_array($_GET['action'], ['solve', 'pin', 'unpin', 'lock', 'unlock', 'delete']))) {
if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || $_SERVER['REQUEST_METHOD'] === 'DELETE' || (isset($_GET['action']) && in_array($_GET['action'], ['solve', 'pin', 'unpin', 'lock', 'unlock', 'delete', 'update_tags']))) {
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$thread_id = $data['thread_id'] ?? $_GET['thread_id'] ?? 0;
$message_id = $data['message_id'] ?? null;
@ -187,9 +127,25 @@ if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || $_SERVER['REQUEST_METHOD'] === 'DE
$stmt = db()->prepare("DELETE FROM forum_threads WHERE id = ?");
$stmt->execute([$thread_id]);
db()->commit();
} elseif ($action === 'update_tags') {
if ($thread['user_id'] != $user_id && !$is_admin) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']); exit;
}
$tag_ids = $data['tag_ids'] ?? [];
db()->beginTransaction();
$stmt = db()->prepare("DELETE FROM thread_tags WHERE thread_id = ?");
$stmt->execute([$thread_id]);
if (!empty($tag_ids)) {
$stmtTag = db()->prepare("INSERT INTO thread_tags (thread_id, tag_id) VALUES (?, ?)");
foreach ($tag_ids as $tag_id) {
if ($tag_id) $stmtTag->execute([$thread_id, $tag_id]);
}
}
db()->commit();
}
echo json_encode(['success' => true]);
} catch (Exception $e) {
if (db()->inTransaction()) db()->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;

View File

@ -4042,46 +4042,67 @@ document.addEventListener('DOMContentLoaded', () => {
const modal = new bootstrap.Modal(document.getElementById('pollVotersModal'));
modal.show();
}
});
// Forum: Edit Thread Tags
const editThreadTagsBtn = e.target.closest('.edit-thread-tags');
if (editThreadTagsBtn) {
const threadId = editThreadTagsBtn.dataset.threadId;
const tagIds = editThreadTagsBtn.dataset.tagIds ? editThreadTagsBtn.dataset.tagIds.split(',') : [];
const modal = document.getElementById('editThreadTagsModal');
if (modal) {
document.getElementById('edit-tags-thread-id').value = threadId;
// Manage Thread Tags
const manageThreadTagsBtn = document.getElementById('manage-thread-tags');
if (manageThreadTagsBtn) {
manageThreadTagsBtn.addEventListener('click', () => {
const threadId = manageThreadTagsBtn.dataset.id;
const currentTags = manageThreadTagsBtn.dataset.tags ? manageThreadTagsBtn.dataset.tags.split(',').filter(id => id !== "") : [];
const threadIdInput = document.getElementById('manage-tags-thread-id');
if (threadIdInput) threadIdInput.value = threadId;
// Reset all checkboxes
document.querySelectorAll('.tag-checkbox').forEach(cb => {
cb.checked = currentTags.includes(cb.value);
});
const modalEl = document.getElementById('manageThreadTagsModal');
if (modalEl) {
const modal = new bootstrap.Modal(modalEl);
modal.show();
}
});
}
const saveThreadTagsBtn = document.getElementById('save-thread-tags-btn');
if (saveThreadTagsBtn) {
saveThreadTagsBtn.addEventListener('click', async () => {
const threadIdInput = document.getElementById('manage-tags-thread-id');
const threadId = threadIdInput ? threadIdInput.value : null;
if (!threadId) return;
const selectedTags = Array.from(document.querySelectorAll('.tag-checkbox:checked')).map(cb => cb.value);
try {
saveThreadTagsBtn.disabled = true;
saveThreadTagsBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Enregistrement...';
// Clear and then check checkboxes
const checkboxes = modal.querySelectorAll('.tag-checkbox');
checkboxes.forEach(cb => {
cb.checked = tagIds.includes(cb.value);
const resp = await fetch('api_v1_threads.php?action=update_tags', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
thread_id: threadId,
tag_ids: selectedTags
})
});
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
const result = await resp.json();
if (result.success) {
location.reload();
} else {
alert(result.error || 'Erreur lors de la mise à jour des tags');
saveThreadTagsBtn.disabled = false;
saveThreadTagsBtn.innerHTML = 'Enregistrer';
}
} catch (e) {
console.error(e);
alert('Une erreur est survenue');
saveThreadTagsBtn.disabled = false;
saveThreadTagsBtn.innerHTML = 'Enregistrer';
}
}
});
document.getElementById('edit-thread-tags-form')?.addEventListener('submit', async (ev) => {
ev.preventDefault();
const form = ev.target;
const formData = new FormData(form);
try {
const resp = await fetch('api_v1_threads.php', {
method: 'POST',
body: formData
});
const data = await resp.json();
if (data.success) {
location.reload();
} else {
alert(data.error || 'Erreur lors de la mise à jour des tags');
}
} catch (err) {
console.error(err);
}
});
});
});
}
});

105
index.php
View File

@ -257,11 +257,14 @@ if ($is_dm_view) {
$active_thread = null;
if ($active_thread_id) {
$stmt = db()->prepare("SELECT t.*, (SELECT GROUP_CONCAT(CONCAT(ft.id, ':', ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags, u.display_name as username, u.username as login_name FROM forum_threads t JOIN users u ON t.user_id = u.id WHERE t.id = ?");
$stmt = db()->prepare("SELECT t.*, (SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags, u.display_name as username, u.username as login_name FROM forum_threads t JOIN users u ON t.user_id = u.id WHERE t.id = ?");
$stmt->execute([$active_thread_id]);
$active_thread = $stmt->fetch();
if ($active_thread) {
$stmt_t_ids = db()->prepare("SELECT tag_id FROM thread_tags WHERE thread_id = ?");
$stmt_t_ids->execute([$active_thread_id]);
$active_thread_tag_ids = $stmt_t_ids->fetchAll(PDO::FETCH_COLUMN);
$stmt = db()->prepare("
SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
@ -309,7 +312,7 @@ if ($is_dm_view) {
}
$stmt = db()->prepare("
SELECT t.*, (SELECT GROUP_CONCAT(CONCAT(ft.id, ':', ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags, u.display_name as username, u.avatar_url,
SELECT t.*, (SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags, u.display_name as username, u.avatar_url,
(SELECT COUNT(*) FROM messages m WHERE m.thread_id = t.id) as message_count,
(SELECT MAX(created_at) FROM messages m WHERE m.thread_id = t.id) as last_message_at,
(SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color,
@ -881,6 +884,11 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div class="d-flex justify-content-between align-items-center mb-3">
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?><?php echo isset($_GET['status']) ? '&status='.htmlspecialchars($_GET['status']) : ''; ?>" class="btn btn-sm btn-outline-secondary"> Retour au forum</a>
<div class="d-flex gap-2">
<?php if ($active_thread["user_id"] == $current_user_id || $can_manage_server): ?>
<button class="btn btn-sm btn-outline-info" id="manage-thread-tags" data-id="<?php echo $active_thread["id"]; ?>" data-tags="<?php echo implode(",", $active_thread_tag_ids); ?>">
<i class="fa-solid fa-tags me-1"></i> Gérer les tags
</button>
<?php endif; ?>
<?php if (Permissions::canDoInChannel($current_user_id, $active_channel_id, Permissions::PIN_THREADS)): ?>
<button class="btn btn-sm <?php echo $active_thread['is_pinned'] ? 'btn-primary' : 'btn-outline-primary'; ?>" id="toggle-pin-thread" data-id="<?php echo $active_thread['id']; ?>" data-pinned="<?php echo $active_thread['is_pinned']; ?>">
<i class="fa-solid fa-thumbtack me-1"></i> <?php echo $active_thread['is_pinned'] ? 'Désépingler' : 'Épingler'; ?>
@ -1428,7 +1436,7 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<?php if($thread['tags']):
$tag_list = explode('|', $thread['tags']);
foreach($tag_list as $tag_data):
list($t_id, $t_name, $t_color) = explode(':', $tag_data);
list($t_name, $t_color) = explode(':', $tag_data);
?>
<span class="badge rounded-pill ms-1" style="background-color: <?php echo htmlspecialchars($t_color); ?>; font-size: 0.6em;"><?php echo htmlspecialchars($t_name); ?></span>
<?php endforeach; endif; ?>
@ -1460,37 +1468,6 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
<div class="small text-muted mt-1">
Par <?php echo htmlspecialchars($active_thread['username']); ?> • Dans #<?php echo htmlspecialchars($current_channel_name); ?>
</div>
<?php if($channel_type === 'forum'): ?>
<div class="mt-2 d-flex flex-wrap gap-1 align-items-center">
<?php if(!empty($active_thread['tags'])):
$tag_list = explode('|', $active_thread['tags']);
foreach($tag_list as $tag_data):
list($t_id, $t_name, $t_color) = explode(':', $tag_data);
?>
<span class="badge rounded-pill" style="background-color: <?php echo htmlspecialchars($t_color); ?>; font-size: 0.7em;">
<?php echo htmlspecialchars($t_name); ?>
</span>
<?php endforeach; endif; ?>
<?php if($active_thread['user_id'] == $current_user_id || $can_manage_channels): ?>
<button class="btn btn-link btn-sm p-0 ms-1 text-muted text-decoration-none edit-thread-tags"
data-thread-id="<?php echo $active_thread['id']; ?>"
data-active-tags="<?php
$active_tags_ids = [];
if (!empty($active_thread['tags'])) {
$tag_list_ids = explode('|', $active_thread['tags']);
foreach($tag_list_ids as $tdid) {
$parts_id = explode(':', $tdid);
$active_tags_ids[] = $parts_id[0];
}
}
echo implode(',', $active_tags_ids);
?>" title="Gérer les tags">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<?php endif; ?>
</div>
<?php endif; ?>
</div>
<a href="?server_id=<?php echo $active_server_id; ?>&channel_id=<?php echo $active_channel_id; ?><?php echo !empty($selected_tag_ids) ? '&tags='.implode(',', $selected_tag_ids) : ''; ?>" class="btn btn-outline-secondary btn-sm">
<i class="fa-solid fa-arrow-left me-1"></i> Retour au forum
@ -3933,40 +3910,46 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
</div>
<!-- Edit Thread Tags Modal -->
<div class="modal fade" id="editThreadTagsModal" tabindex="-1">
<!-- Manage Thread Tags Modal -->
<div class="modal fade" id="manageThreadTagsModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header border-0">
<h5 class="modal-title">Modifier les tags</h5>
<div class="modal-content border-0 shadow-lg" style="background-color: #313338; color: white;">
<div class="modal-header border-0 pb-0">
<h5 class="modal-title fw-bold">Gérer les tags de la discussion</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="edit-thread-tags-form">
<input type="hidden" name="thread_id" id="edit-tags-thread-id">
<input type="hidden" name="action" value="update_tags">
<?php if (!empty($forum_tags)): ?>
<div class="mb-3">
<label class="form-label d-block text-muted small fw-bold text-uppercase">Tags disponibles</label>
<div id="edit-thread-tags-list" class="d-flex flex-wrap gap-2 p-3 bg-dark bg-opacity-50 rounded">
<?php foreach ($forum_tags as $tag): ?>
<div class="form-check form-check-inline m-0">
<input class="btn-check tag-checkbox" type="checkbox" name="tag_ids[]" value="<?php echo $tag['id']; ?>" id="edit-tag-<?php echo $tag['id']; ?>">
<label class="btn btn-outline-secondary btn-sm rounded-pill px-3" for="edit-tag-<?php echo $tag['id']; ?>" style="--bs-btn-active-bg: <?php echo $tag['color']; ?>; --bs-btn-active-border-color: <?php echo $tag['color']; ?>;">
<?php echo htmlspecialchars($tag['name']); ?>
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<?php else: ?>
<p class="text-muted text-center py-3">Aucun tag n'est configuré pour ce salon.</p>
<?php endif; ?>
<button type="submit" class="btn btn-primary w-100 mt-2 py-2 fw-bold">Enregistrer les modifications</button>
</form>
<input type="hidden" id="manage-tags-thread-id">
<div class="mb-3">
<label class="form-label text-uppercase fw-bold mb-2" style="font-size: 0.75em; color: #b5bac1;">Tags disponibles</label>
<div id="thread-tags-selection-list" class="d-flex flex-wrap gap-2">
<?php if (isset($forum_tags)): ?>
<?php foreach($forum_tags as $tag): ?>
<div class="tag-selection-item">
<input type="checkbox" class="tag-checkbox btn-check" id="tag-check-<?php echo $tag["id"]; ?>" value="<?php echo $tag["id"]; ?>" autocomplete="off">
<label class="btn btn-sm btn-outline-light rounded-pill px-3" for="tag-check-<?php echo $tag["id"]; ?>" style="border-color: <?php echo $tag["color"]; ?>; color: <?php echo $tag["color"]; ?>;">
<?php echo htmlspecialchars($tag["name"]); ?>
</label>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</div>
<div class="modal-footer border-0 pt-0">
<button type="button" class="btn btn-link text-white text-decoration-none" data-bs-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-primary px-4" id="save-thread-tags-btn">Enregistrer</button>
</div>
</div>
</div>
</div>
<style>
.tag-selection-item .btn-check:checked + .btn {
background-color: var(--bs-btn-border-color);
color: #fff !important;
}
</style>
</body>
</html>