Skip to content

Commit b405c55

Browse files
Merge pull request #6989 from christianbeeznest/fixes-updates176
Forum: visual polish for threads & posts
2 parents 5cc53df + 41cc1e4 commit b405c55

File tree

4 files changed

+523
-265
lines changed

4 files changed

+523
-265
lines changed

public/main/forum/forumfunction.inc.php

Lines changed: 190 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,18 +1276,25 @@ function getPosts(
12761276
{
12771277
$em = Database::getManager();
12781278

1279+
$orderDirection = strtoupper($orderDirection);
1280+
if (!in_array($orderDirection, ['ASC', 'DESC'], true)) {
1281+
$orderDirection = 'ASC';
1282+
}
1283+
1284+
// Build visibility criteria based on permissions
12791285
if (api_is_allowed_to_edit(false, true)) {
1286+
// Entity maps 'visible' as boolean; keeping legacy behavior as requested.
12801287
$visibleCriteria = Criteria::expr()->neq('visible', 2);
12811288
} else {
12821289
$visibleCriteria = Criteria::expr()->eq('visible', 1);
12831290
}
12841291

1292+
$threadRef = $em->getReference(CForumThread::class, $threadId);
1293+
12851294
$criteria = Criteria::create();
12861295
$criteria
1287-
->where(Criteria::expr()->eq('thread', $threadId))
1288-
//->andWhere(Criteria::expr()->eq('cId', $forum->getCId()))
1289-
->andWhere($visibleCriteria)
1290-
;
1296+
->where(Criteria::expr()->eq('thread', $threadRef))
1297+
->andWhere($visibleCriteria);
12911298

12921299
$groupId = api_get_group_id();
12931300
$filterModerated = true;
@@ -1306,24 +1313,33 @@ function getPosts(
13061313
}
13071314

13081315
if ($recursive) {
1309-
$criteria->andWhere(Criteria::expr()->eq('postParent', $postId));
1316+
// Compare association with entity reference when filtering by parent
1317+
$parentRef = $postId ? $em->getReference(CForumPost::class, $postId) : null;
1318+
$criteria->andWhere(Criteria::expr()->eq('postParent', $parentRef));
13101319
}
13111320

13121321
$qb = $em->getRepository(CForumPost::class)->createQueryBuilder('p');
13131322
$qb->select('p')
13141323
->addCriteria($criteria)
13151324
->addOrderBy('p.iid', $orderDirection);
13161325

1326+
// Apply moderation filter if forum is moderated and user is not editor
13171327
if ($filterModerated && 1 == $forum->isModerated()) {
13181328
if (!api_is_allowed_to_edit(false, true)) {
13191329
$userId = api_get_user_id();
1330+
1331+
// instead of a non-existent scalar field like p.posterId
13201332
$qb->andWhere(
1321-
'p.status = 1 OR
1322-
(p.status = '.CForumPost::STATUS_WAITING_MODERATION." AND p.posterId = $userId) OR
1323-
(p.status = ".CForumPost::STATUS_REJECTED." AND p.posterId = $userId) OR
1324-
(p.status IS NULL AND p.posterId = $userId)
1325-
"
1326-
);
1333+
'(p.status = :st_valid)
1334+
OR (p.status IN (:st_own) AND IDENTITY(p.user) = :uid)
1335+
OR (p.status IS NULL AND IDENTITY(p.user) = :uid)'
1336+
)
1337+
->setParameter('st_valid', CForumPost::STATUS_VALIDATED)
1338+
->setParameter('st_own', [
1339+
CForumPost::STATUS_WAITING_MODERATION,
1340+
CForumPost::STATUS_REJECTED,
1341+
])
1342+
->setParameter('uid', $userId);
13271343
}
13281344
}
13291345

@@ -1340,8 +1356,6 @@ function getPosts(
13401356
'post_text' => $post->getPostText(),
13411357
'thread_id' => $post->getThread() ? $post->getThread()->getIid() : 0,
13421358
'forum_id' => $post->getForum()->getIid(),
1343-
//'poster_id' => $post->getPosterId(),
1344-
//'poster_name' => $post->getPosterName(),
13451359
'post_date' => $post->getPostDate(),
13461360
'post_notification' => $post->getPostNotification(),
13471361
'post_parent_id' => $post->getPostParent() ? $post->getPostParent()->getIid() : 0,
@@ -1351,6 +1365,7 @@ function getPosts(
13511365
'entity' => $post,
13521366
];
13531367

1368+
// Fill user info if available
13541369
$user = $post->getUser();
13551370
if ($user) {
13561371
$postInfo['user_id'] = $user->getId();
@@ -1366,6 +1381,8 @@ function getPosts(
13661381
if (!$recursive) {
13671382
continue;
13681383
}
1384+
1385+
// Recursive fetch of children when requested
13691386
$list = array_merge(
13701387
$list,
13711388
getPosts(
@@ -2005,17 +2022,6 @@ function show_add_post_form(CForum $forum, CForumThread $thread, CForumPost $pos
20052022
);
20062023
}
20072024

2008-
$iframe = null;
2009-
if ($showPreview) {
2010-
if ('newthread' !== $action && !empty($threadId)) {
2011-
$iframe = '<iframe style="border: 1px solid black"
2012-
src="'.api_get_path(WEB_CODE_PATH).'forum/iframe_thread.php?'.api_get_cidreq().'&forum='.$forumId.'&thread='.$threadId.'#'.$postId.'" width="100%"></iframe>';
2013-
}
2014-
if (!empty($iframe)) {
2015-
$form->addElement('label', get_lang('Thread'), $iframe);
2016-
}
2017-
}
2018-
20192025
if (in_array($action, ['quote', 'replymessage'])) {
20202026
$form->addFile('user_upload[]', get_lang('Attachment'));
20212027
$form->addButton(
@@ -2110,6 +2116,11 @@ function show_add_post_form(CForum $forum, CForumThread $thread, CForumPost $pos
21102116
);
21112117
}
21122118

2119+
if ($showPreview && 'newthread' !== $action && !empty($threadId)) {
2120+
$previewHtml = render_thread_preview_html((int) $forumId, (int) $threadId, (int) $postId, 20);
2121+
$form->addElement('html', '<div class="mt-4">'.$previewHtml.'</div>');
2122+
}
2123+
21132124
// Validation or display
21142125
if ($form->validate()) {
21152126
$check = Security::check_token('post');
@@ -2202,6 +2213,102 @@ function show_add_post_form(CForum $forum, CForumThread $thread, CForumPost $pos
22022213
}
22032214
}
22042215

2216+
/**
2217+
* Build a lightweight Tailwind-based preview for a thread (no iframe).
2218+
* Security: keep XSS protection as in the original iframe.
2219+
*/
2220+
function render_thread_preview_html(
2221+
int $forumId,
2222+
int $threadId,
2223+
int $highlightPostId = 0,
2224+
int $limit = 20,
2225+
string $heightClass = 'h-72'
2226+
): string {
2227+
$tablePosts = Database::get_course_table(TABLE_FORUM_POST); // c_forum_post
2228+
$tableUsers = Database::get_main_table(TABLE_MAIN_USER); // user
2229+
2230+
$sql = "SELECT
2231+
p.iid,
2232+
p.post_date,
2233+
p.title AS post_title,
2234+
p.post_text,
2235+
u.id AS user_id,
2236+
u.username,
2237+
u.firstname,
2238+
u.lastname
2239+
FROM $tablePosts p
2240+
INNER JOIN $tableUsers u ON u.id = p.poster_id
2241+
WHERE p.thread_id = ".(int)$threadId."
2242+
AND p.visible = 1
2243+
AND (p.status IS NULL OR p.status = ".(int) CForumPost::STATUS_VALIDATED.")
2244+
ORDER BY p.post_date ASC
2245+
LIMIT ".(int)$limit;
2246+
2247+
$res = Database::query($sql);
2248+
$rows = [];
2249+
while ($r = Database::fetch_array($res)) {
2250+
$rows[] = $r;
2251+
}
2252+
2253+
ob_start(); ?>
2254+
<div class="mt-6 w-full">
2255+
<div
2256+
class="forum-preview <?php echo $heightClass ?> overflow-y-auto border border-gray-25 rounded-xl p-3 bg-white"
2257+
role="region"
2258+
aria-label="<?php echo get_lang('Discussion thread'); ?>"
2259+
>
2260+
<div class="sticky top-0 z-10 bg-white pb-2 text-gray-90 font-bold shadow-[0_1px_0_0_#e4e9ed]">
2261+
<?php echo get_lang('Discussion thread'); ?>
2262+
</div>
2263+
2264+
<?php if (empty($rows)): ?>
2265+
<div class="text-gray-50 text-body-2 mt-2"><?php echo get_lang('No posts yet'); ?></div>
2266+
<?php else: foreach ($rows as $row):
2267+
$postId = (int) $row['iid'];
2268+
$isFocus = $highlightPostId && $postId === (int) $highlightPostId;
2269+
2270+
// Compute display name (fallback to username)
2271+
$isAnon = ((int)$row['user_id'] === (int)\Chamilo\CoreBundle\Entity\User::ANONYMOUS);
2272+
$displayName = $isAnon
2273+
? get_lang('Anonymous')
2274+
: (api_get_person_name($row['firstname'], $row['lastname']) ?: $row['username']);
2275+
2276+
$userTitle = api_htmlentities(sprintf(get_lang('Login: %s'), $row['username']), ENT_QUOTES);
2277+
?>
2278+
<article id="post-<?php echo $postId ?>"
2279+
class="mb-2 border border-gray-25 rounded-lg p-3 <?php echo $isFocus ? 'bg-gray-15 ring-1 ring-primary' : 'bg-white' ?>">
2280+
<div class="text-caption text-gray-50" title="<?php echo $userTitle ?>">
2281+
<?php echo Security::remove_XSS($displayName) ?><?php echo api_convert_and_format_date($row['post_date']) ?>
2282+
</div>
2283+
2284+
<div class="font-bold mt-1 mb-1 text-gray-90">
2285+
<?php echo Security::remove_XSS($row['post_title']) ?>
2286+
</div>
2287+
2288+
<div class="text-body-2 leading-4">
2289+
<?php echo Security::remove_XSS($row['post_text'], STUDENT) ?>
2290+
</div>
2291+
</article>
2292+
<?php endforeach; endif; ?>
2293+
</div>
2294+
2295+
<div class="mt-2">
2296+
<a class="text-body-2 underline text-primary"
2297+
href="<?php echo api_get_path(WEB_CODE_PATH) . 'forum/viewthread.php?' . api_get_cidreq()
2298+
. '&forum='.(int)$forumId.'&thread='.(int)$threadId ?>">
2299+
<?php echo get_lang('View full thread') ?>
2300+
</a>
2301+
</div>
2302+
</div>
2303+
<?php
2304+
if ($highlightPostId) {
2305+
// Try to bring the highlighted post into view.
2306+
echo '<script>try{document.getElementById("post-'.$highlightPostId.'")?.scrollIntoView({behavior:"instant",block:"start"});}catch(e){}</script>';
2307+
}
2308+
2309+
return (string) ob_get_clean();
2310+
}
2311+
22052312
function newThread(CForum $forum, $form_values = '', $showPreview = true)
22062313
{
22072314
$_user = api_get_user_info();
@@ -5167,29 +5274,76 @@ function getCountPostsWithStatus($status, $forum, $threadId = null)
51675274
*
51685275
* @return bool
51695276
*/
5170-
function postIsEditableByStudent($forum, $post)
5277+
/**
5278+
* Student editability guard that accepts either an entity or the array shape used in views.
5279+
*
5280+
* @param CForum $forum
5281+
* @param CForumPost|array $post
5282+
*/
5283+
function postIsEditableByStudent(CForum $forum, $post): bool
51715284
{
5285+
// Normalize $post to an entity
5286+
/** @var CForumPost|null $entity */
5287+
$entity = null;
5288+
5289+
if ($post instanceof CForumPost) {
5290+
$entity = $post;
5291+
} elseif (is_array($post)) {
5292+
// Prefer the embedded entity from getPosts()
5293+
if (isset($post['entity']) && $post['entity'] instanceof CForumPost) {
5294+
$entity = $post['entity'];
5295+
} else {
5296+
// Fallback: fetch by id if present in the array
5297+
$postId = $post['post_id'] ?? $post['iid'] ?? null;
5298+
if ($postId) {
5299+
$em = Database::getManager();
5300+
$entity = $em->getRepository(CForumPost::class)->find($postId);
5301+
}
5302+
}
5303+
}
5304+
5305+
// If we still don't have a valid entity, be conservative: deny student edit
5306+
if (!$entity instanceof CForumPost) {
5307+
return false;
5308+
}
5309+
5310+
// Admins/teachers can always edit
51725311
if (api_is_platform_admin() || api_is_allowed_to_edit()) {
51735312
return true;
51745313
}
51755314

5176-
if (1 == $forum->isModerated()) {
5177-
if (null === $post->getStatus()) {
5315+
$isModerated = (bool) $forum->isModerated();
5316+
$userId = (int) api_get_user_id();
5317+
$ownerId = $entity->getUser() ? (int) $entity->getUser()->getId() : 0;
5318+
$isOwner = ($ownerId === $userId);
5319+
$status = $entity->getStatus();
5320+
5321+
// Unmoderated forum: student can edit only own posts
5322+
if (!$isModerated) {
5323+
return $isOwner;
5324+
}
5325+
5326+
// editable by owner when status is NULL or WAITING or REJECTED.
5327+
if ($isOwner) {
5328+
if ($status === null) {
51785329
return true;
5179-
} else {
5180-
return in_array(
5181-
$post->getStatus(),
5182-
[
5183-
CForumPost::STATUS_WAITING_MODERATION,
5184-
CForumPost::STATUS_REJECTED,
5185-
]
5186-
);
51875330
}
5331+
5332+
return in_array(
5333+
$status,
5334+
[
5335+
CForumPost::STATUS_WAITING_MODERATION,
5336+
CForumPost::STATUS_REJECTED,
5337+
],
5338+
true
5339+
);
51885340
}
51895341

5190-
return true;
5342+
// Not owner and not a teacher/admin
5343+
return false;
51915344
}
51925345

5346+
51935347
/**
51945348
* @return bool
51955349
*/

0 commit comments

Comments
 (0)