ability to change permissions for any markdown, this can be extended nicely in future close #303

merge-requests/218/head
Thomas Lynch 3 years ago
parent 92806c4578
commit b474134ed1
  1. 22
      configs/template.js.example
  2. 15
      controllers/forms/globalsettings.js
  3. 4
      helpers/paramconverter.js
  4. 2
      helpers/posting/diceroll.js
  5. 39
      helpers/posting/linkmatch.js
  6. 68
      helpers/posting/markdown.js
  7. 4
      helpers/posting/message.js
  8. 11
      migrations/0.0.23.js
  9. 2
      models/forms/addcustompage.js
  10. 2
      models/forms/addnews.js
  11. 2
      models/forms/changeboardsettings.js
  12. 19
      models/forms/changeglobalsettings.js
  13. 2
      models/forms/editnews.js
  14. 2
      models/forms/editpost.js
  15. 2
      models/forms/makepost.js
  16. 2
      package.json
  17. 9
      views/pages/globalmanagesettings.pug

@ -80,6 +80,25 @@ module.exports = {
// permission level required to delete boards
deleteBoardPermLevel: 2,
//todo: migrate the above 2 into this
permLevels: {
markdown: {
pink: 4,
green: 4,
bold: 4,
underline: 4,
strike: 4,
italic: 4,
title: 4,
spoiler: 4,
mono: 4,
code: 4,
link: 4,
detected: 4,
dice: 4,
},
},
/* delete files immediately rather than pruning later. usually disabled to prevent re-thumbnailing and processing commonly
uploaded files, but deleting immediately is better if you are concerned about "deleted" content not being immediately removed */
pruneImmediately: true,
@ -205,7 +224,8 @@ module.exports = {
'html',
'json',
'golang',
'rust'
'rust',
'aa',
],
//threshold below which auto language is ignored

@ -53,7 +53,20 @@ module.exports = async (req, res, next) => {
{ result: numberBody(req.body.block_bypass_expire_after_uses), expected: true, error: 'Block bypass expire after uses must be a number > 0' },
{ result: numberBody(req.body.block_bypass_expire_after_time), expected: true, error: 'Invalid block bypass expire after time' },
{ result: numberBody(req.body.ip_hash_perm_level, -1), expected: true, error: 'Invalid ip hash perm level' },
{ result: numberBody(req.body.delete_board_perm_level), expected: true, error: 'Invalid delete board perm level' },
{ result: numberBody(req.body.delete_board_perm_level, 0, 4), expected: true, error: 'Invalid delete board perm level' },
{ result: numberBody(req.body.perm_levels_markdown_green, 0, 4), expected: true, error: 'Invalid greentext markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_pink, 0, 4), expected: true, error: 'Invalid pinktext markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_title, 0, 4), expected: true, error: 'Invalid title markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_bold, 0, 4), expected: true, error: 'Invalid bold markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_underline, 0, 4), expected: true, error: 'Invalid underline markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_strike, 0, 4), expected: true, error: 'Invalid strike markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_italic, 0, 4), expected: true, error: 'Invalid italicmarkdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_mono, 0, 4), expected: true, error: 'Invalid mono markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_code, 0, 4), expected: true, error: 'Invalid code block markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_spoiler, 0, 4), expected: true, error: 'Invalid spoiler markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_detected, 0, 4), expected: true, error: 'Invalid detected markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_link, 0, 4), expected: true, error: 'Invalid link markdown perm level' },
{ result: numberBody(req.body.perm_levels_markdown_dice, 0, 4), expected: true, error: 'Invalid dice markdown perm level' },
{ result: numberBody(req.body.rate_limit_cost_captcha, 1, 100), expected: true, error: 'Rate limit cost captcha must be a number from 1-100' },
{ result: numberBody(req.body.rate_limit_cost_board_settings, 1, 100), expected: true, error: 'Rate limit cost board settings must be a number from 1-100' },
{ result: numberBody(req.body.rate_limit_cost_edit_post, 1, 100), expected: true, error: 'Rate limit cost edit post must be a number from 1-100' },

