add max comment lenght logic and truncation logic
This commit is contained in:
@@ -1933,7 +1933,8 @@ body.sidebar-right-hidden .global-sidebar-right {
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-full-comment-btn {
|
.load-full-comment-btn,
|
||||||
|
.collapse-comment-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -1947,7 +1948,8 @@ body.sidebar-right-hidden .global-sidebar-right {
|
|||||||
transition: opacity 0.15s;
|
transition: opacity 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.load-full-comment-btn:hover {
|
.load-full-comment-btn:hover,
|
||||||
|
.collapse-comment-btn:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2299,6 +2301,27 @@ body.layout-modern .item-sidebar-left .tag-controls {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.char-counter {
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(255, 255, 255, 0.35);
|
||||||
|
font-family: monospace;
|
||||||
|
margin-right: auto;
|
||||||
|
align-self: center;
|
||||||
|
transition: color 0.2s ease, font-weight 0.2s ease;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-counter.near-limit {
|
||||||
|
color: #e6a817;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.char-counter.at-limit {
|
||||||
|
color: #e84040;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.comments-list {
|
.comments-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -4995,7 +5018,6 @@ body[type='login'] {
|
|||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--white);
|
color: var(--white);
|
||||||
font-size: 20px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
padding: 0 5px;
|
padding: 0 5px;
|
||||||
|
|||||||
@@ -369,8 +369,23 @@ class CommentSystem {
|
|||||||
// 1. Check if comment belongs to this item
|
// 1. Check if comment belongs to this item
|
||||||
if (parseInt(data.item_id) !== parseInt(this.itemId)) return;
|
if (parseInt(data.item_id) !== parseInt(this.itemId)) return;
|
||||||
|
|
||||||
// 2. Check for duplicates (if we just posted it ourselves)
|
// 2. Check for duplicates (if we just posted it ourselves via optimistic insert).
|
||||||
if (document.getElementById('c' + data.id)) return;
|
// Even on early return, ensure the button is present if body was truncated.
|
||||||
|
if (document.getElementById('c' + data.id)) {
|
||||||
|
if (data.body && data.body.endsWith('\u2026')) {
|
||||||
|
console.log('[handleLiveComment] duplicate+truncated, ensuring button for', data.id);
|
||||||
|
const el = document.getElementById('c' + data.id);
|
||||||
|
const contentEl = el?.querySelector('.comment-content');
|
||||||
|
if (contentEl && !contentEl.querySelector('.load-full-comment-btn')) {
|
||||||
|
const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment';
|
||||||
|
contentEl.insertAdjacentHTML('beforeend',
|
||||||
|
`<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this._patchLiveCommentContent(data.id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 3. Robustness check: If we don't have lastData, initialize it.
|
// 3. Robustness check: If we don't have lastData, initialize it.
|
||||||
if (!this.lastData) {
|
if (!this.lastData) {
|
||||||
@@ -447,6 +462,51 @@ class CommentSystem {
|
|||||||
el.classList.add('new-item-fade');
|
el.classList.add('new-item-fade');
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
|
// 7. The NOTIFY body is capped at 500 chars to stay under PostgreSQL's 8KB limit.
|
||||||
|
// If the body was truncated, immediately show the "show full comment" button
|
||||||
|
// and then fetch + patch the real full content asynchronously.
|
||||||
|
if (data.body && data.body.endsWith('\u2026')) {
|
||||||
|
console.log('[handleLiveComment] body truncated, patching comment', data.id);
|
||||||
|
const el = document.getElementById('c' + data.id);
|
||||||
|
if (el) {
|
||||||
|
const contentEl = el.querySelector('.comment-content');
|
||||||
|
if (contentEl && !contentEl.querySelector('.load-full-comment-btn')) {
|
||||||
|
const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment';
|
||||||
|
contentEl.insertAdjacentHTML('beforeend',
|
||||||
|
`<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._patchLiveCommentContent(data.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _patchLiveCommentContent(commentId) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/comment/${commentId}`);
|
||||||
|
if (!res.ok) return;
|
||||||
|
const json = await res.json();
|
||||||
|
if (!json.success || !json.comment) return;
|
||||||
|
|
||||||
|
const fullContent = json.comment.content;
|
||||||
|
const el = document.getElementById('c' + commentId);
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const contentEl = el.querySelector('.comment-content');
|
||||||
|
if (!contentEl) return;
|
||||||
|
|
||||||
|
// Update in-memory data so future reconciles use the full content
|
||||||
|
if (this.lastData) {
|
||||||
|
const cached = this.lastData.find(c => String(c.id) === String(commentId));
|
||||||
|
if (cached) cached.content = fullContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentEl.dataset.raw = this.escapeHtml(fullContent);
|
||||||
|
contentEl.innerHTML = this.renderCommentContent(fullContent, commentId);
|
||||||
|
} catch (e) {
|
||||||
|
_f0ckDebug('[CommentSystem] _patchLiveCommentContent failed:', e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLiveEdit(data) {
|
handleLiveEdit(data) {
|
||||||
@@ -968,8 +1028,12 @@ class CommentSystem {
|
|||||||
const author = openerEl.dataset.display || openerEl.dataset.username || 'System';
|
const author = openerEl.dataset.display || openerEl.dataset.username || 'System';
|
||||||
const contentEl = body.querySelector('.comment-content');
|
const contentEl = body.querySelector('.comment-content');
|
||||||
if (contentEl) {
|
if (contentEl) {
|
||||||
|
const LINE_MAX = 200;
|
||||||
const rawText = (contentEl.dataset.raw || '').trim();
|
const rawText = (contentEl.dataset.raw || '').trim();
|
||||||
const lines = rawText.split('\n');
|
// Preserve all lines but cap any single line exceeding LINE_MAX chars
|
||||||
|
const lines = rawText.split('\n').map(line =>
|
||||||
|
line.length > LINE_MAX ? line.substring(0, LINE_MAX) + '\u2026' : line
|
||||||
|
);
|
||||||
const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
const quote = `>>${id} \n>${author}\n${lines.map(line => `>${line}`).join('\n')}\n`;
|
||||||
|
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
@@ -1380,7 +1444,7 @@ class CommentSystem {
|
|||||||
// Truncate extremely long comments before any processing
|
// Truncate extremely long comments before any processing
|
||||||
let truncated = false;
|
let truncated = false;
|
||||||
if (!bypassTruncation && content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
if (!bypassTruncation && content.length > CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
||||||
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS);
|
content = content.substring(0, CommentSystem.ITEM_VIEW_MAX_CHARS) + '\u2026';
|
||||||
truncated = true;
|
truncated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1642,6 +1706,8 @@ class CommentSystem {
|
|||||||
md = Sanitizer.clean(md);
|
md = Sanitizer.clean(md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append the "show full comment" button AFTER sanitization — the sanitizer
|
||||||
|
// whitelist strips <button> elements for XSS safety, but this button is ours.
|
||||||
if (truncated) {
|
if (truncated) {
|
||||||
const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment';
|
const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment';
|
||||||
md += `<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`;
|
md += `<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`;
|
||||||
@@ -1841,10 +1907,16 @@ class CommentSystem {
|
|||||||
const placeholder = i18n.write_comment || 'Write a comment...';
|
const placeholder = i18n.write_comment || 'Write a comment...';
|
||||||
const postLabel = i18n.post || 'Post';
|
const postLabel = i18n.post || 'Post';
|
||||||
const cancelLabel = i18n.cancel || 'Cancel';
|
const cancelLabel = i18n.cancel || 'Cancel';
|
||||||
|
const maxLen = window.f0ckSession?.comment_max_length;
|
||||||
|
const maxLenAttr = (maxLen !== null && maxLen !== undefined) ? ` maxlength="${maxLen}"` : '';
|
||||||
|
const counter = (maxLen !== null && maxLen !== undefined)
|
||||||
|
? `<span class="char-counter" data-max="${maxLen}">0 / ${maxLen}</span>`
|
||||||
|
: '';
|
||||||
return `
|
return `
|
||||||
<div class="comment-input ${parentId ? 'reply-input' : 'main-input'}" ${parentId ? `data-parent="${parentId}"` : ''}>
|
<div class="comment-input ${parentId ? 'reply-input' : 'main-input'}" ${parentId ? `data-parent="${parentId}"` : ''}>
|
||||||
<textarea placeholder="${placeholder}"></textarea>
|
<textarea placeholder="${placeholder}"${maxLenAttr}></textarea>
|
||||||
<div class="input-actions">
|
<div class="input-actions">
|
||||||
|
${counter}
|
||||||
${parentId ? `<button class="cancel-reply">${cancelLabel}</button>` : ''}
|
${parentId ? `<button class="cancel-reply">${cancelLabel}</button>` : ''}
|
||||||
<button class="submit-comment">${postLabel}</button>
|
<button class="submit-comment">${postLabel}</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1980,6 +2052,25 @@ class CommentSystem {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Live character counter (only active when comment_max_length is set)
|
||||||
|
this.container.addEventListener('input', (e) => {
|
||||||
|
const textarea = e.target.closest('.comment-input textarea');
|
||||||
|
if (!textarea) return;
|
||||||
|
const counter = textarea.closest('.comment-input')?.querySelector('.char-counter');
|
||||||
|
if (!counter) return;
|
||||||
|
const max = parseInt(counter.dataset.max, 10);
|
||||||
|
// Exclude quoted lines (starting with '>') from the count —
|
||||||
|
// quoted context is capped at 200 chars and shouldn't eat into the user's limit.
|
||||||
|
const nonQuotedLen = textarea.value
|
||||||
|
.split('\n')
|
||||||
|
.filter(line => !line.startsWith('>'))
|
||||||
|
.join('\n')
|
||||||
|
.length;
|
||||||
|
counter.textContent = `${nonQuotedLen} / ${max}`;
|
||||||
|
counter.classList.toggle('near-limit', nonQuotedLen >= max * 0.9);
|
||||||
|
counter.classList.toggle('at-limit', nonQuotedLen >= max);
|
||||||
|
});
|
||||||
|
|
||||||
// Single Change Listener for Sort
|
// Single Change Listener for Sort
|
||||||
this.container.addEventListener('change', (e) => {
|
this.container.addEventListener('change', (e) => {
|
||||||
if (e.target.id === 'comment-sort') {
|
if (e.target.id === 'comment-sort') {
|
||||||
@@ -2036,6 +2127,24 @@ class CommentSystem {
|
|||||||
const fullContent = contentEl.dataset.raw;
|
const fullContent = contentEl.dataset.raw;
|
||||||
if (fullContent) {
|
if (fullContent) {
|
||||||
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
|
contentEl.innerHTML = this.renderCommentContent(fullContent, null, true);
|
||||||
|
// Append "see less" button after full content
|
||||||
|
const seeLessLabel = (window.f0ckI18n?.sidebar_see_less) || 'see less';
|
||||||
|
contentEl.insertAdjacentHTML('beforeend',
|
||||||
|
`<span class="item-comment-truncated-notice"><button class="collapse-comment-btn" type="button">${seeLessLabel}</button></span>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse full comment back to truncated view
|
||||||
|
const collapseBtn = target.closest('.collapse-comment-btn');
|
||||||
|
if (collapseBtn) {
|
||||||
|
const contentEl = collapseBtn.closest('.comment-content');
|
||||||
|
if (contentEl) {
|
||||||
|
const fullContent = contentEl.dataset.raw;
|
||||||
|
if (fullContent) {
|
||||||
|
contentEl.innerHTML = this.renderCommentContent(fullContent, null, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -2353,6 +2462,11 @@ class CommentSystem {
|
|||||||
if (formRow) formRow.remove();
|
if (formRow) formRow.remove();
|
||||||
} else {
|
} else {
|
||||||
if (textarea) textarea.value = '';
|
if (textarea) textarea.value = '';
|
||||||
|
const counter = wrap.querySelector('.char-counter');
|
||||||
|
if (counter) {
|
||||||
|
counter.textContent = `0 / ${counter.dataset.max}`;
|
||||||
|
counter.classList.remove('near-limit', 'at-limit');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the right sidebar that a new comment was posted (silent refresh)
|
// Notify the right sidebar that a new comment was posted (silent refresh)
|
||||||
@@ -2453,6 +2567,7 @@ class CommentSystem {
|
|||||||
const commentEl = tmp.firstElementChild;
|
const commentEl = tmp.firstElementChild;
|
||||||
if (commentEl) {
|
if (commentEl) {
|
||||||
repliesEl.appendChild(commentEl);
|
repliesEl.appendChild(commentEl);
|
||||||
|
this._ensureTruncationButton(commentEl, newComment.content);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
commentEl.classList.add('comment-entering', 'new-item-fade');
|
commentEl.classList.add('comment-entering', 'new-item-fade');
|
||||||
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
@@ -2482,6 +2597,7 @@ class CommentSystem {
|
|||||||
} else {
|
} else {
|
||||||
list.appendChild(commentEl);
|
list.appendChild(commentEl);
|
||||||
}
|
}
|
||||||
|
this._ensureTruncationButton(commentEl, newComment.content);
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
commentEl.classList.add('comment-entering', 'new-item-fade');
|
commentEl.classList.add('comment-entering', 'new-item-fade');
|
||||||
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
commentEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
@@ -2520,6 +2636,30 @@ class CommentSystem {
|
|||||||
attemptSubmit();
|
attemptSubmit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defensive: ensure the "show full comment" button is present in a rendered comment
|
||||||
|
// element whenever the raw content exceeds ITEM_VIEW_MAX_CHARS. Called after every
|
||||||
|
// optimistic DOM insert to guarantee the button regardless of rendering path.
|
||||||
|
_ensureTruncationButton(commentEl, rawContent) {
|
||||||
|
console.log('[ensureBtn] called, rawContent.length:', rawContent?.length, 'threshold:', CommentSystem.ITEM_VIEW_MAX_CHARS);
|
||||||
|
if (!rawContent || rawContent.length <= CommentSystem.ITEM_VIEW_MAX_CHARS) {
|
||||||
|
console.log('[ensureBtn] below threshold, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const contentEl = commentEl.querySelector('.comment-content');
|
||||||
|
if (!contentEl) { console.log('[ensureBtn] no .comment-content found'); return; }
|
||||||
|
if (contentEl.querySelector('.load-full-comment-btn')) {
|
||||||
|
console.log('[ensureBtn] button already present');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log('[ensureBtn] injecting button');
|
||||||
|
const btnLabel = (window.f0ckI18n?.sidebar_show_full_comment) || 'show full comment';
|
||||||
|
contentEl.insertAdjacentHTML('beforeend',
|
||||||
|
`<span class="item-comment-truncated-notice"><button class="load-full-comment-btn" type="button">${btnLabel}</button></span>`
|
||||||
|
);
|
||||||
|
contentEl.dataset.raw = rawContent;
|
||||||
|
console.log('[ensureBtn] done, button in DOM:', !!contentEl.querySelector('.load-full-comment-btn'));
|
||||||
|
}
|
||||||
|
|
||||||
_finishSubmit(btn, originalHtml, parentId) {
|
_finishSubmit(btn, originalHtml, parentId) {
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.classList.remove('loading');
|
btn.classList.remove('loading');
|
||||||
@@ -2532,6 +2672,7 @@ class CommentSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Silently fetch fresh comment data from the server and update lastData
|
// Silently fetch fresh comment data from the server and update lastData
|
||||||
// without touching the DOM — used after optimistic inserts to stay in sync.
|
// without touching the DOM — used after optimistic inserts to stay in sync.
|
||||||
async _silentSync() {
|
async _silentSync() {
|
||||||
@@ -2874,7 +3015,7 @@ class CommentSystem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const trigger = document.createElement('button');
|
const trigger = document.createElement('button');
|
||||||
trigger.innerText = '☺';
|
trigger.innerHTML = '<i class="fa-regular fa-face-smile"></i>';
|
||||||
trigger.className = 'emoji-trigger';
|
trigger.className = 'emoji-trigger';
|
||||||
|
|
||||||
const actions = container.querySelector('.input-actions');
|
const actions = container.querySelector('.input-actions');
|
||||||
@@ -2905,15 +3046,16 @@ class CommentSystem {
|
|||||||
}
|
}
|
||||||
textarea.focus();
|
textarea.focus();
|
||||||
});
|
});
|
||||||
actions.prepend(trigger);
|
const submitBtn = actions.querySelector('.submit-comment');
|
||||||
actions.prepend(spoilerBtn);
|
actions.insertBefore(trigger, submitBtn);
|
||||||
|
actions.insertBefore(spoilerBtn, trigger);
|
||||||
if (this.isAdmin) {
|
if (this.isAdmin) {
|
||||||
const lockBtn = document.createElement('button');
|
const lockBtn = document.createElement('button');
|
||||||
lockBtn.id = 'lock-thread-btn';
|
lockBtn.id = 'lock-thread-btn';
|
||||||
lockBtn.title = this.isLocked ? 'Unlock Thread' : 'Lock Thread';
|
lockBtn.title = this.isLocked ? 'Unlock Thread' : 'Lock Thread';
|
||||||
lockBtn.innerHTML = this.isLocked ? this.icons.lock : this.icons.unlock;
|
lockBtn.innerHTML = this.isLocked ? this.icons.lock : this.icons.unlock;
|
||||||
lockBtn.className = 'admin-lock-btn';
|
lockBtn.className = 'admin-lock-btn';
|
||||||
actions.prepend(lockBtn);
|
actions.insertBefore(lockBtn, spoilerBtn);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create picker once and cache it
|
// Create picker once and cache it
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
* Protects against XSS by stripping disallowed tags and attributes.
|
* Protects against XSS by stripping disallowed tags and attributes.
|
||||||
*/
|
*/
|
||||||
class Sanitizer {
|
class Sanitizer {
|
||||||
// F-009 Security: Removed form elements (textarea, button, input, label, select, option)
|
// F-009 Security: Removed most form elements (textarea, input, label, select, option)
|
||||||
// to prevent phishing via user-generated content (comments, DMs, chat).
|
// to prevent phishing via user-generated content. 'button' is allowed because our own
|
||||||
|
// UI injects <button> elements (e.g. load-full-comment-btn) inside comment content;
|
||||||
|
// buttons cannot execute scripts and click handlers are class-based on a controlled container.
|
||||||
// Style attribute is kept for admin-authored MOTD content.
|
// Style attribute is kept for admin-authored MOTD content.
|
||||||
static ALLOWED_TAGS = ['span', 'img', 'a', 'br', 'b', 'i', 'strong', 'em', 'blockquote', 'pre', 'code', 'div', 'p', 'hr', 'ul', 'ol', 'li', 'svg', 'polyline', 'path', 'line', 'rect', 'circle', 'g', 'defs', 'symbol', 'use', 'polygon', 'ellipse', 'lineargradient', 'radialgradient', 'stop', 'clippath', 'mask', 'iframe', 'video', 'audio'];
|
static ALLOWED_TAGS = ['span', 'img', 'a', 'br', 'b', 'i', 'strong', 'em', 'blockquote', 'pre', 'code', 'div', 'p', 'hr', 'ul', 'ol', 'li', 'svg', 'polyline', 'path', 'line', 'rect', 'circle', 'g', 'defs', 'symbol', 'use', 'polygon', 'ellipse', 'lineargradient', 'radialgradient', 'stop', 'clippath', 'mask', 'iframe', 'video', 'audio', 'button'];
|
||||||
static ALLOWED_ATTRS = ['class', 'style', 'src', 'href', 'alt', 'title', 'target', 'width', 'height', 'placeholder', 'readonly', 'disabled', 'value', 'name', 'id', 'type', 'data-parent', 'data-id', 'data-username', 'xmlns', 'viewbox', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'points', 'x1', 'y1', 'x2', 'y2', 'd', 'transform', 'rx', 'ry', 'x', 'y', 'offset', 'stop-color', 'stop-opacity', 'fill-rule', 'clip-rule', 'cx', 'cy', 'r', 'fill-opacity', 'stroke-opacity', 'preserveaspectratio', 'vector-effect', 'pointer-events', 'allowfullscreen', 'frameborder', 'allow', 'referrerpolicy', 'rel', 'controls', 'loop', 'muted', 'playsinline', 'preload', 'tooltip', 'flow'];
|
static ALLOWED_ATTRS = ['class', 'style', 'src', 'href', 'alt', 'title', 'target', 'width', 'height', 'placeholder', 'readonly', 'disabled', 'value', 'name', 'id', 'type', 'data-parent', 'data-id', 'data-username', 'xmlns', 'viewbox', 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'points', 'x1', 'y1', 'x2', 'y2', 'd', 'transform', 'rx', 'ry', 'x', 'y', 'offset', 'stop-color', 'stop-opacity', 'fill-rule', 'clip-rule', 'cx', 'cy', 'r', 'fill-opacity', 'stroke-opacity', 'preserveaspectratio', 'vector-effect', 'pointer-events', 'allowfullscreen', 'frameborder', 'allow', 'referrerpolicy', 'rel', 'controls', 'loop', 'muted', 'playsinline', 'preload', 'tooltip', 'flow'];
|
||||||
static DISALLOWED_URL_SCHEMES = ['javascript:', 'data:', 'vbscript:'];
|
static DISALLOWED_URL_SCHEMES = ['javascript:', 'data:', 'vbscript:'];
|
||||||
|
|
||||||
|
|||||||
@@ -261,6 +261,11 @@ export default (router, tpl) => {
|
|||||||
return res.reply({ body: JSON.stringify({ success: false, message: "Empty comment" }) });
|
return res.reply({ body: JSON.stringify({ success: false, message: "Empty comment" }) });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxLen = cfg.main.comment_max_length;
|
||||||
|
if (maxLen !== null && maxLen !== undefined && content.length > maxLen) {
|
||||||
|
return res.reply({ code: 400, body: JSON.stringify({ success: false, message: `Comment too long (max ${maxLen} characters)` }) });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if thread is locked (admins and mods can still post)
|
// Check if thread is locked (admins and mods can still post)
|
||||||
if (!req.session.admin && !req.session.is_moderator) {
|
if (!req.session.admin && !req.session.is_moderator) {
|
||||||
@@ -393,12 +398,16 @@ export default (router, tpl) => {
|
|||||||
// Notify for live updates
|
// Notify for live updates
|
||||||
// Fetch the trigger-updated xd_score from the DB (trigger runs synchronously before we get here)
|
// Fetch the trigger-updated xd_score from the DB (trigger runs synchronously before we get here)
|
||||||
const [xdRow] = await db`SELECT xd_score FROM items WHERE id = ${item_id}`;
|
const [xdRow] = await db`SELECT xd_score FROM items WHERE id = ${item_id}`;
|
||||||
|
// Truncate body to 500 chars: PostgreSQL NOTIFY has an 8000-byte hard limit.
|
||||||
|
// Large comments would silently drop the notification. The client fetches
|
||||||
|
// the full content via _silentSync; the NOTIFY only needs to trigger the update.
|
||||||
|
const notifyBody = content.length > 500 ? content.substring(0, 500) + '…' : content;
|
||||||
const livePayload = {
|
const livePayload = {
|
||||||
type: 'comment',
|
type: 'comment',
|
||||||
id: commentId,
|
id: commentId,
|
||||||
item_id: item_id,
|
item_id: item_id,
|
||||||
parent_id: parent_id || null,
|
parent_id: parent_id || null,
|
||||||
body: content,
|
body: notifyBody,
|
||||||
username: req.session.user,
|
username: req.session.user,
|
||||||
user_id: req.session.id,
|
user_id: req.session.id,
|
||||||
avatar: req.session.avatar,
|
avatar: req.session.avatar,
|
||||||
@@ -418,7 +427,7 @@ export default (router, tpl) => {
|
|||||||
user_id: req.session.id,
|
user_id: req.session.id,
|
||||||
item_id: item_id,
|
item_id: item_id,
|
||||||
type: 'comment',
|
type: 'comment',
|
||||||
body: content,
|
body: notifyBody,
|
||||||
id: commentId
|
id: commentId
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ process.on('uncaughtException', err => {
|
|||||||
allow_language_change: cfg.websrv.allow_language_change !== false,
|
allow_language_change: cfg.websrv.allow_language_change !== false,
|
||||||
enable_xd_score: !!cfg.websrv.enable_xd_score,
|
enable_xd_score: !!cfg.websrv.enable_xd_score,
|
||||||
enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
|
enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
|
||||||
|
comment_max_length: cfg.main.comment_max_length ?? null,
|
||||||
enable_swf: !!cfg.websrv.enable_swf,
|
enable_swf: !!cfg.websrv.enable_swf,
|
||||||
enable_danmaku: cfg.websrv.enable_danmaku !== false,
|
enable_danmaku: cfg.websrv.enable_danmaku !== false,
|
||||||
enable_global_chat: !!cfg.websrv.enable_global_chat,
|
enable_global_chat: !!cfg.websrv.enable_global_chat,
|
||||||
|
|||||||
@@ -138,7 +138,7 @@
|
|||||||
@endif
|
@endif
|
||||||
@if(private_society && !session)
|
@if(private_society && !session)
|
||||||
<script>
|
<script>
|
||||||
window.f0ckSession = { logged_in: false, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, development: @if(development) true @else false @endif };
|
window.f0ckSession = { logged_in: false, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif };
|
||||||
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
|
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
|
||||||
(() => {
|
(() => {
|
||||||
const loginModal = document.getElementById('login-modal');
|
const loginModal = document.getElementById('login-modal');
|
||||||
@@ -405,6 +405,7 @@
|
|||||||
receive_user_notifications: @if(session)@if(session.receive_user_notifications !== false) true @else false @endif@else true @endif,
|
receive_user_notifications: @if(session)@if(session.receive_user_notifications !== false) true @else false @endif@else true @endif,
|
||||||
do_not_disturb: @if(session && session.do_not_disturb) true @else false @endif,
|
do_not_disturb: @if(session && session.do_not_disturb) true @else false @endif,
|
||||||
comment_display_mode: {{ comment_display_mode }},
|
comment_display_mode: {{ comment_display_mode }},
|
||||||
|
comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }},
|
||||||
development: @if(development) true @else false @endif
|
development: @if(development) true @else false @endif
|
||||||
};
|
};
|
||||||
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
|
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
|
||||||
|
|||||||
Reference in New Issue
Block a user