@ -27,7 +27,9 @@ const { ObjectId } = require(__dirname+'/../db/db.js')
'board_defaults_tph_trigger_action', 'board_defaults_pph_trigger_action', 'board_defaults_captcha_reset', 'board_defaults_lock_reset', 'board_defaults_thread_limit',
'board_defaults_reply_limit', 'board_defaults_bump_limit', 'board_defaults_max_files', 'board_defaults_min_thread_message_length',
'board_defaults_min_reply_message_length', 'board_defaults_max_thread_message_length', 'board_defaults_max_reply_message_length', 'board_defaults_filter_mode',
] //convert these to numbers before they hit our routes
'perm_levels_markdown_pink', 'perm_levels_markdown_green', 'perm_levels_markdown_bold', 'perm_levels_markdown_underline', 'perm_levels_markdown_strike',
'perm_levels_markdown_italic', 'perm_levels_markdown_title', 'perm_levels_markdown_spoiler', 'perm_levels_markdown_mono', 'perm_levels_markdown_code',
'perm_levels_markdown_link', 'perm_levels_markdown_detected', 'perm_levels_markdown_dice'] //convert these to numbers before they hit our routes
, timeFields = ['ban_duration', 'board_defaults_filter_ban_duration', 'default_ban_duration', 'block_bypass_expire_after_time', 'dnsbl_cache_time']
, timeFieldRegex = /^(?<YEAR>[\d]+y)?(?<MONTH>[\d]+mo)?(?<WEEK>[\d]+w)?(?<DAY>[\d]+d)?(?<HOUR>[\d]+h)?(?<MINUTE>[\d]+m)?(?<SECOND>[\d]+s)?$/
, timeUtils = require(__dirname+'/timeutils.js')

@ -30,7 +30,7 @@ module.exports = {
return `${matchWithoutValue}=${sum}${value ? value : ''}`;
},
markdown: (match, numdice, numsides, operator, modifier, value) => {
markdown: (permLevel, match, numdice, numsides, operator, modifier, value) => {
numdice = parseInt(numdice);
numsides = parseInt(numsides);
value = parseInt(value);

@ -2,31 +2,32 @@
const parenPairRegex = /\((?:[^)(]+|\((?:[^)(]+|\([^)(]\))*\))*\)/g
module.exports = (match, p1, p2, p3, offset, string, groups) => {
let url, label, urlOnly, excess = '';
if (!groups) {
const parensPairs = match.match(parenPairRegex);
let trimmedMatch;
module.exports = (permLevel, match, p1, p2, p3, offset, string, groups) => {
let { url, label, urlOnly } = groups
, excess = '';
url = url || urlOnly;
if (urlOnly) {
const parensPairs = url.match(parenPairRegex);
let trimmedMatch = url;
//naive solution to conflict with detected markdown
if (parensPairs) {
const lastMatch = parensPairs[parensPairs.length-1];
const lastIndex = match.lastIndexOf(lastMatch);
trimmedMatch = match.substring(0, lastIndex+lastMatch.length);
excess = match.substring(lastIndex+lastMatch.length);
} else if (match.indexOf(')') !== -1){
trimmedMatch = match.substring(0, match.indexOf(')'));
excess = match.substring(match.indexOf(')'));
} else {
trimmedMatch = match;
const lastIndex = url.lastIndexOf(lastMatch);
trimmedMatch = url.substring(0, lastIndex+lastMatch.length);
excess = url.substring(lastIndex+lastMatch.length);
} else if (url.indexOf(')') !== -1){
trimmedMatch = url.substring(0, url.indexOf(')'));
excess = url.substring(url.indexOf(')'));
}
trimmedMatch = trimmedMatch
.replace(/\(/g, '%28')
.replace(/\)/g, '%29');
url = trimmedMatch;
} else {
({ url, label, urlOnly } = groups);
url = url || urlOnly;
}
if (permLevel >= 4) {
label = url
.replace(/\(/g, '&lpar;')
.replace(/\)/g, '&rpar;');
}
url = url.replace(/\(/g, '%28')
.replace(/\)/g, '%29');
//TODO: if the link domain is one of the site domains, remove the domain and make it an absolute link, for users on different domains or anonymizers
return `<a rel='nofollow' referrerpolicy='same-origin' target='_blank' href='${url}'>${label || url}</a>${excess}`;
};

@ -10,31 +10,41 @@ const greentextRegex = /^&gt;((?!&gt;\d+|&gt;&gt;&#x2F;\w+(&#x2F;\d*)?|&gt;&gt;#
, italicRegex = /\*\*(.+?)\*\*/gm
, spoilerRegex = /\|\|([\s\S]+?)\|\|/gm
, detectedRegex = /(\(\(\(.+?\)\)\))/gm
, linkRegex = /(https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^]+)/g
, aLinkRegex = /\[(?<label>[^\[][^\]]*?)\]\((?<url>https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^)]+)\)|(?<urlOnly>https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^]+)/g
, linkRegex = /\[(?<label>[^\[][^\]]*?)\]\((?<url>https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^)]+)\)|(?<urlOnly>https?\:&#x2F;&#x2F;[^\s<>\[\]{}|\\^]+)/g
, codeRegex = /(?:(?<language>[a-z+]{1,10})\r?\n)?(?<code>[\s\S]+)/i
, includeSplitRegex = /(\[code\][\s\S]+?\[\/code\])/gm
, splitRegex = /\[code\]([\s\S]+?)\[\/code\]/gm
, trimNewlineRegex = /^\s*(\r?\n)*|(\r?\n)*$/g
, escape = require(__dirname+'/escape.js')
, { highlight, highlightAuto } = require('highlight.js')
, { addCallback } = require(__dirname+'/../../redis.js')
, config = require(__dirname+'/../../config.js')
, diceroll = require(__dirname+'/diceroll.js')
, linkmatch = require(__dirname+'/linkmatch.js')
, replacements = [
{ regex: pinktextRegex, cb: (match, pinktext) => `<span class='pinktext'>&lt;${pinktext}</span>` },
{ regex: greentextRegex, cb: (match, greentext) => `<span class='greentext'>&gt;${greentext}</span>` },
{ regex: boldRegex, cb: (match, bold) => `<span class='bold'>${bold}</span>` },
{ regex: underlineRegex, cb: (match, underline) => `<span class='underline'>${underline}</span>` },
{ regex: strikeRegex, cb: (match, strike) => `<span class='strike'>${strike}</span>` },
{ regex: titleRegex, cb: (match, title) => `<span class='title'>${title}</span>` },
{ regex: italicRegex, cb: (match, italic) => `<span class='em'>${italic}</span>` },
{ regex: spoilerRegex, cb: (match, spoiler) => `<span class='spoiler'>${spoiler}</span>` },
{ regex: monoRegex, cb: (match, mono) => `<span class='mono'>${mono}</span>` },
{ regex: linkRegex, aRegex: aLinkRegex, cb: linkmatch },
{ regex: detectedRegex, cb: (match, detected) => `<span class='detected'>${detected}</span>` },
{ regex: diceroll.regexMarkdown, cb: diceroll.markdown },
, linkmatch = require(__dirname+'/linkmatch.js');
let replacements = []
, markdownPermLevels;
const updateMarkdownPerms = () => {
markdownPermLevels = config.get.permLevels.markdown;
replacements = [
{ permLevel: markdownPermLevels.pink, regex: pinktextRegex, cb: (permLevel, match, pinktext) => `<span class='pinktext'>&lt;${pinktext}</span>` },
{ permLevel: markdownPermLevels.green, regex: greentextRegex, cb: (permLevel, match, greentext) => `<span class='greentext'>&gt;${greentext}</span>` },
{ permLevel: markdownPermLevels.bold, regex: boldRegex, cb: (permLevel, match, bold) => `<span class='bold'>${bold}</span>` },
{ permLevel: markdownPermLevels.underline, regex: underlineRegex, cb: (permLevel, match, underline) => `<span class='underline'>${underline}</span>` },
{ permLevel: markdownPermLevels.strike, regex: strikeRegex, cb: (permLevel, match, strike) => `<span class='strike'>${strike}</span>` },
{ permLevel: markdownPermLevels.title, regex: titleRegex, cb: (permLevel, match, title) => `<span class='title'>${title}</span>` },
{ permLevel: markdownPermLevels.italic, regex: italicRegex, cb: (permLevel, match, italic) => `<span class='em'>${italic}</span>` },
{ permLevel: markdownPermLevels.spoiler, regex: spoilerRegex, cb: (permLevel, match, spoiler) => `<span class='spoiler'>${spoiler}</span>` },
{ permLevel: markdownPermLevels.mono, regex: monoRegex, cb: (permLevel, match, mono) => `<span class='mono'>${mono}</span>` },
{ permLevel: markdownPermLevels.link, regex: linkRegex, cb: linkmatch },
{ permLevel: markdownPermLevels.detected, regex: detectedRegex, cb: (permLevel, match, detected) => `<span class='detected'>${detected}</span>` },
{ permLevel: markdownPermLevels.dice, regex: diceroll.regexMarkdown, cb: diceroll.markdown },
];
};
updateMarkdownPerms();
addCallback('config', updateMarkdownPerms);
module.exports = {
@ -53,7 +63,7 @@ module.exports = {
return chunks.join('');
},
markdown: (text, allowAdvanced=false) => {
markdown: (text, permLevel=4) => {
const chunks = text.split(splitRegex);
const { highlightOptions } = config.get;
for (let i = 0; i < chunks.length; i++) {
@ -61,8 +71,8 @@ module.exports = {
if (i % 2 === 0) {
const escaped = escape(chunks[i]);
const newlineFix = escaped.replace(/^\r?\n/,''); //fix ending newline because of codeblock
chunks[i] = module.exports.processRegularChunk(newlineFix, allowAdvanced);
} else {
chunks[i] = module.exports.processRegularChunk(newlineFix, permLevel);
} else if (permLevel <= markdownPermLevels.code){
chunks[i] = module.exports.processCodeChunk(chunks[i], highlightOptions);
}
}
@ -82,19 +92,21 @@ module.exports = {
return `<span class='code hljs'><small>possible language: ${language}, relevance: ${relevance}</small>\n${value}</span>`;
}
} else if (lang !== 'plain' && highlightOptions.languageSubset.includes(lang)) {
const { value } = highlight(lang, trimFix, true);
return `<span class='code hljs'><small>language: ${lang}</small>\n${value}</span>`;
} else if (lang === 'aa') {
return `<span class='aa'>${escape(matches.groups.code)}</span>`;
if (lang === 'aa') {
return `<span class='aa'>${escape(matches.groups.code)}</span>`;
} else {
const { value } = highlight(lang, trimFix, true);
return `<span class='code hljs'><small>language: ${lang}</small>\n${value}</span>`;
}
}
return `<span class='code'>${escape(trimFix)}</span>`;
},
processRegularChunk: (text, allowAdvanced) => {
for (let i = 0; i < replacements.length; i++) {
//if allowAdvanced is true, use aRegex if available
const replaceRegex = allowAdvanced === true && replacements[i].aRegex || replacements[i].regex;
text = text.replace(replaceRegex, replacements[i].cb);
processRegularChunk: (text, permLevel) => {
//so theoretically now with some more options in the global manage page you can set permissions or enable/disable markdowns
const allowedReplacements = replacements.filter(r => r.permLevel >= permLevel);
for (let i = 0; i < allowedReplacements.length; i++) {
text = text.replace(allowedReplacements[i].regex, allowedReplacements[i].cb.bind(null, permLevel));
}
return text;
},

@ -5,7 +5,7 @@ const quoteHandler = require(__dirname+'/quotes.js')
, sanitizeOptions = require(__dirname+'/sanitizeoptions.js')
, sanitize = require('sanitize-html');
module.exports = async (inputMessage, boardName, threadId=null, allowAdvanced=false) => {
module.exports = async (inputMessage, boardName, threadId=null, permLevel=4) => {
let message = inputMessage;
let quotes = [];
@ -13,7 +13,7 @@ module.exports = async (inputMessage, boardName, threadId=null, allowAdvanced=fa
//markdown a post, link the quotes, sanitize and return message and quote arrays
if (message && message.length > 0) {
message = markdown(message, allowAdvanced);
message = markdown(message, permLevel);
const { quotedMessage, threadQuotes, crossQuotes } = await quoteHandler.process(boardName, message, threadId);
message = quotedMessage;
quotes = threadQuotes;

@ -0,0 +1,11 @@
'use strict';
const fs = require('fs-extra');
module.exports = async(db, redis) => {
console.log('add markdown permissions');
const template = require(__dirname+'/../configs/template.js.example');
const settings = await redis.get('globalsettings');
const newSettings = { ...settings, permLevels: template.permLevels };
redis.set('globalsettings', newSettings);
};

@ -9,7 +9,7 @@ const { CustomPages } = require(__dirname+'/../../db/')
module.exports = async (req, res, next) => {
const message = prepareMarkdown(req.body.message, false);
const { message: markdownMessage } = await messageHandler(message, null, null, true);
const { message: markdownMessage } = await messageHandler(message, null, null, res.locals.permLevel);
const post = {
'board': req.params.board,

@ -9,7 +9,7 @@ const { News } = require(__dirname+'/../../db/')
module.exports = async (req, res, next) => {
const message = prepareMarkdown(req.body.message, false);
const { message: markdownNews } = await messageHandler(message, null, null, true);
const { message: markdownNews } = await messageHandler(message, null, null, res.locals.permLevel);
const post = {
'title': req.body.title,

@ -26,7 +26,7 @@ module.exports = async (req, res, next) => {
const announcement = req.body.announcement === null ? null : prepareMarkdown(req.body.announcement, false);
let markdownAnnouncement = oldSettings.announcement.markdown;
if (announcement !== oldSettings.announcement.raw) {
({ message: markdownAnnouncement } = await messageHandler(announcement, req.params.board, null, true))
({ message: markdownAnnouncement } = await messageHandler(announcement, req.params.board, null, res.locals.permLevel))
}
let moderators = req.body.moderators != null ? req.body.moderators.split(/\r?\n/).filter(n => n && !(n == res.locals.board.owner)).slice(0,10) : [];

@ -19,7 +19,7 @@ module.exports = async (req, res, next) => {
const announcement = req.body.global_announcement === null ? null : prepareMarkdown(req.body.global_announcement, false);
let markdownAnnouncement = oldSettings.globalAnnouncement.markdown;
if (announcement !== oldSettings.globalAnnouncement.raw) {
({ message: markdownAnnouncement } = await messageHandler(announcement, null, null, true))
({ message: markdownAnnouncement } = await messageHandler(announcement, null, null, res.locals.permLevel))
}
const newSettings = {
@ -75,6 +75,23 @@ module.exports = async (req, res, next) => {
},
ipHashPermLevel: numberSetting(req.body.ip_hash_perm_level, oldSettings.ipHashPermLevel),
deleteBoardPermLevel: numberSetting(req.body.delete_board_perm_level, oldSettings.deleteBoardPermLevel),
permLevels: {
markdown: {
green: numberSetting(req.body.perm_levels_markdown_green, oldSettings.permLevels.markdown.green),
pink: numberSetting(req.body.perm_levels_markdown_pink, oldSettings.permLevels.markdown.pink),
title: numberSetting(req.body.perm_levels_markdown_title, oldSettings.permLevels.markdown.title),
bold: numberSetting(req.body.perm_levels_markdown_bold, oldSettings.permLevels.markdown.bold),
underline: numberSetting(req.body.perm_levels_markdown_underline, oldSettings.permLevels.markdown.underline),
strike: numberSetting(req.body.perm_levels_markdown_strike, oldSettings.permLevels.markdown.strike),
italic: numberSetting(req.body.perm_levels_markdown_italic, oldSettings.permLevels.markdown.italic),
mono: numberSetting(req.body.perm_levels_markdown_mono, oldSettings.permLevels.markdown.mono),
code: numberSetting(req.body.perm_levels_markdown_code, oldSettings.permLevels.markdown.code),
spoiler: numberSetting(req.body.perm_levels_markdown_spoiler, oldSettings.permLevels.markdown.spoiler),
detected: numberSetting(req.body.perm_levels_markdown_detected, oldSettings.permLevels.markdown.detected),
link: numberSetting(req.body.perm_levels_markdown_link, oldSettings.permLevels.markdown.link),
dice: numberSetting(req.body.perm_levels_markdown_dice, oldSettings.permLevels.markdown.dice),
},
},
pruneImmediately: booleanSetting(req.body.prune_immediately, oldSettings.pruneImmediately),
hashImages: booleanSetting(req.body.hash_images, oldSettings.hashImages),
rateLimitCost: {

@ -9,7 +9,7 @@ const { News } = require(__dirname+'/../../db/')
module.exports = async (req, res, next) => {
const message = prepareMarkdown(req.body.message, false);
const { message: markdownNews } = await messageHandler(message, null, null, true);
const { message: markdownNews } = await messageHandler(message, null, null, res.locals.permLevel);
const updated = await News.updateOne(req.body.news_id, req.body.title, message, markdownNews).then(r => r.matchedCount);

@ -85,7 +85,7 @@ todo: handle some more situations
board.settings, board.owner, res.locals.user ? res.locals.user.username : null);
//new message and quotes
const nomarkup = prepareMarkdown(req.body.message, false);
const { message, quotes, crossquotes } = await messageHandler(nomarkup, req.body.board, post.thread, true);
const { message, quotes, crossquotes } = await messageHandler(nomarkup, req.body.board, post.thread, res.locals.permLevel);
//todo: email and subject (probably dont need any transformation since staff bypass limits on forceanon, and it doesnt have to account for sage/etc
//intersection/difference of quotes sets for linking and unlinking

@ -413,7 +413,7 @@ ${res.locals.numFiles > 0 ? req.files.file.map(f => f.name+'|'+(f.phash || '')).
res.locals.board.settings, res.locals.board.owner, res.locals.user ? res.locals.user.username : null);
//get message, quotes and crossquote array
const nomarkup = prepareMarkdown(req.body.message, true);
const { message, quotes, crossquotes } = await messageHandler(nomarkup, req.params.board, req.body.thread, res.locals.permLevel < 4);
const { message, quotes, crossquotes } = await messageHandler(nomarkup, req.params.board, req.body.thread, res.locals.permLevel);
//build post data for db. for some reason all the property names are lower case :^)
const data = {

@ -1,6 +1,6 @@
{
"name": "jschan",
"version": "0.0.22",
"version": "0.0.23",
"description": "",
"main": "server.js",
"dependencies": {

@ -125,6 +125,15 @@ block content
.label Allow User Board Creation
label.postform-style.ph-5
input(type='checkbox', name='enable_user_board_creation' value='true' checked=settings.enableUserBoardCreation)
each markdownName in Object.keys(settings.permLevels.markdown)
.row
.label #{markdownName} Markdown
select(name=`perm_levels_markdown_${markdownName}`)
option(value='0', selected=settings.permLevels.markdown[markdownName] === 0) Admin
option(value='1', selected=settings.permLevels.markdown[markdownName] === 1) Global Staff
option(value='2', selected=settings.permLevels.markdown[markdownName] === 2) Board Owner
option(value='3', selected=settings.permLevels.markdown[markdownName] === 3) Board Mod
option(value='4', selected=settings.permLevels.markdown[markdownName] === 4) Everybody
.row
.label Default Ban Duration
input(type='number', name='default_ban_duration', placeholder='e.g. 1w', value=settings.defaultBanDuration)

Loading…
Cancel
Save