Merge branch 'feature/396-localisation' into develop

indiachan-spamvector
Thomas Lynch 1 year ago
commit 5a9a264f4d
Signed by: fatchan
GPG Key ID: A7E5E8B7E11EE92D
  1. 24
      CHANGELOG.md
  2. 1
      README.md
  3. 3
      configs/template.js.example
  4. 43
      controllers/forms.js
  5. 58
      controllers/forms/actions.js
  6. 10
      controllers/forms/addassets.js
  7. 22
      controllers/forms/addcustompage.js
  8. 10
      controllers/forms/addflags.js
  9. 12
      controllers/forms/addnews.js
  10. 14
      controllers/forms/addstaff.js
  11. 18
      controllers/forms/appeal.js
  12. 85
      controllers/forms/boardsettings.js
  13. 24
      controllers/forms/changepassword.js
  14. 18
      controllers/forms/create.js
  15. 12
      controllers/forms/deleteaccount.js
  16. 6
      controllers/forms/deleteaccounts.js
  17. 10
      controllers/forms/deleteassets.js
  18. 10
      controllers/forms/deletebanners.js
  19. 18
      controllers/forms/deleteboard.js
  20. 6
      controllers/forms/deletecustompage.js
  21. 10
      controllers/forms/deleteflags.js
  22. 6
      controllers/forms/deletenews.js
  23. 12
      controllers/forms/deletesessions.js
  24. 12
      controllers/forms/deletestaff.js
  25. 18
      controllers/forms/editaccount.js
  26. 28
      controllers/forms/editbans.js
  27. 22
      controllers/forms/editcustompage.js
  28. 14
      controllers/forms/editnews.js
  29. 24
      controllers/forms/editpost.js
  30. 8
      controllers/forms/editrole.js
  31. 16
      controllers/forms/editstaff.js
  32. 34
      controllers/forms/globalactions.js
  33. 276
      controllers/forms/globalsettings.js
  34. 14
      controllers/forms/login.js
  35. 34
      controllers/forms/makepost.js
  36. 22
      controllers/forms/register.js
  37. 12
      controllers/forms/resign.js
  38. 14
      controllers/forms/transfer.js
  39. 10
      controllers/forms/twofactor.js
  40. 10
      controllers/forms/uploadbanners.js
  41. 50
      controllers/pages.js
  42. 6
      gulp/res/css/style.css
  43. 8
      gulp/res/js/captcha.js
  44. 4
      gulp/res/js/filters.js
  45. 6
      gulp/res/js/forms.js
  46. 4
      gulp/res/js/hideimages.js
  47. 24
      gulp/res/js/i18n.js
  48. 67
      gulp/res/js/live.js
  49. 3
      gulp/res/js/ptchina-playlist.js
  50. 14441
      gulp/res/js/tegaki.js
  51. 11
      gulp/res/js/threadstat.js
  52. 23
      gulp/res/js/time.js
  53. 4
      gulp/res/js/yous.js
  54. 71
      gulpfile.js
  55. 19
      lib/build/render.js
  56. 10
      lib/build/tasks.js
  57. 2
      lib/captcha/captcha.js
  58. 20
      lib/converter/timeutils.js
  59. 5
      lib/converter/timeutils.test.js
  60. 9
      lib/file/video/videothumbnail.js
  61. 30
      lib/input/modlogactions.js
  62. 19
      lib/locale/locale.js
  63. 17
      lib/middleware/captcha/blockbypass.js
  64. 5
      lib/middleware/captcha/verify.js
  65. 19
      lib/middleware/file/filemiddlewares.js
  66. 10
      lib/middleware/input/paramconverter.js
  67. 5
      lib/middleware/ip/dnsbl.js
  68. 3
      lib/middleware/ip/geoip.js
  69. 6
      lib/middleware/ip/processip.js
  70. 33
      lib/middleware/locale/locale.js
  71. 5
      lib/middleware/misc/referrercheck.js
  72. 15
      lib/middleware/permission/haspermsmiddleware.js
  73. 44
      lib/misc/countries.js
  74. 8
      lib/misc/dotwofactor.js
  75. 4
      lib/misc/dynamic.js
  76. 6
      lib/post/deletequotes.js
  77. 11
      lib/post/filteractions.js
  78. 6
      lib/post/markdown/escape.test.js
  79. 7
      lib/post/markdown/handler/diceroll.js
  80. 21
      lib/post/markdown/handler/diceroll.test.js
  81. 14
      lib/post/markdown/markdown.js
  82. 19
      lib/post/name.js
  83. 5
      lib/post/name.test.js
  84. 1383
      locales/en-GB.json
  85. 1383
      locales/pt-PT.json
  86. 1383
      locales/ru-RU.json
  87. 34
      migrations/1.0.0.js
  88. 69
      models/forms/actionhandler.js
  89. 17
      models/forms/addassets.js
  90. 5
      models/forms/addcustompage.js
  91. 13
      models/forms/addflags.js
  92. 5
      models/forms/addnews.js
  93. 6
      models/forms/addstaff.js
  94. 5
      models/forms/banposter.js
  95. 5
      models/forms/blockbypass.js
  96. 8
      models/forms/bumplockposts.js
  97. 16
      models/forms/changeboardsettings.js
  98. 7
      models/forms/changeglobalsettings.js
  99. 17
      models/forms/changepassword.js
  100. 9
      models/forms/create.js
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,3 +1,27 @@
### 1.0.0
Version 1.0.0 is here. jschan is mature enough and there's no need to go to version 0.999, but this does not mean it is perfect or complete!
I want to keep improving jschan and there are plenty of new features and changes planned for versions 2, 3, and beyond.
Thank you so much to all the jschan site admins, users & contributors for your help along the way. Your contributions have been invaluable to making this software great.
Special shoutout to l29utp0 & loynet (ptchan.org), Homicide (94chan.org) and some_random_guy (trashchan.xyz).
-Tom
Now, back to the program. Here are the changes for 1.0.0, with one especially notable feature:
- Multiple language support. jschan now supports language packs. There is a global and board-level language setting which completely translates the interface to another language. No javascript required.
- An effort has been made to translate everything, but given there is almost 4 years of code, some things may have slipped through the cracks. If something isn't translated, please report it on gitgud.
- 1.0.0 includes three* language packs: English (en-GB) and Portuguese (pt-PT) (Russian machine translation is also included, but may not be accurate).
- Huge credit to the ptchan.org admins for providing the Portuguese translation.
- Contributions for new language packs or improvements to existing ones are very welcome! Reach out via email/IRC to discuss imbursement for contributing language packs.
- Note: "global" pages e.g. overboards will adhere to global language setting. In a future iteration, these will be updated to (optionally, with global setting) support adhering to users browser language header for better UX going between a non-global language board and global pages.
- Improve the css and markup to only show the appropriate wording e.g "tap" or "click" in tooltips depending if you are on mobile/desktop.
- Notify the user when making a playlist from a thread if there were no files, rather than just silently logging.
- Improve the installation process to fix a potential issue with the database connection settings.
- Customflags will now show correctly when editing a post on a board with custom flags enabled.
- Security improvement to the 2FA validation flow during login.
- Log a few more errors related to hcaptcha/recaptcha, for debugging purposes. (already caught and returned in a friendly manner)
- Remove showing language and relevance data when auto detecting highlighted code block language
- More minor bugfixes to permissions pages displays.
### 0.11.4
- Bugfix for the message stating how many banners were deleted when deleting banners.
- Add an option to limit the total resolution of an image/video (width*height).

@ -9,6 +9,7 @@ API documentation: [fatchan/jschan-docs](https://gitgud.io/fatchan/jschan-docs/)
Join the IRC: [open in client](ircs://irc.fatpeople.lol:6697/general) OR: [webchat](https://irc.fatpeople.lol/#general)
## Features
- [x] Multiple language support (currently English & Portuguese)
- [x] User created boards ala [infinity](https://github.com/ctrlcctrlv/infinity)
- [x] Multiple files per post
- [x] Antispam/Anti-flood & DNSBL

@ -24,6 +24,8 @@ module.exports = {
url: ''
},
language: 'en-GB',
filters: [],
strictFiltering: false,
filterMode: 0,
@ -368,6 +370,7 @@ module.exports = {
//default board settings when a board is created
boardDefaults: {
language: 'en-GB',
theme: 'yotsuba-b',
codeTheme: 'ir-black',
sfw: false, //safe for work board

@ -20,6 +20,7 @@ const express = require('express')
, dnsblCheck = require(__dirname+'/../lib/middleware/ip/dnsbl.js')
, blockBypass = require(__dirname+'/../lib/middleware/captcha/blockbypass.js')
, fileMiddlewares = require(__dirname+'/../lib/middleware/file/filemiddlewares.js')
, { setBoardLanguage } = require(__dirname+'/../lib/middleware/locale/locale.js')
//controllers
, { deleteBoardController, editBansController, appealController, globalActionController, twofactorController,
actionController, addCustomPageController, deleteCustomPageController, addNewsController,
@ -31,14 +32,14 @@ const express = require('express')
editRoleController, newCaptchaForm, blockBypassForm, logoutForm, deleteSessionsController } = require(__dirname+'/forms/index.js');
//make new post
router.post('/board/:board/post', geoIp, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, fileMiddlewares.posts,
router.post('/board/:board/post', geoIp, processIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, calcPerms, banCheck, fileMiddlewares.posts,
makePostController.paramConverter, verifyCaptcha, numFiles, blockBypass.middleware, dnsblCheck, imageHashes, makePostController.controller);
router.post('/board/:board/modpost', geoIp, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, isLoggedIn,
router.post('/board/:board/modpost', geoIp, processIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, calcPerms, banCheck, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), fileMiddlewares.posts, makePostController.paramConverter, csrf, numFiles, blockBypass.middleware, dnsblCheck, imageHashes, makePostController.controller); //mod post has token instead of captcha
//post actions
router.post('/board/:board/actions', geoIp, processIp, useSession, sessionRefresh, Boards.exists, calcPerms, banCheck, actionController.paramConverter, verifyCaptcha, actionController.controller); //public, with captcha
router.post('/board/:board/modactions', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, calcPerms, banCheck, isLoggedIn,
router.post('/board/:board/actions', geoIp, processIp, useSession, sessionRefresh, Boards.exists, setBoardLanguage, calcPerms, banCheck, actionController.paramConverter, verifyCaptcha, actionController.controller); //public, with captcha
router.post('/board/:board/modactions', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, banCheck, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), actionController.paramConverter, actionController.controller); //board manage page
router.post('/global/actions', geoIp, processIp, useSession, sessionRefresh, csrf, calcPerms, isLoggedIn,
@ -47,43 +48,43 @@ router.post('/global/actions', geoIp, processIp, useSession, sessionRefresh, csr
//appeal ban
router.post('/appeal', geoIp, processIp, useSession, sessionRefresh, appealController.paramConverter, verifyCaptcha, appealController.controller);
//edit post
router.post('/editpost', geoIp, processIp, useSession, sessionRefresh, csrf, editPostController.paramConverter, Boards.bodyExists, calcPerms,
router.post('/editpost', geoIp, processIp, useSession, sessionRefresh, csrf, editPostController.paramConverter, Boards.bodyExists, setBoardLanguage, calcPerms,
hasPerms.any(Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_BOARD_GENERAL), editPostController.controller);
//board management forms
router.post('/board/:board/transfer', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/transfer', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.any(Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_GLOBAL_BOARDS), transferController.paramConverter, transferController.controller);
router.post('/board/:board/settings', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/settings', geoIp, processIp, useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), boardSettingsController.paramConverter, boardSettingsController.controller);
router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/editbans', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_BANS), editBansController.paramConverter, editBansController.controller); //edit bans
router.post('/board/:board/deleteboard', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/deleteboard', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.any(Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_GLOBAL_BOARDS), deleteBoardController.controller); //delete board
//board crud banners, flags, assets, custompages
router.post('/board/:board/addbanners', useSession, sessionRefresh, fileMiddlewares.banner, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/addbanners', useSession, sessionRefresh, Boards.exists, setBoardLanguage, fileMiddlewares.banner, csrf, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), numFiles, uploadBannersController.controller); //add banners
router.post('/board/:board/deletebanners', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/deletebanners', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteBannersController.paramConverter, deleteBannersController.controller); //delete banners
router.post('/board/:board/addassets', useSession, sessionRefresh, fileMiddlewares.asset, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/addassets', useSession, sessionRefresh, Boards.exists, setBoardLanguage, fileMiddlewares.asset, csrf, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), numFiles, addAssetsController.controller); //add assets
router.post('/board/:board/deleteassets', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/deleteassets', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteAssetsController.paramConverter, deleteAssetsController.controller); //delete assets
router.post('/board/:board/addflags', useSession, sessionRefresh, fileMiddlewares.flag, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/addflags', useSession, sessionRefresh, Boards.exists, setBoardLanguage, fileMiddlewares.flag, csrf, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), numFiles, addFlagsController.controller); //add flags
router.post('/board/:board/deleteflags', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/deleteflags', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteFlagsController.paramConverter, deleteFlagsController.controller); //delete flags
router.post('/board/:board/addcustompages', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/addcustompages', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), addCustomPageController.paramConverter, addCustomPageController.controller); //add custom pages
router.post('/board/:board/deletecustompages', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/deletecustompages', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), deleteCustomPageController.paramConverter, deleteCustomPageController.controller); //delete custom pages
router.post('/board/:board/editcustompage', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/editcustompage', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), editCustomPageController.paramConverter, editCustomPageController.controller); //edit custom page
router.post('/board/:board/addstaff', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/addstaff', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_STAFF), addStaffController.paramConverter, addStaffController.controller); //add board staff
router.post('/board/:board/editstaff', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/editstaff', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_STAFF), editStaffController.paramConverter, editStaffController.controller); //edit staff permission
router.post('/board/:board/deletestaff', useSession, sessionRefresh, csrf, Boards.exists, calcPerms, isLoggedIn,
router.post('/board/:board/deletestaff', useSession, sessionRefresh, csrf, Boards.exists, setBoardLanguage, calcPerms, isLoggedIn,
hasPerms.one(Permissions.MANAGE_BOARD_STAFF), deleteStaffController.paramConverter, deleteStaffController.controller); //delete board staff
//global management forms

@ -21,30 +21,32 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
res.locals.actions = actionChecker(req, res);
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedposts, 1), expected: false, blocking: true, error: 'Must select at least one post' },
{ result: lengthBody(res.locals.actions.validActions, 1), expected: false, blocking: true, error: 'No actions selected' },
{ result: lengthBody(req.body.checkedposts, 1, globalLimits.multiInputs.posts.anon), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: `Must not select >${globalLimits.multiInputs.posts.anon} posts per request` },
{ result: lengthBody(req.body.checkedposts, 1, globalLimits.multiInputs.posts.staff), expected: false, error: `Must not select >${globalLimits.multiInputs.posts.staff} posts per request` },
{ result: (existsBody(req.body.report_ban) && !req.body.checkedreports), expected: false, error: 'Must select post and reports to ban reporter' },
{ result: (existsBody(req.body.checkedreports) && !req.body.report_ban), expected: false, error: 'Must select a report action if checked reports' },
{ result: (existsBody(req.body.checkedreports) && !req.body.checkedposts), expected: false, error: 'Must check parent post if checking reports for report action' },
{ result: (existsBody(req.body.checkedreports) && existsBody(req.body.checkedposts) && lengthBody(req.body.checkedreports, 1, req.body.checkedposts.length*5)), expected: false, error: 'Invalid number of reports checked' },
{ result: res.locals.actions.hasPermission, expected: true, blocking: true, error: 'No permission' },
{ result: (existsBody(req.body.delete) && !res.locals.board.settings.userPostDelete), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: 'User post deletion is disabled on this board' },
{ result: (existsBody(req.body.spoiler) && !res.locals.board.settings.userPostSpoiler), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: 'User file spoiling is disabled on this board' },
{ result: (existsBody(req.body.unlink_file) && !res.locals.board.settings.userPostUnlink), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: 'User file unlinking is disabled on this board' },
{ result: (existsBody(req.body.edit) && lengthBody(req.body.checkedposts, 1, 1)), expected: false, error: 'Must select only 1 post for edit action' },
{ result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: `Password must be ${globalLimits.fieldLength.postpassword} characters or less` },
{ result: lengthBody(req.body.report_reason, 0, globalLimits.fieldLength.report_reason), expected: false, error: `Report must be ${globalLimits.fieldLength.report_reason} characters or less` },
{ result: lengthBody(req.body.ban_reason, 0, globalLimits.fieldLength.ban_reason), expected: false, error: `Ban reason must be ${globalLimits.fieldLength.ban_reason} characters or less` },
{ result: lengthBody(req.body.log_message, 0, globalLimits.fieldLength.log_message), expected: false, error: `Modlog message must be ${globalLimits.fieldLength.log_message} characters or less` },
{ result: (existsBody(req.body.report || req.body.global_report) && lengthBody(req.body.report_reason, 1)), expected: false, blocking: true, error: 'Reports must have a reason' },
{ result: (existsBody(req.body.move) && (!req.body.move_to_thread && !req.body.move_to_board)), expected: false, error: 'Must input destinaton thread number or board to move posts' },
{ result: lengthBody(req.body.checkedposts, 1), expected: false, blocking: true, error: __('Must select at least one post') },
{ result: lengthBody(res.locals.actions.validActions, 1), expected: false, blocking: true, error: __('No actions selected') },
{ result: lengthBody(req.body.checkedposts, 1, globalLimits.multiInputs.posts.anon), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('Must not select >%s posts per request', globalLimits.multiInputs.posts.anon) },
{ result: lengthBody(req.body.checkedposts, 1, globalLimits.multiInputs.posts.staff), expected: false, error: __('Must not select >%s posts per request', globalLimits.multiInputs.posts.staff) },
{ result: (existsBody(req.body.report_ban) && !req.body.checkedreports), expected: false, error: __('Must select post and reports to ban reporter') },
{ result: (existsBody(req.body.checkedreports) && !req.body.report_ban), expected: false, error: __('Must select a report action if checked reports') },
{ result: (existsBody(req.body.checkedreports) && !req.body.checkedposts), expected: false, error: __('Must check parent post if checking reports for report action') },
{ result: (existsBody(req.body.checkedreports) && existsBody(req.body.checkedposts) && lengthBody(req.body.checkedreports, 1, req.body.checkedposts.length*5)), expected: false, error: __('Invalid number of reports checked') },
{ result: res.locals.actions.hasPermission, expected: true, blocking: true, error: __('No permission') },
{ result: (existsBody(req.body.delete) && !res.locals.board.settings.userPostDelete), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('User post deletion is disabled on this board') },
{ result: (existsBody(req.body.spoiler) && !res.locals.board.settings.userPostSpoiler), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('User file spoiling is disabled on this board') },
{ result: (existsBody(req.body.unlink_file) && !res.locals.board.settings.userPostUnlink), permission: Permissions.MANAGE_BOARD_GENERAL, expected: false, error: __('User file unlinking is disabled on this board') },
{ result: (existsBody(req.body.edit) && lengthBody(req.body.checkedposts, 1, 1)), expected: false, error: __('Must select only 1 post for edit action') },
{ result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: __('Password must be %s characters or less', globalLimits.fieldLength.postpassword) },
{ result: lengthBody(req.body.report_reason, 0, globalLimits.fieldLength.report_reason), expected: false, error: __('Report must be %s characters or less', globalLimits.fieldLength.report_reason) },
{ result: lengthBody(req.body.ban_reason, 0, globalLimits.fieldLength.ban_reason), expected: false, error: __('Ban reason must be %s characters or less', globalLimits.fieldLength.ban_reason) },
{ result: lengthBody(req.body.log_message, 0, globalLimits.fieldLength.log_message), expected: false, error: __('Modlog message must be %s characters or less', globalLimits.fieldLength.log_message) },
{ result: (existsBody(req.body.report || req.body.global_report) && lengthBody(req.body.report_reason, 1)), expected: false, blocking: true, error: __('Reports must have a reason') },
{ result: (existsBody(req.body.move) && (!req.body.move_to_thread && !req.body.move_to_board)), expected: false, error: __('Must input destinaton thread number or board to move posts') },
{ result: async () => {
if (req.body.move && req.body.move_to_thread) {
const moveBoard = req.body.move_to_board || req.params.board;
@ -52,7 +54,7 @@ module.exports = {
return res.locals.destinationThread != null;
}
return true;
}, expected: true, error: 'Destination for move does not exist' },
}, expected: true, error: __('Destination for move does not exist') },
{ result: async () => {
if (req.body.move && req.body.move_to_board
&& req.body.move_to_board !== req.params.board) {
@ -69,12 +71,12 @@ module.exports = {
return res.locals.destinationBoard != null;
}
return true;
}, expected: true, error: 'Destination for move does not exist, or you do not have permission' },
}, expected: true, error: __('Destination for move does not exist, or you do not have permission') },
], res.locals.permissions);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/`
});
@ -88,8 +90,8 @@ module.exports = {
if (!res.locals.posts || res.locals.posts.length === 0) {
return dynamicResponse(req, res, 404, 'message', {
'title': 'Not found',
'error': 'Selected posts not found',
'title': __('Not found'),
'error': __('Selected posts not found'),
'redirect': `/${req.params.board}/`
});
}
@ -100,8 +102,8 @@ module.exports = {
} else if (req.body.move) {
if (!res.locals.destinationBoard && !res.locals.destinationThread) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad Request',
'error': 'Invalid post move destination',
'title': __('Bad Request'),
'error': __('Invalid post move destination'),
'redirect': `/${req.params.board}/`
});
}
@ -113,8 +115,8 @@ module.exports = {
});
if (res.locals.posts.length === 0) {
return dynamicResponse(req, res, 409, 'message', {
'title': 'Conflict',
'error': 'Invalid selected posts or destination thread',
'title': __('Conflict'),
'error': __('Invalid selected posts or destination thread'),
'redirect': `/${req.params.board}/`
});
}

@ -12,18 +12,20 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: 'Must provide a file' },
{ result: numberBody(res.locals.numFiles, 0, globalLimits.assetFiles.max), expected: true, error: `Exceeded max asset uploads in one request of ${globalLimits.assetFiles.max}` },
{ result: numberBody(res.locals.board.assets.length+res.locals.numFiles, 0, globalLimits.assetFiles.total), expected: true, error: `Total number of assets would exceed global limit of ${globalLimits.assetFiles.total}` },
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: __('Must provide a file') },
{ result: numberBody(res.locals.numFiles, 0, globalLimits.assetFiles.max), expected: true, error: __('Exceeded max asset uploads in one request of %s', globalLimits.assetFiles.max) },
{ result: numberBody(res.locals.board.assets.length+res.locals.numFiles, 0, globalLimits.assetFiles.total), expected: true, error: __('Total number of assets would exceed global limit of %s', globalLimits.assetFiles.total) },
]);
if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
});

@ -16,32 +16,34 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: existsBody(req.body.message), expected: true, error: 'Missing message' },
{ result: existsBody(req.body.title), expected: true, error: 'Missing title' },
{ result: existsBody(req.body.page), expected: true, error: 'Missing .html name' },
{ result: existsBody(req.body.message), expected: true, error: __('Missing message') },
{ result: existsBody(req.body.title), expected: true, error: __('Missing title' ) },
{ result: existsBody(req.body.page), expected: true, error: __('Missing .html name') },
{ result: () => {
if (req.body.page) {
return /^[a-z0-9_-]+$/i.test(req.body.page);
}
return false;
} , expected: true, error: '.html name must contain a-z 0-9 _ - only' },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.customPages.maxLength), expected: true, error: `Message must be ${globalLimits.customPages.maxLength} characters or less` },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: 'Title must be 50 characters or less' },
{ result: lengthBody(req.body.page, 0, 50), expected: false, error: '.html name must be 50 characters or less' },
} , expected: true, error: __('.html name must contain a-z 0-9 _ - only') },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.customPages.maxLength), expected: true, error: __('Message must be %s characters or less', globalLimits.customPages.maxLength) },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: __('Title must be 50 characters or less') },
{ result: lengthBody(req.body.page, 0, 50), expected: false, error: __('.html name must be 50 characters or less') },
{ result: async () => {
return (await CustomPages.boardCount(req.params.board)) > globalLimits.customPages.max;
}, expected: false, error: `Can only create ${globalLimits.customPages.max} pages per board`},
}, expected: false, error: __('Can only create %s pages per board', globalLimits.customPages.max) },
{ result: async () => {
return (await CustomPages.findOne(req.params.board, req.body.page)) == null;
}, expected: true, error: '.html name must be unique'},
}, expected: true, error: __('.html name must be unique') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.booard}/manage/custompages.html`
});

@ -12,18 +12,20 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: 'Must provide a file' },
{ result: numberBody(res.locals.numFiles, 0, globalLimits.flagFiles.max), expected: true, error: `Exceeded max flag uploads in one request of ${globalLimits.flagFiles.max}` },
{ result: numberBody(Object.keys(res.locals.board.flags).length+res.locals.numFiles, 0, globalLimits.flagFiles.total), expected: true, error: `Total number of flags would exceed global limit of ${globalLimits.flagFiles.total}` },
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: __('Must provide a file') },
{ result: numberBody(res.locals.numFiles, 0, globalLimits.flagFiles.max), expected: true, error: __('Exceeded max flag uploads in one request of %s', globalLimits.flagFiles.max) },
{ result: numberBody(Object.keys(res.locals.board.flags).length+res.locals.numFiles, 0, globalLimits.flagFiles.total), expected: true, error: __('Total number of flags would exceed global limit of %s', globalLimits.flagFiles.total) },
]);
if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
});

@ -15,18 +15,20 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: existsBody(req.body.message), expected: true, error: 'Missing message' },
{ result: existsBody(req.body.title), expected: true, error: 'Missing title' },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.fieldLength.message), expected: true, error: `Message must be ${globalLimits.fieldLength.message} characters or less` },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: 'Title must be 50 characters or less' },
{ result: existsBody(req.body.message), expected: true, error: __('Missing message') },
{ result: existsBody(req.body.title), expected: true, error: __('Missing title') },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.fieldLength.message), expected: true, error: __('Message must be %s characters or less', globalLimits.fieldLength.message) },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: __('Title must be 50 characters or less') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/globalmanage/news.html'
});

@ -14,20 +14,22 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing staff username' },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: 'Username must be 50 characters or less' },
{ result: (res.locals.board.owner === req.body.username), expected: false, blocking: true, error: 'User is already board owner' },
{ result: (res.locals.board.staff[req.body.username] != null), expected: false, blocking: true, error: 'User is already staff' },
{ result: existsBody(req.body.username), expected: true, error: __('Missing staff username') },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: __('Username must be 50 characters or less') },
{ result: (res.locals.board.owner === req.body.username), expected: false, blocking: true, error: __('User is already board owner') },
{ result: (res.locals.board.staff[req.body.username] != null), expected: false, blocking: true, error: __('User is already staff') },
{ result: async () => {
const numAccounts = await Accounts.countUsers([req.body.username]);
return numAccounts > 0;
}, expected: true, error: 'User does not exist' },
}, expected: true, error: __('User does not exist') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
});

@ -17,17 +17,19 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: existsBody(req.body.message), expected: true, error: 'Appeals must include a message' },
{ result: existsBody(req.body.checkedbans), expected: true, error: 'Must select at least one ban to appeal' },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.fieldLength.message), expected: true, error: `Appeal message must be ${globalLimits.fieldLength.message} characters or less` },
{ result: existsBody(req.body.message), expected: true, error: __('Appeals must include a message') },
{ result: existsBody(req.body.checkedbans), expected: true, error: __('Must select at least one ban to appeal') },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.fieldLength.message), expected: true, error: __('Appeal message must be %s characters or less', globalLimits.fieldLength.message) },
]); //should appeals really be based off message field length global limit? minor.
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/'
});
@ -44,15 +46,15 @@ module.exports = {
/* this can occur if they selected invalid id, non-ip match, already appealed, or unappealable bans. prevented by databse filter, so we use
use the updatedCount return value to check if any appeals were made successfully. if not, we end up here. */
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'error': 'Invalid bans selected',
'title': __('Bad request'),
'error': __('Invalid bans selected'),
'redirect': '/'
});
}
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': `Appealed ${amount} bans successfully`,
'title': __('Success'),
'message': __('Appealed %s bans successfully', amount),
'redirect': '/'
});

@ -7,6 +7,7 @@ const changeBoardSettings = require(__dirname+'/../../models/forms/changeboardse
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, config = require(__dirname+'/../../lib/misc/config.js')
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
, i18n = require(__dirname+'/../../lib/locale/locale.js')
, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
inArrayBody, arrayInBody } = require(__dirname+'/../../lib/input/schema.js');
@ -14,7 +15,7 @@ module.exports = {
paramConverter: paramConverter({
timeFields: ['ban_duration', 'delete_protection_age'],
trimFields: ['filters', 'moderators', 'tags', 'announcement', 'description', 'name', 'custom_css'],
trimFields: ['filters', 'tags', 'announcement', 'description', 'name', 'custom_css', 'language'],
allowedArrays: ['countries'],
numberFields: ['lock_reset', 'captcha_reset', 'filter_mode', 'lock_mode', 'message_r9k_mode', 'file_r9k_mode', 'captcha_mode', 'tph_trigger', 'pph_trigger', 'pph_trigger_action',
'tph_trigger_action', 'bump_limit', 'reply_limit', 'max_files', 'thread_limit', 'max_thread_message_length', 'max_reply_message_length', 'min_thread_message_length',
@ -23,61 +24,63 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits, rateLimitCost } = config.get
, maxThread = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) || globalLimits.fieldLength.message)
, maxReply = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) || globalLimits.fieldLength.message);
const errors = await checkSchema([
{ result: lengthBody(req.body.description, 0, globalLimits.fieldLength.description), expected: false, error: `Board description must be ${globalLimits.fieldLength.description} characters or less` },
{ result: lengthBody(req.body.announcements, 0, 5000), expected: false, error: 'Board announcements must be 5000 characters or less' },
{ result: lengthBody(req.body.tags, 0, 2000), expected: false, error: 'Tags length must be 2000 characters or less' },
{ result: lengthBody(req.body.filters, 0, 20000), expected: false, error: 'Filters length must be 20000 characters or less' },
{ result: lengthBody(req.body.custom_css, 0, globalLimits.customCss.max), expected: false, error: `Custom CSS must be ${globalLimits.customCss.max} characters or less` },
{ result: arrayInBody(globalLimits.customCss.filters, req.body.custom_css), permission: Permissions.ROOT, expected: false, error: `Custom CSS strict mode is enabled and does not allow the following: "${globalLimits.customCss.filters.join('", "')}"` },
{ result: lengthBody(req.body.moderators, 0, 500), expected: false, error: 'Moderators length must be 500 characters orless' },
{ result: lengthBody(req.body.name, 1, globalLimits.fieldLength.boardname), expected: false, error: `Board name must be 1-${globalLimits.fieldLength.boardname} characters` },
{ result: lengthBody(req.body.default_name, 0, 50), expected: false, error: 'Anon name must be 50 characters or less' },
{ result: numberBody(req.body.reply_limit, globalLimits.replyLimit.min, globalLimits.replyLimit.max), expected: true, error: `Reply Limit must be ${globalLimits.replyLimit.min}-${globalLimits.replyLimit.max}` },
{ result: numberBody(req.body.bump_limit, globalLimits.bumpLimit.min, globalLimits.bumpLimit.max), expected: true, error: `Bump Limit must be ${globalLimits.bumpLimit.min}-${globalLimits.bumpLimit.max}` },
{ result: numberBody(req.body.thread_limit, globalLimits.threadLimit.min, globalLimits.threadLimit.max), expected: true, error: `Threads Limit must be ${globalLimits.threadLimit.min}-${globalLimits.threadLimit.max}` },
{ result: numberBody(req.body.max_files, 0, globalLimits.postFiles.max), expected: true, error: `Max files must be 0-${globalLimits.postFiles.max}` },
{ result: numberBody(req.body.min_thread_message_length, 0, globalLimits.fieldLength.message), expected: true, error: `Min thread message length must be 0-${globalLimits.fieldLength.message}` },
{ result: numberBody(req.body.min_reply_message_length, 0, globalLimits.fieldLength.message), expected: true, error: `Min reply message length must be 0-${globalLimits.fieldLength.message}` },
{ result: numberBody(req.body.max_thread_message_length, 0, globalLimits.fieldLength.message), expected: true, error: `Max thread message length must be 0-${globalLimits.fieldLength.message}` },
{ result: numberBody(req.body.max_reply_message_length, 0, globalLimits.fieldLength.message), expected: true, error: `Max reply message length must be 0-${globalLimits.fieldLength.message}` },
{ result: minmaxBody(req.body.min_thread_message_length, req.body.max_thread_message_length), expected: true, error: 'Min and max thread message lengths must not violate eachother' },
{ result: minmaxBody(req.body.min_reply_message_length, req.body.max_reply_message_length), expected: true, error: 'Min and max reply message lengths must not violate eachother' },
{ result: lengthBody(req.body.description, 0, globalLimits.fieldLength.description), expected: false, error: __('Board description must be %s characters or less', globalLimits.fieldLength.description) },
{ result: lengthBody(req.body.announcements, 0, 5000), expected: false, error: __('Board announcements must be 5000 characters or less') },
{ result: lengthBody(req.body.tags, 0, 2000), expected: false, error: __('Tags length must be 2000 characters or less') },
{ result: lengthBody(req.body.filters, 0, 20000), expected: false, error: __('Filters length must be 20000 characters or less') },
{ result: lengthBody(req.body.custom_css, 0, globalLimits.customCss.max), expected: false, error: __('Custom CSS must be %s characters or less', globalLimits.customCss.max) },
{ result: arrayInBody(globalLimits.customCss.filters, req.body.custom_css), permission: Permissions.ROOT, expected: false, error: __('Custom CSS strict mode is enabled and does not allow the following: "%s"', globalLimits.customCss.filters.join('", "')) },
{ result: lengthBody(req.body.name, 1, globalLimits.fieldLength.boardname), expected: false, error: __('Board name must be 1-%s characters', globalLimits.fieldLength.boardname) },
{ result: lengthBody(req.body.default_name, 0, 50), expected: false, error: __('Anon name must be 50 characters or less') },
{ result: numberBody(req.body.reply_limit, globalLimits.replyLimit.min, globalLimits.replyLimit.max), expected: true, error: __('Reply Limit must be %s-%s', globalLimits.replyLimit.min, globalLimits.replyLimit.max) },
{ result: numberBody(req.body.bump_limit, globalLimits.bumpLimit.min, globalLimits.bumpLimit.max), expected: true, error: __('Bump Limit must be %s-%s', globalLimits.bumpLimit.min, globalLimits.bumpLimit.max) },
{ result: numberBody(req.body.thread_limit, globalLimits.threadLimit.min, globalLimits.threadLimit.max), expected: true, error: __('Threads Limit must be %s-%s', globalLimits.threadLimit.min, globalLimits.threadLimit.max) },
{ result: numberBody(req.body.max_files, 0, globalLimits.postFiles.max), expected: true, error: __('Max files must be 0-%s', globalLimits.postFiles.max) },
{ result: numberBody(req.body.min_thread_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Min thread message length must be 0-%s', globalLimits.fieldLength.message) },
{ result: numberBody(req.body.min_reply_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Min reply message length must be 0-%s', globalLimits.fieldLength.message) },
{ result: numberBody(req.body.max_thread_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Max thread message length must be 0-%s', globalLimits.fieldLength.message) },
{ result: numberBody(req.body.max_reply_message_length, 0, globalLimits.fieldLength.message), expected: true, error: __('Max reply message length must be 0-%s', globalLimits.fieldLength.message) },
{ result: minmaxBody(req.body.min_thread_message_length, req.body.max_thread_message_length), expected: true, error: __('Min and max thread message lengths must not violate eachother') },
{ result: minmaxBody(req.body.min_reply_message_length, req.body.max_reply_message_length), expected: true, error: __('Min and max reply message lengths must not violate eachother') },
{ result: numberBodyVariable(req.body.min_thread_message_length, res.locals.board.settings.minThreadMessageLength,
req.body.min_thread_message_length, maxThread, req.body.max_thread_message_length), expected: true,
error: `Min thread message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Thread Message Length" (currently ${res.locals.board.settings.maxThreadMessageLength})` },
error: __('Min thread message length must be 0-%s and not more than "Max Thread Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) },
{ result: numberBodyVariable(req.body.min_reply_message_length, res.locals.board.settings.minReplyMessageLength,
req.body.min_reply_message_length, maxReply, req.body.max_reply_message_length), expected: true,
error: `Min reply message length must be 0-${globalLimits.fieldLength.message} and not more than "Max Reply Message Length" (currently ${res.locals.board.settings.maxReplyMessageLength})` },
error: __('Min reply message length must be 0-%s and not more than "Max Reply Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) },
{ result: numberBodyVariable(req.body.max_thread_message_length, res.locals.board.settings.minThreadMessageLength,
req.body.min_thread_message_length, globalLimits.fieldLength.message, globalLimits.fieldLength.message), expected: true,
error: `Max thread message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Thread Message Length" (currently ${res.locals.board.settings.minThreadMessageLength})` },
error: __('Max thread message length must be 0-%s and not less than "Min Thread Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.minThreadMessageLength) },
{ result: numberBodyVariable(req.body.max_reply_message_length, res.locals.board.settings.minReplyMessageLength,
req.body.min_reply_message_length, globalLimits.fieldLength.message, globalLimits.fieldLength.message), expected: true,
error: `Max reply message length must be 0-${globalLimits.fieldLength.message} and not less than "Min Reply Message Length" (currently ${res.locals.board.settings.minReplyMessageLength})` },
{ result: numberBody(req.body.lock_mode, 0, 2), expected: true, error: 'Invalid lock mode' },
{ result: numberBody(req.body.captcha_mode, 0, 2), expected: true, error: 'Invalid captcha mode' },
{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: 'Invalid filter mode' },
{ result: numberBody(req.body.tph_trigger, 0, 10000), expected: true, error: 'Invalid tph trigger threshold' },
{ result: numberBody(req.body.tph_trigger_action, 0, 4), expected: true, error: 'Invalid tph trigger action' },
{ result: numberBody(req.body.pph_trigger, 0, 10000), expected: true, error: 'Invalid pph trigger threshold' },
{ result: numberBody(req.body.pph_trigger_action, 0, 4), expected: true, error: 'Invalid pph trigger action' },
{ result: numberBody(req.body.lock_reset, 0, 2), expected: true, error: 'Invalid trigger reset lock' },
{ result: numberBody(req.body.captcha_reset, 0, 2), expected: true, error: 'Invalid trigger reset captcha' },
{ result: numberBody(req.body.ban_duration, 0), expected: true, error: 'Invalid filter auto ban duration' },
{ result: numberBody(req.body.delete_protection_age, 0), expected: true, error: 'Invalid OP thread age delete protection' },
{ result: numberBody(req.body.delete_protection_count, 0), expected: true, error: 'Invalid OP thread reply count delete protection' },
{ result: inArrayBody(req.body.theme, themes), expected: true, error: 'Invalid theme' },
{ result: inArrayBody(req.body.code_theme, codeThemes), expected: true, error: 'Invalid code theme' },
error: __('Max reply message length must be 0-%s and not less than "Min Reply Message Length" (currently %s)', globalLimits.fieldLength.message, res.locals.board.settings.minReplyMessageLength) },
{ result: numberBody(req.body.lock_mode, 0, 2), expected: true, error: __('Invalid lock mode') },
{ result: numberBody(req.body.captcha_mode, 0, 2), expected: true, error: __('Invalid captcha mode') },
{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: __('Invalid filter mode') },
{ result: numberBody(req.body.tph_trigger, 0, 10000), expected: true, error: __('Invalid tph trigger threshold') },
{ result: numberBody(req.body.tph_trigger_action, 0, 4), expected: true, error: __('Invalid tph trigger action') },
{ result: numberBody(req.body.pph_trigger, 0, 10000), expected: true, error: __('Invalid pph trigger threshold') },
{ result: numberBody(req.body.pph_trigger_action, 0, 4), expected: true, error: __('Invalid pph trigger action') },
{ result: numberBody(req.body.lock_reset, 0, 2), expected: true, error: __('Invalid trigger reset lock') },
{ result: numberBody(req.body.captcha_reset, 0, 2), expected: true, error: __('Invalid trigger reset captcha') },
{ result: numberBody(req.body.ban_duration, 0), expected: true, error: __('Invalid filter auto ban duration') },
{ result: numberBody(req.body.delete_protection_age, 0), expected: true, error: __('Invalid OP thread age delete protection') },
{ result: numberBody(req.body.delete_protection_count, 0), expected: true, error: __('Invalid OP thread reply count delete protection') },
{ result: inArrayBody(req.body.language, i18n.getLocales()), expected: true, error: __('Invalid language') },
{ result: inArrayBody(req.body.theme, themes), expected: true, error: __('Invalid theme') },
{ result: inArrayBody(req.body.code_theme, codeThemes), expected: true, error: __('Invalid code theme') },
], res.locals.permissions);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/settings.html`
});
@ -88,8 +91,8 @@ module.exports = {
const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'settings', rateLimitCost.boardSettings));
if (ratelimitBoard > 100 || ratelimitIp > 100) {
return dynamicResponse(req, res, 429, 'message', {
'title': 'Ratelimited',
'error': 'You are changing settings too quickly, please wait a minute and try again',
'title': __('Ratelimited'),
'error': __('You are changing settings too quickly, please wait a minute and try again'),
'redirect': `/${req.params.board}/manage/settings.html`
});
}

@ -13,22 +13,24 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: 'Username must be 50 characters or less' },
{ result: existsBody(req.body.password), expected: true, error: 'Missing password' },
{ result: lengthBody(req.body.password, 0, 50), expected: false, error: 'Password must be 50 characters or less' },
{ result: existsBody(req.body.newpassword), expected: true, error: 'Missing new password' },
{ result: lengthBody(req.body.newpassword, 0, 100), expected: false, error: 'New pasword must be 100 characters or less' },
{ result: existsBody(req.body.newpasswordconfirm), expected: true, error: 'Missing new password confirmation' },
{ result: lengthBody(req.body.newpasswordconfirm, 0, 100), expected: false, error: 'New password confirmation must be 100 characters or less' },
{ result: (req.body.newpassword === req.body.newpasswordconfirm), expected: true, error: 'New password and password confirmation must match' },
{ result: lengthBody(req.body.twofactor, 0, 6), expected: false, error: 'Invalid 2FA code' },
{ result: existsBody(req.body.username), expected: true, error: __('Missing username') },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: __('Username must be 50 characters or less') },
{ result: existsBody(req.body.password), expected: true, error: __('Missing password') },
{ result: lengthBody(req.body.password, 0, 50), expected: false, error: __('Password must be 50 characters or less') },
{ result: existsBody(req.body.newpassword), expected: true, error: __('Missing new password') },
{ result: lengthBody(req.body.newpassword, 0, 100), expected: false, error: __('New pasword must be 100 characters or less') },
{ result: existsBody(req.body.newpasswordconfirm), expected: true, error: __('Missing new password confirmation') },
{ result: lengthBody(req.body.newpasswordconfirm, 0, 100), expected: false, error: __('New password confirmation must be 100 characters or less') },
{ result: (req.body.newpassword === req.body.newpasswordconfirm), expected: true, error: __('New password and password confirmation must match') },
{ result: lengthBody(req.body.twofactor, 0, 6), expected: false, error: __('Invalid 2FA code') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/changepassword.html'
});

@ -15,21 +15,23 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: res.locals.permissions.get(Permissions.CREATE_BOARD), blocking: true, expected: true, error: 'No permission' },
{ result: existsBody(req.body.uri), expected: true, error: 'Missing URI' },
{ result: lengthBody(req.body.uri, 0, globalLimits.fieldLength.uri), expected: false, error: `URI must be ${globalLimits.fieldLength.uri} characters or less` },
{ result: existsBody(req.body.name), expected: true, error: 'Missing name' },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.boardname), expected: false, error: `Name must be ${globalLimits.fieldLength.boardname} characters or less` },
{ result: alphaNumericRegex.test(req.body.uri), expected: true, error: 'URI must contain a-z 0-9 only' },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.description), expected: false, error: `Description must be ${globalLimits.fieldLength.description} characters or less` },
{ result: res.locals.permissions.get(Permissions.CREATE_BOARD), blocking: true, expected: true, error: __('No permission') },
{ result: existsBody(req.body.uri), expected: true, error: __('Missing URI') },
{ result: lengthBody(req.body.uri, 0, globalLimits.fieldLength.uri), expected: false, error: __('URI must be %s characters or less', globalLimits.fieldLength.uri) },
{ result: existsBody(req.body.name), expected: true, error: __('Missing name') },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.boardname), expected: false, error: __('Name must be %s characters or less', globalLimits.fieldLength.boardname) },
{ result: alphaNumericRegex.test(req.body.uri), expected: true, error: __('URI must contain a-z 0-9 only') },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.description), expected: false, error: __('Description must be %s characters or less', globalLimits.fieldLength.description) },
], res.locals.permissions);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/create.html'
});

@ -11,16 +11,18 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { staffBoards, ownedBoards } = res.locals.user;
const errors = await checkSchema([
{ result: existsBody(req.body.confirm), expected: true, error: 'Missing confirmation' },
{ result: (numberBody(ownedBoards.length, 0, 0) && numberBody(staffBoards.length, 0, 0)), expected: true, error: 'You cannot delete your account while you hold staff position on any board' },
{ result: existsBody(req.body.confirm), expected: true, error: __('Missing confirmation') },
{ result: (numberBody(ownedBoards.length, 0, 0) && numberBody(staffBoards.length, 0, 0)), expected: true, error: __('You cannot delete your account while you hold staff position on any board') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/account.html',
});
@ -33,8 +35,8 @@ module.exports = {
}
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Account deleted',
'title': __('Success'),
'message': __('Account deleted'),
'redirect': '/',
});

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedaccounts, 1), expected: false, error: 'Must select at least one account' },
{ result: lengthBody(req.body.checkedaccounts, 1), expected: false, error: __('Must select at least one account') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/globalmanage/accounts.html'
});

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedassets, 1), expected: false, error: 'Must select at least one asset to delete' },
{ result: lengthBody(req.body.checkedassets, 1), expected: false, error: __('Must select at least one asset to delete') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
});
@ -28,8 +30,8 @@ module.exports = {
for (let i = 0; i < req.body.checkedassets.length; i++) {
if (!res.locals.board.assets.includes(req.body.checkedassets[i])) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Invalid assets selected',
'title': __('Bad request'),
'message': __('Invalid assets selected'),
'redirect': `/${req.params.board}/manage/assets.html`
});
}

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedbanners, 1), expected: false, error: 'Must select at least one banner to delete' },
{ result: lengthBody(req.body.checkedbanners, 1), expected: false, error: __('Must select at least one banner to delete') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
});
@ -28,8 +30,8 @@ module.exports = {
for (let i = 0; i < req.body.checkedbanners.length; i++) {
if (!res.locals.board.banners.includes(req.body.checkedbanners[i])) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Invalid banners selected',
'title': __('Bad request'),
'message': __('Invalid banners selected'),
'redirect': `/${req.params.board}/manage/assets.html`
});
}

@ -14,21 +14,23 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
let board = null;
const errors = await checkSchema([
{ result: existsBody(req.body.confirm), expected: true, error: 'Missing confirmation' },
{ result: existsBody(req.body.uri), expected: true, error: 'Missing URI' },
{ result: alphaNumericRegex.test(req.body.uri), blocking: true, expected: true, error: 'URI must contain a-z 0-9 only'},
{ result: req.params.board == null || (req.params.board === req.body.uri), expected: true, error: 'URI does not match current board' },
{ result: existsBody(req.body.confirm), expected: true, error: __('Missing confirmation') },
{ result: existsBody(req.body.uri), expected: true, error: __('Missing URI') },
{ result: alphaNumericRegex.test(req.body.uri), blocking: true, expected: true, error: __('URI must contain a-z 0-9 only') },
{ result: req.params.board == null || (req.params.board === req.body.uri), expected: true, error: __('URI does not match current board') },
{ result: async () => {
board = await Boards.findOne(req.body.uri);
return board != null;
}, expected: true, error: `Board /${req.body.uri}/ does not exist` }
}, expected: true, error: __('Board /%s/ does not exist', req.body.uri) }
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.params.board ? `/${req.params.board}/manage/settings.html` : '/globalmanage/settings.html'
});
@ -41,8 +43,8 @@ module.exports = {
}
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Board deleted',
'title': __('Success'),
'message': __('Board deleted'),
'redirect': req.params.board ? '/' : '/globalmanage/settings.html'
});

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedcustompages, 1), expected: false, error: 'Must select at least one custom page to delete' },
{ result: lengthBody(req.body.checkedcustompages, 1), expected: false, error: __('Must select at least one custom page to delete') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/custompages.html`
});

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedflags, 1), expected: false, error: 'Must select at least one flag to delete' },
{ result: lengthBody(req.body.checkedflags, 1), expected: false, error: __('Must select at least one flag to delete') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
});
@ -28,8 +30,8 @@ module.exports = {
for (let i = 0; i < req.body.checkedflags.length; i++) {
if (!res.locals.board.flags[req.body.checkedflags[i]]) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Invalid flags selected',
'title': __('Bad request'),
'message': __('Invalid flags selected'),
'redirect': `/${req.params.board}/manage/assets.html`
});
}

@ -14,13 +14,15 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkednews, 1), expected: false, error: 'Must select at least one newspost to delete' },
{ result: lengthBody(req.body.checkednews, 1), expected: false, error: __('Must select at least one newspost to delete') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/globalmanage/news.html'
});

@ -13,19 +13,21 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const username = res.locals.user.username;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedsessionids, 1), expected: false, blocking: true, error: 'Must select at least one session to delete' },
{ result: lengthBody(req.body.checkedsessionids, 1), expected: false, blocking: true, error: __('Must select at least one session to delete') },
{ result: () => {
//return if any input "session ids" dont start with sess: or dont end with :username
return req.body.checkedsessionids.some(sid => !sid.startsWith('sess:') || !sid.endsWith(`:${username}`));
}, expected: false, error: 'Invalid checked sessions' },
}, expected: false, error: __('Invalid checked sessions') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/sessions.html',
});
@ -38,8 +40,8 @@ module.exports = {
}
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Sessions deleted',
'title': __('Success'),
'message': __('Sessions deleted'),
'redirect': '/sessions.html', //if deleting all, will get redirected back to login anyway
});

@ -14,17 +14,19 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedstaff, 1), expected: false, error: 'Must select at least one staff to delete' },
{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => !res.locals.board.staff[s]), expected: false, error: 'Invalid staff selection' },
{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => s === res.locals.board.owner), expected: false, permission: Permissions.ROOT, error: 'You can\'t delete the board owner' },
{ result: lengthBody(req.body.checkedstaff, 1), expected: false, error: __('Must select at least one staff to delete') },
{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => !res.locals.board.staff[s]), expected: false, error: __('Invalid staff selection') },
{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => s === res.locals.board.owner), expected: false, permission: Permissions.ROOT, error: __('You can\'t delete the board owner') },
//not really necessary, but its a bit retarded to "delete yourself" as staff this way
{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => s === res.locals.user.username), expected: false, error: 'Resign from the accounts page instead' },
{ result: existsBody(req.body.checkedstaff) && req.body.checkedstaff.some(s => s === res.locals.user.username), expected: false, error: __('Resign from the accounts page instead') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
});

@ -17,19 +17,21 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' },
{ result: lengthBody(req.body.username, 1, 50), expected: false, error: 'Username must be 50 characters or less' },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: 'Username must contain a-z 0-9 only' },
{ result: existsBody(req.body.username), expected: true, error: __('Missing username') },
{ result: lengthBody(req.body.username, 1, 50), expected: false, error: __('Username must be 50 characters or less') },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: __('Username must contain a-z 0-9 only') },
{ result: async () => {
res.locals.editingAccount = await Accounts.findOne(req.body.username);
return res.locals.editingAccount != null;
}, expected: true, blocking: true, error: 'Invalid account username' },
{ result: (res.locals.user.username === req.body.username), expected: false, error: 'You can\'t edit your own permissions' },
}, expected: true, blocking: true, error: __('Invalid account username') },
{ result: (res.locals.user.username === req.body.username), expected: false, error: __('You can\'t edit your own permissions') },
{ result: !existsBody(req.body.template) //no template, OR the template is a valid one
|| inArrayBody(req.body.template, [roleManager.roles.ANON.base64, roleManager.roles.GLOBAL_STAFF.base64,
roleManager.roles.ADMIN.base64, roleManager.roles.BOARD_STAFF.base64, roleManager.roles.BOARD_OWNER.base64]),
expected: true, error: 'Invalid template selection' },
expected: true, error: __('Invalid template selection') },
{ result: () => {
//not applying a template, OR the user doesn't have root perms, has to be a function to execute after the async result above.
if (!existsBody(req.body.template)) {
@ -38,12 +40,12 @@ module.exports = {
const editingPermission = new Permission(res.locals.editingAccount.permissions);
return !editingPermission.get(Permissions.ROOT);
},
expected: true, error: 'You can\'t apply template permissions to a ROOT user.' },
expected: true, error: __('You can\'t apply template permissions to a ROOT user.') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
});

@ -22,21 +22,23 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: lengthBody(req.body.checkedbans, 1), expected: false, error: 'Must select at least one ban' },
{ result: inArrayBody(req.body.option, ['unban', 'edit_duration', 'edit_note', 'upgrade', 'deny_appeal']), expected: true, error: 'Invalid ban action' },
{ result: req.body.option !== 'edit_duration' || numberBody(req.body.ban_duration, 1), expected: true, error: 'Invalid ban duration' },
{ result: req.body.option !== 'edit_note' || !lengthBody(req.body.ban_note, 1, globalLimits.fieldLength.log_message), expected: true, error: `Ban note must be ${globalLimits.fieldLength.log_message} characters or less` },
{ result: req.body.option !== 'upgrade' || inArrayBody(req.body.upgrade, [1, 2]), expected: true, error: 'Invalid ban upgrade option' },
{ result: lengthBody(req.body.checkedbans, 1), expected: false, error: __('Must select at least one ban') },
{ result: inArrayBody(req.body.option, ['unban', 'edit_duration', 'edit_note', 'upgrade', 'deny_appeal']), expected: true, error: __('Invalid ban action') },
{ result: req.body.option !== 'edit_duration' || numberBody(req.body.ban_duration, 1), expected: true, error: __('Invalid ban duration') },
{ result: req.body.option !== 'edit_note' || !lengthBody(req.body.ban_note, 1, globalLimits.fieldLength.log_message), expected: true, error: __('Ban note must be %s characters or less', globalLimits.fieldLength.log_message) },
{ result: req.body.option !== 'upgrade' || inArrayBody(req.body.upgrade, [1, 2]), expected: true, error: __('Invalid ban upgrade option') },
]);
const redirect = req.params.board ? `/${req.params.board}/manage/bans.html` : '/globalmanage/bans.html';
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
redirect
});
@ -48,33 +50,33 @@ module.exports = {
switch(req.body.option) {
case 'unban':
amount = await removeBans(req, res, next);
message = `Removed ${amount} bans`;
message = __('Removed %s bans', amount);
break;
case 'deny_appeal':
amount = await denyAppeals(req, res, next);
message = `Denied ${amount} appeals`;
message = __('Denied %s appeals', amount);
break;
case 'upgrade':
amount = await upgradeBans(req, res, next);
message = `Upgraded ${amount} bans`;
message = __('Upgraded %s bans', amount);
break;
case 'edit_duration':
amount = await editBanDuration(req, res, next);
message = `Edited duration for ${amount} bans`;
message = __('Edited duration for %s bans', amount);
break;
case 'edit_note':
amount = await editBanNote(req, res, next);
message = `Edited note for ${amount} bans`;
message = __('Edited note for %s bans', amount);
break;
default:
throw 'Invalid ban action'; //should never happen anyway
throw __('Invalid ban action'); //should never happen anyway
}
} catch (err) {
return next(err);
}
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'title': __('Success'),
message,
redirect
});

@ -17,34 +17,36 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: existsBody(req.body.page_id), expected: true, error: 'Missing page id' },
{ result: existsBody(req.body.message), expected: true, error: 'Missing message' },
{ result: existsBody(req.body.title), expected: true, error: 'Missing title' },
{ result: existsBody(req.body.page), expected: true, error: 'Missing .html name' },
{ result: existsBody(req.body.page_id), expected: true, error: __('Missing page id') },
{ result: existsBody(req.body.message), expected: true, error: __('Missing message') },
{ result: existsBody(req.body.title), expected: true, error: __('Missing title') },
{ result: existsBody(req.body.page), expected: true, error: __('Missing .html name') },
{ result: () => {
if (req.body.page) {
return /^[a-z0-9_-]+$/i.test(req.body.page);
}
return false;
} , expected: true, error: '.html name must contain a-z 0-9 _ - only' },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.customPages.maxLength), expected: true, error: `Message must be ${globalLimits.customPages.maxLength} characters or less` },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: 'Title must be 50 characters or less' },
{ result: lengthBody(req.body.page, 0, 50), expected: false, error: '.html name must be 50 characters or less' },
} , expected: true, error: __('.html name must contain a-z 0-9 _ - only') },
{ result: numberBody(res.locals.messageLength, 0, globalLimits.customPages.maxLength), expected: true, error: __('Message must be %s characters or less', globalLimits.customPages.maxLength) },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: __('Title must be 50 characters or less') },
{ result: lengthBody(req.body.page, 0, 50), expected: false, error: __('.html name must be 50 characters or less') },
{ result: async () => {
const existingPage = await CustomPages.findOne(req.params.board, req.body.page);
if (existingPage && existingPage.page === req.body.page) {
return existingPage._id.equals(req.body.page_id);
}
return true;
}, expected: true, error: '.html name must be unique'},
}, expected: true, error: __('.html name must be unique') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/custompages.html`,
});

@ -15,17 +15,19 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.news_id), expected: true, error: 'Missing news id' },
{ result: existsBody(req.body.message), expected: true, error: 'Missing message' },
{ result: numberBody(res.locals.messageLength, 0, 10000), expected: true, error: 'Message must be 10000 characters or less' },
{ result: existsBody(req.body.title), expected: true, error: 'Missing title' },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: 'Title must be 50 characters or less' },
{ result: existsBody(req.body.news_id), expected: true, error: __('Missing news id') },
{ result: existsBody(req.body.message), expected: true, error: __('Missing message') },
{ result: numberBody(res.locals.messageLength, 0, 10000), expected: true, error: __('Message must be 10000 characters or less') },
{ result: existsBody(req.body.title), expected: true, error: __('Missing title') },
{ result: lengthBody(req.body.title, 0, 50), expected: false, error: __('Title must be 50 characters or less') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.headers.referer || '/globalmanage/news.html'
});

@ -18,25 +18,27 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { rateLimitCost, globalLimits } = config.get;
const errors = await checkSchema([
{ result: existsBody(req.body.board), expected: true, error: 'Missing board' },
{ result: numberBody(req.body.postId, 1), expected: true, error: 'Missing postId' },
{ result: lengthBody(req.body.message, 0, globalLimits.fieldLength.message), expected: false, error: `Message must be ${globalLimits.fieldLength.message} characters or less` },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.name), expected: false, error: `Name must be ${globalLimits.fieldLength.name} characters or less` },
{ result: lengthBody(req.body.subject, 0, globalLimits.fieldLength.subject), expected: false, error: `Subject must be ${globalLimits.fieldLength.subject} characters or less` },
{ result: lengthBody(req.body.email, 0, globalLimits.fieldLength.email), expected: false, error: `Email must be ${globalLimits.fieldLength.email} characters or less` },
{ result: lengthBody(req.body.log_message, 0, globalLimits.fieldLength.log_message), expected: false, error: `Modlog message must be ${globalLimits.fieldLength.log_message} characters or less` },
{ result: existsBody(req.body.board), expected: true, error: __('Missing board') },
{ result: numberBody(req.body.postId, 1), expected: true, error: __('Missing postId') },
{ result: lengthBody(req.body.message, 0, globalLimits.fieldLength.message), expected: false, error: __('Message must be %s characters or less', globalLimits.fieldLength.message) },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.name), expected: false, error: __('Name must be %s characters or less', globalLimits.fieldLength.name) },
{ result: lengthBody(req.body.subject, 0, globalLimits.fieldLength.subject), expected: false, error: __('Subject must be %s characters or less', globalLimits.fieldLength.subject) },
{ result: lengthBody(req.body.email, 0, globalLimits.fieldLength.email), expected: false, error: __('Email must be %s characters or less', globalLimits.fieldLength.email) },
{ result: lengthBody(req.body.log_message, 0, globalLimits.fieldLength.log_message), expected: false, error: __('Modlog message must be %s characters or less', globalLimits.fieldLength.log_message) },
{ result: async () => {
res.locals.post = await Posts.getPost(req.body.board, req.body.postId);
return res.locals.post != null;
}, expected: true, error: 'Post doesn\'t exist' }
}, expected: true, error: __('Post doesn\'t exist') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
});
}
@ -46,8 +48,8 @@ module.exports = {
const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'edit', rateLimitCost.editPost));
if (ratelimitUser > 100 || ratelimitIp > 100) {
return dynamicResponse(req, res, 429, 'message', {
'title': 'Ratelimited',
'error': 'You are editing posts too quickly, please wait a minute and try again',
'title': __('Ratelimited'),
'error': __('You are editing posts too quickly, please wait a minute and try again'),
});
}
}

@ -14,17 +14,19 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.roleid), expected: true, error: 'Missing role id' },
{ result: existsBody(req.body.roleid), expected: true, error: __('Missing role id') },
{ result: async () => {
res.locals.editingRole = await Roles.findOne(req.body.roleid);
return res.locals.editingRole != null && res.locals.editingRole.name !== 'ROOT';
}, blocking: true, expected: true, error: 'You can\'t edit this role' },
}, blocking: true, expected: true, error: __('You can\'t edit this role') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/roles.html`,
});

@ -13,18 +13,20 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' },
{ result: lengthBody(req.body.username, 1, 50), expected: false, error: 'Username must be 50 characters or less' },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: 'Username must contain a-z 0-9 only' },
{ result: (res.locals.board.staff[req.body.username] != null), expected: true, error: 'Invalid staff username' },
{ result: (req.body.username === res.locals.board.owner), expected: false, error: 'You can\'t edit the permissions of the board owner' },
{ result: (res.locals.user.username === req.body.username), expected: false, error: 'You can\'t edit your own permissions' },
{ result: existsBody(req.body.username), expected: true, error: __('Missing username') },
{ result: lengthBody(req.body.username, 1, 50), expected: false, error: __('Username must be 50 characters or less') },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: __('Username must contain a-z 0-9 only') },
{ result: (res.locals.board.staff[req.body.username] != null), expected: true, error: __('Invalid staff username') },
{ result: (req.body.username === res.locals.board.owner), expected: false, error: __('You can\'t edit the permissions of the board owner') },
{ result: (res.locals.user.username === req.body.username), expected: false, error: __('You can\'t edit your own permissions') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
});

@ -20,31 +20,33 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
res.locals.actions = actionChecker(req, res);
const errors = await checkSchema([
{ result: lengthBody(req.body.globalcheckedposts, 1), expected: false, blocking: true, error: 'Must select at least one post' },
{ result: lengthBody(res.locals.actions.validActions, 1), expected: false, blocking: true, error: 'No actions selected' },
{ result: lengthBody(req.body.globalcheckedposts, 1, globalLimits.multiInputs.posts.staff), expected: false, error: `Must not select >${globalLimits.multiInputs.posts.staff} posts per request` },
{ result: (existsBody(req.body.global_report_ban) && !req.body.checkedreports), expected: false, error: 'Must select post and reports to ban reporter' },
{ result: (existsBody(req.body.checkedreports) && !req.body.global_report_ban), expected: false, error: 'Must select a report action if checked reports' },
{ result: (existsBody(req.body.checkedreports) && !req.body.globalcheckedposts), expected: false, error: 'Must check parent post if checking reports for report action' },
{ result: lengthBody(req.body.globalcheckedposts, 1), expected: false, blocking: true, error: __('Must select at least one post') },
{ result: lengthBody(res.locals.actions.validActions, 1), expected: false, blocking: true, error: __('No actions selected') },
{ result: lengthBody(req.body.globalcheckedposts, 1, globalLimits.multiInputs.posts.staff), expected: false, error: __('Must not select >%s posts per request', globalLimits.multiInputs.posts.staff) },
{ result: (existsBody(req.body.global_report_ban) && !req.body.checkedreports), expected: false, error: __('Must select post and reports to ban reporter') },
{ result: (existsBody(req.body.checkedreports) && !req.body.global_report_ban), expected: false, error: __('Must select a report action if checked reports') },
{ result: (existsBody(req.body.checkedreports) && !req.body.globalcheckedposts), expected: false, error: __('Must check parent post if checking reports for report action') },
{ result: (existsBody(req.body.checkedreports) && req.body.globalcheckedposts
&& lengthBody(req.body.checkedreports, 1, req.body.globalcheckedposts.length*5)), expected: false, error: 'Invalid number of reports checked' },
{ result: (res.locals.actions.numGlobal > 0 && res.locals.actions.validActions.length <= res.locals.actions.numGlobal), expected: true, blocking: true, error: 'Invalid actions selected' },
{ result: res.locals.actions.hasPermission, expected: true, blocking: true, error: 'No permission' },
{ result: (existsBody(req.body.edit) && lengthBody(req.body.globalcheckedposts, 1, 1)), expected: false, error: 'Must select only 1 post for edit action' },
{ result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: `Password must be ${globalLimits.fieldLength.postpassword} characters or less` },
{ result: lengthBody(req.body.ban_reason, 0, globalLimits.fieldLength.ban_reason), expected: false, error: `Ban reason must be ${globalLimits.fieldLength.ban_reason} characters or less` },
{ result: lengthBody(req.body.log_message, 0, globalLimits.fieldLength.log_message), expected: false, error: `Modlog message must be ${globalLimits.fieldLength.log_message} characters or less` },
&& lengthBody(req.body.checkedreports, 1, req.body.globalcheckedposts.length*5)), expected: false, error: __('Invalid number of reports checked') },
{ result: (res.locals.actions.numGlobal > 0 && res.locals.actions.validActions.length <= res.locals.actions.numGlobal), expected: true, blocking: true, error: __('Invalid actions selected') },
{ result: res.locals.actions.hasPermission, expected: true, blocking: true, error: __('No permission') },
{ result: (existsBody(req.body.edit) && lengthBody(req.body.globalcheckedposts, 1, 1)), expected: false, error: __('Must select only 1 post for edit action') },
{ result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: __('Password must be %s characters or less', globalLimits.fieldLength.postpassword) },
{ result: lengthBody(req.body.ban_reason, 0, globalLimits.fieldLength.ban_reason), expected: false, error: __('Ban reason must be %s characters or less', globalLimits.fieldLength.ban_reason) },
{ result: lengthBody(req.body.log_message, 0, globalLimits.fieldLength.log_message), expected: false, error: __('Modlog message must be %s characters or less', globalLimits.fieldLength.log_message) },
]);
//return the errors
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/globalmanage/reports.html'
});
@ -58,8 +60,8 @@ module.exports = {
}
if (!res.locals.posts || res.locals.posts.length === 0) {
return dynamicResponse(req, res, 404, 'message', {
'title': 'Not found',
'error': 'Selected posts not found',
'title': __('Not found'),
'error': __('Selected posts not found'),
'redirect': '/globalmanage/reports.html'
});
}

@ -6,6 +6,7 @@ const changeGlobalSettings = require(__dirname+'/../../models/forms/changeglobal
, config = require(__dirname+'/../../lib/misc/config.js')
, { fontPaths } = require(__dirname+'/../../lib/misc/fonts.js')
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
, i18n = require(__dirname+'/../../lib/locale/locale.js')
, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
inArrayBody } = require(__dirname+'/../../lib/input/schema.js');
@ -14,7 +15,7 @@ module.exports = {
paramConverter: paramConverter({
timeFields: ['hot_threads_max_age', 'inactive_account_time', 'ban_duration', 'board_defaults_filter_ban_duration', 'default_ban_duration', 'block_bypass_expire_after_time', 'dnsbl_cache_time', 'board_defaults_delete_protection_age'],
trimFields: ['captcha_options_grid_question', 'captcha_options_grid_trues', 'captcha_options_grid_falses', 'captcha_options_font', 'allowed_hosts', 'dnsbl_blacklists', 'other_mime_types',
'highlight_options_language_subset', 'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'archive_links', 'reverse_links'],
'highlight_options_language_subset', 'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'archive_links', 'reverse_links', 'language'],
numberFields: ['inactive_account_action', 'abandoned_board_action', 'filter_mode', 'auth_level', 'captcha_options_text_wave', 'captcha_options_text_paint', 'captcha_options_text_noise',
'captcha_options_grid_noise', 'captcha_options_grid_edge', 'captcha_options_generate_limit', 'captcha_options_grid_size', 'captcha_options_grid_image_size',
'captcha_options_num_distorts_min', 'captcha_options_num_distorts_max', 'captcha_options_distortion', 'captcha_options_grid_icon_y_offset', 'flood_timers_same_content_same_ip', 'flood_timers_same_content_any_ip',
@ -38,6 +39,8 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
@ -46,7 +49,7 @@ module.exports = {
return /\.[a-z0-9]+/i.test(req.body.thumb_extension);
}
return false;
}, expected: true, error: 'Thumb extension must be like .xxx' },
}, expected: true, error: __('Thumb extension must be like .xxx') },
{ result: () => {
if (req.body.other_mime_types) {
return req.body.other_mime_types
@ -56,155 +59,156 @@ module.exports = {
});
}
return false;
}, expected: false, error: 'Extra mime types must be like type/subtype' },
}, expected: false, error: __('Extra mime types must be like type/subtype') },
{ result: () => {
if (req.body.archive_links) {
/* eslint-disable no-useless-escape */
return /https?\:\/\/[^\s<>\[\]{}|\\^]+%s[^\s<>\[\]{}|\\^]*/i.test(req.body.archive_links);
}
return false;
}, expected: true, error: 'Invalid archive links URL format, must be a link containing %s where the url param belongs.' },
}, expected: true, error: __('Invalid archive links URL format, must be a link containing %s where the url param belongs.') },
{ result: () => {
if (req.body.reverse_links) {
return /https?\:\/\/[^\s<>\[\]{}|\\^]+%s[^\s<>\[\]{}|\\^]*/i.test(req.body.reverse_links);
}
return false;
}, expected: true, error: 'Invalid reverse image search links URL format, must be a link containing %s where the url param belongs.' },
{ result: numberBody(req.body.inactive_account_time), expected: true, error: 'Invalid inactive account time' },
{ result: numberBody(req.body.inactive_account_action, 0, 2), expected: true, error: 'Inactive account action must be a number from 0-2' },
{ result: numberBody(req.body.abandoned_board_action, 0, 3), expected: true, error: 'Abandoned board action must be a number from 0-3' },
{ result: lengthBody(req.body.global_announcement, 0, 10000), expected: false, error: 'Global announcement must not exceed 10000 characters' },
{ result: lengthBody(req.body.filters, 0, 50000), expected: false, error: 'Filter text cannot exceed 50000 characters' },
{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: 'Filter mode must be a number from 0-2' },
{ result: numberBody(req.body.ban_duration), expected: true, error: 'Invalid filter auto ban duration' },
{ result: lengthBody(req.body.allowed_hosts, 0, 10000), expected: false, error: 'Allowed hosts must not exceed 10000 characters' },
{ result: lengthBody(req.body.country_code_header, 0, 100), expected: false, error: 'Country code header length must not exceed 100 characters' },
{ result: lengthBody(req.body.ip_header, 0, 100), expected: false, error: 'IP header length must not exceed 100 characters' },
{ result: lengthBody(req.body.meta_site_name, 0, 100), expected: false, error: 'Meta site name must not exceed 100 characters' },
{ result: lengthBody(req.body.meta_url, 0, 100), expected: false, error: 'Meta url must not exceed 100 characters' },
{ result: inArrayBody(req.body.captcha_options_type, ['grid', 'grid2', 'text', 'google', 'hcaptcha']), expected: true, error: 'Invalid captcha options type' },
{ result: numberBody(req.body.captcha_options_generate_limit, 1), expected: true, error: 'Captcha options generate limit must be a number > 0' },
{ result: numberBody(req.body.captcha_options_grid_size, 2, 6), expected: true, error: 'Captcha options grid size must be a number from 2-6' },
{ result: numberBody(req.body.captcha_options_grid_image_size, 50, 500), expected: true, error: 'Captcha options grid image size must be a number from 50-500' },
{ result: numberBody(req.body.captcha_options_grid_icon_y_offset, 0, 50), expected: true, error: 'Captcha options icon y offset must be a number from 0-50' },
{ result: numberBody(req.body.captcha_options_num_distorts_min, 0, 10), expected: true, error: 'Captcha options min distorts must be a number from 0-10' },
{ result: numberBody(req.body.captcha_options_num_distorts_max, 0, 10), expected: true, error: 'Captcha options max distorts must be a number from 0-10' },
{ result: minmaxBody(req.body.captcha_options_num_distorts_min, req.body.captcha_options_num_distorts_max), expected: true, error: 'Captcha options distorts min must be less than max' },
{ result: numberBody(req.body.captcha_options_distortion, 0, 50), expected: true, error: 'Captcha options distortion must be a number from 0-50' },
{ result: inArrayBody(req.body.captcha_options_font, fontPaths), expected: true, error: 'Invalid captcha options font' },
{ result: numberBody(req.body.captcha_options_text_wave, 0, 10), expected: true, error: 'Captcha options text wave effect strength must be a number form 0-10' },
{ result: numberBody(req.body.captcha_options_text_paint, 0, 10), expected: true, error: 'Captcha options text paint effect strength must be a number from 0-10' },
{ result: numberBody(req.body.captcha_options_text_noise, 0, 10), expected: true, error: 'Captcha options text noise effect strength must be a number from 0-10' },
{ result: numberBody(req.body.captcha_options_grid_noise, 0, 10), expected: true, error: 'Captcha options grid noise effect strength must be a number from 0-10' },
{ result: numberBody(req.body.captcha_options_grid_edge, 0, 50), expected: true, error: 'Captcha options grid edge effect strength must be a number from 0-50' },
{ result: numberBody(req.body.dnsbl_cache_time), expected: true, error: 'Invalid dnsbl cache time' },
{ result: numberBody(req.body.flood_timers_same_content_same_ip), expected: true, error: 'Invalid flood time same content same ip' },
{ result: numberBody(req.body.flood_timers_same_content_any_ip), expected: true, error: 'Invalid flood time same contenet any ip' },
{ result: numberBody(req.body.flood_timers_any_content_same_ip), expected: true, error: 'Invalid flood time any content same ip' },
{ 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.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' },
{ result: numberBody(req.body.hot_threads_limit), expected: true, error: 'Invalid hot threads limit' },
{ result: numberBody(req.body.hot_threads_threshold), expected: true, error: 'Invalid hot threads threshold' },
{ result: numberBody(req.body.hot_threads_max_age), expected: true, error: 'Invalid hot threads max age' },
{ result: numberBody(req.body.overboard_limit), expected: true, error: 'Invalid overboard limit' },
{ result: numberBody(req.body.overboard_catalog_limit), expected: true, error: 'Invalid overboard catalog limit' },
{ result: numberBody(req.body.lock_wait), expected: true, error: 'Invalid lock wait' },
{ result: numberBody(req.body.prune_modlogs), expected: true, error: 'Prune modlogs must be a number of days' },
{ result: numberBody(req.body.prune_ips), expected: true, error: 'Prune ips must be a number of days' },
{ result: lengthBody(req.body.thumb_extension, 1), expected: false, error: 'Thumbnail extension must be at least 1 character' },
{ result: numberBody(req.body.thumb_size), expected: true, error: 'Invalid thumbnail size' },
{ result: numberBody(req.body.video_thumb_percentage, 0, 100), expected: true, error: 'Video thumbnail percentage must be a number from 1-100' },
{ result: numberBody(req.body.default_ban_duration), expected: true, error: 'Invalid default ban duration' },
{ result: numberBody(req.body.quote_limit), expected: true, error: 'Quote limit must be a number' },
{ result: numberBody(req.body.preview_replies), expected: true, error: 'Preview replies must be a number' },
{ result: numberBody(req.body.sticky_preview_replies), expected: true, error: 'Sticky preview replies must be a number' },
{ result: numberBody(req.body.early_404_fraction), expected: true, error: 'Early 404 fraction must be a number' },
{ result: numberBody(req.body.early_404_replies), expected: true, error: 'Early 404 fraction must be a number' },
{ result: numberBody(req.body.max_recent_news), expected: true, error: 'Max recent news must be a number' },
{ result: lengthBody(req.body.space_file_name_replacement, 1, 1), expected: false, error: 'Space file name replacement must be 1 character' },
{ result: lengthBody(req.body.highlight_options_language_subset, 0, 10000), expected: false, error: 'Highlight options language subset must not exceed 10000 characters' },
{ result: lengthBody(req.body.highlight_options_threshold), expected: false, error: 'Highlight options threshold must be a number' },
{ result: numberBody(req.body.global_limits_thread_limit_min), expected: true, error: 'Global thread limit minimum must be a number' },
{ result: numberBody(req.body.global_limits_thread_limit_max), expected: true, error: 'Global thread limit maximum must be a number' },
{ result: minmaxBody(req.body.global_limits_thread_limit_min, req.body.global_limits_thread_limit_max), expected: true, error: 'Global thread limit min must be less than max' },
{ result: numberBody(req.body.global_limits_reply_limit_min), expected: true, error: 'Global reply limit minimum must be a number' },
{ result: numberBody(req.body.global_limits_reply_limit_max), expected: true, error: 'Global reply limit maximum must be a number' },
{ result: minmaxBody(req.body.global_limits_reply_limit_min, req.body.global_limits_reply_limit_max), expected: true, error: 'Global reply limit min must be less than max' },
{ result: numberBody(req.body.global_limits_bump_limit_min), expected: true, error: 'Global bump limit minimum must be a number' },
{ result: numberBody(req.body.global_limits_bump_limit_max), expected: true, error: 'Global bump limit minimum must be a number' },
{ result: minmaxBody(req.body.global_limits_bump_limit_min, req.body.global_limits_bump_limit_max), expected: true, error: 'Global bump limit min must be less than max' },
{ result: numberBody(req.body.global_limits_post_files_max), expected: true, error: 'Post files max must be a number' },
{ result: numberBody(req.body.global_limits_post_files_size_max), expected: true, error: 'Post files size must be a number' },
{ result: numberBody(req.body.global_limits_post_files_size_image_resolution), expected: true, error: 'Image resolution max must be a number' },
{ result: numberBody(req.body.global_limits_post_files_size_video_resolution), expected: true, error: 'Video resolution max must be a number' },
{ result: numberBody(req.body.global_limits_banner_files_width, 1), expected: true, error: 'Banner files height must be a number > 0' },
{ result: numberBody(req.body.global_limits_banner_files_height, 1), expected: true, error: 'Banner files width must be a number > 0' },
{ result: numberBody(req.body.global_limits_banner_files_size_max), expected: true, error: 'Banner files size must be a number' },
{ result: numberBody(req.body.global_limits_banner_files_max), expected: true, error: 'Banner files max must be a number' },
{ result: numberBody(req.body.global_limits_banner_files_total), expected: true, error: 'Banner files total must be a number' },
{ result: numberBody(req.body.global_limits_flag_files_size_max), expected: true, error: 'Flag files size must be a number' },
{ result: numberBody(req.body.global_limits_flag_files_max), expected: true, error: 'Flag files max must be a number' },
{ result: numberBody(req.body.global_limits_flag_files_total), expected: true, error: 'Flag files total must be a number' },
{ result: numberBody(req.body.global_limits_asset_files_size_max), expected: true, error: 'Asset files size must be a number' },
{ result: numberBody(req.body.global_limits_asset_files_max), expected: true, error: 'Asset files max must be a number' },
{ result: numberBody(req.body.global_limits_asset_files_total), expected: true, error: 'Asset files total must be a number' },
{ result: numberBody(req.body.global_limits_field_length_name), expected: true, error: 'Global limit name field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_email), expected: true, error: 'Global limit email field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_subject), expected: true, error: 'Global limit subject field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_postpassword, 20), expected: true, error: 'Global limit postpassword field length must be a number >=20' },
{ result: numberBody(req.body.global_limits_field_length_message), expected: true, error: 'Global limit message field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_report_reason), expected: true, error: 'Global limit report reason field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_ban_reason), expected: true, error: 'Global limit ban reason field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_log_message), expected: true, error: 'Global limit log message field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_uri), expected: true, error: 'Global limit board uri field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_boardname), expected: true, error: 'Global limit board name field length must be a number' },
{ result: numberBody(req.body.global_limits_field_length_description), expected: true, error: 'Global limit board description field length must be a number' },
{ result: numberBody(req.body.global_limits_multi_input_posts_anon), expected: true, error: 'Multi input anon limit must be a number' },
{ result: numberBody(req.body.global_limits_multi_input_posts_staff), expected: true, error: 'Multi input staff limit must be a number' },
{ result: numberBody(req.body.global_limits_custom_css_max), expected: true, error: 'Custom css max must be a number' },
{ result: lengthBody(req.body.global_limits_custom_css_filters, 0, 10000), expected: false, error: 'Custom css filters must not exceed 10000 characters' },
{ result: numberBody(req.body.global_limits_custom_pages_max), expected: true, error: 'Custom pages max must be a number' },
{ result: numberBody(req.body.global_limits_custom_pages_max_length), expected: true, error: 'Custom pages max length must be a number' },
{ result: inArrayBody(req.body.board_defaults_theme, themeHelper.themes), expected: true, error: 'Invalid board default theme' },
{ result: inArrayBody(req.body.board_defaults_code_theme, themeHelper.codeThemes), expected: true, error: 'Invalid board default code theme' },
{ result: numberBody(req.body.board_defaults_lock_mode, 0, 2), expected: true, error: 'Board default lock mode must be a number from 0-2' },
{ result: numberBody(req.body.board_defaults_file_r9k_mode, 0, 2), expected: true, error: 'Board default file r9k mode must be a number from 0-2' },
{ result: numberBody(req.body.board_defaults_message_r9k_mode, 0, 2), expected: true, error: 'Board default message r9k mode must be a number from 0-2' },
{ result: numberBody(req.body.board_defaults_captcha_mode, 0, 2), expected: true, error: 'Board default captcha mode must be a number from 0-2' },
{ result: numberBody(req.body.board_defaults_tph_trigger), expected: true, error: 'Board default tph trigger must be a number' },
{ result: numberBody(req.body.board_defaults_pph_trigger), expected: true, error: 'Board default pph trigger must be a number' },
{ result: numberBody(req.body.board_defaults_pph_trigger_action, 0, 4), expected: true, error: 'Board default pph trigger action must be a number from 0-4' },
{ result: numberBody(req.body.board_defaults_tph_trigger_action, 0, 4), expected: true, error: 'Board default tph trigger action must be a number from 0-4' },
{ result: numberBody(req.body.board_defaults_captcha_reset, 0, 2), expected: true, error: 'Board defaults captcha reset must be a number from 0-2' },
{ result: numberBody(req.body.board_defaults_lock_reset, 0, 2), expected: true, error: 'Board defaults lock reset must be a number from 0-2' },
{ result: numberBodyVariable(req.body.board_defaults_reply_limit, globalLimits.replyLimit.min, req.body.global_limits_reply_limit_min, globalLimits.replyLimit.max, req.body.global_limits_reply_limit_max), expected: true, error: 'Board defaults reply limit must be within global limits' },
{ result: numberBodyVariable(req.body.board_defaults_thread_limit, globalLimits.threadLimit.min, req.body.global_limits_thread_limit_min, globalLimits.threadLimit.max, req.body.global_limits_thread_limit_max), expected: true, error: 'Board defaults thread limit must be within global limits' },
{ result: numberBodyVariable(req.body.board_defaults_bump_limit, globalLimits.bumpLimit.min, req.body.global_limits_bump_limit_min, globalLimits.bumpLimit.max, req.body.global_limits_bump_limit_max), expected: true, error: 'Board defaults bump limit must be within global limits' },
{ result: numberBodyVariable(req.body.board_defaults_max_files, 0, 0, globalLimits.postFiles.max, req.body.global_limits_post_files_max), expected: true, error: 'Board defaults max files must be within global limits' },
{ result: numberBodyVariable(req.body.board_defaults_max_thread_message_length, 0, 0, globalLimits.fieldLength.message, req.body.global_limits_field_length_message), expected: true, error: 'Board defaults max thread message length must be within global limits' },
{ result: numberBodyVariable(req.body.board_defaults_max_reply_message_length, 0, 0, globalLimits.fieldLength.message, req.body.global_limits_field_length_message), expected: true, error: 'Board defaults max reply message length must be within global limits' },
{ result: numberBody(req.body.board_defaults_min_thread_message_length), expected: true, error: 'Board defaults min thread message length must be a number' },
{ result: numberBody(req.body.board_defaults_min_reply_message_length), expected: true, error: 'Board defaults min reply message length must be a number' },
{ result: minmaxBody(req.body.board_defaults_min_thread_message_length, req.body.board_defaults_max_thread_message_length), expected: true, error: 'Board defaults thread message length min must be less than max' },
{ result: minmaxBody(req.body.board_defaults_min_reply_message_length, req.body.board_defaults_max_reply_message_length), expected: true, error: 'Board defaults reply message length min must be less than max' },
{ result: numberBody(req.body.board_defaults_filter_mode, 0, 2), expected: true, error: 'Board defaults filter mode must be a number from 0-2' },
{ result: numberBody(req.body.frontend_script_default_volume, 0, 100), expected: true, error: 'Default volume must be a number from 0-100' },
{ result: numberBody(req.body.frontend_script_default_tegaki_width), expected: true, error: 'Tegaki width must be a number' },
{ result: numberBody(req.body.frontend_script_default_tegaki_height), expected: true, error: 'Tegaki height must be a number' },
{ result: numberBody(req.body.board_defaults_filter_ban_duration), expected: true, error: 'Board defaults filter ban duration must be a number' },
{ result: numberBody(req.body.board_defaults_delete_protection_age, 0), expected: true, error: 'Invalid board defaults OP thread age delete protection' },
{ result: numberBody(req.body.board_defaults_delete_protection_count, 0), expected: true, error: 'Invalid board defaults OP thread reply count delete protection' },
{ result: lengthBody(req.body.webring_following, 0, 10000), expected: false, error: 'Webring following list must not exceed 10000 characters' },
{ result: lengthBody(req.body.webring_blacklist, 0, 10000), expected: false, error: 'Webring blacklist must not exceed 10000 characters' },
{ result: lengthBody(req.body.webring_logos, 0, 10000), expected: false, error: 'Webring logos list must not exceed 10000 characters' },
}, expected: true, error: __('Invalid reverse image search links URL format, must be a link containing %s where the url param belongs.') },
{ result: numberBody(req.body.inactive_account_time), expected: true, error: __('Invalid inactive account time') },
{ result: numberBody(req.body.inactive_account_action, 0, 2), expected: true, error: __('Inactive account action must be a number from 0-2') },
{ result: numberBody(req.body.abandoned_board_action, 0, 3), expected: true, error: __('Abandoned board action must be a number from 0-3') },
{ result: lengthBody(req.body.global_announcement, 0, 10000), expected: false, error: __('Global announcement must not exceed 10000 characters') },
{ result: lengthBody(req.body.filters, 0, 50000), expected: false, error: __('Filter text cannot exceed 50000 characters') },
{ result: numberBody(req.body.filter_mode, 0, 2), expected: true, error: __('Filter mode must be a number from 0-2') },
{ result: numberBody(req.body.ban_duration), expected: true, error: __('Invalid filter auto ban duration') },
{ result: lengthBody(req.body.allowed_hosts, 0, 10000), expected: false, error: __('Allowed hosts must not exceed 10000 characters') },
{ result: lengthBody(req.body.country_code_header, 0, 100), expected: false, error: __('Country code header length must not exceed 100 characters') },
{ result: lengthBody(req.body.ip_header, 0, 100), expected: false, error: __('IP header length must not exceed 100 characters') },
{ result: lengthBody(req.body.meta_site_name, 0, 100), expected: false, error: __('Meta site name must not exceed 100 characters') },
{ result: lengthBody(req.body.meta_url, 0, 100), expected: false, error: __('Meta url must not exceed 100 characters') },
{ result: inArrayBody(req.body.language, i18n.getLocales()), expected: true, error: __('Invalid language') },
{ result: inArrayBody(req.body.captcha_options_type, ['grid', 'grid2', 'text', 'google', 'hcaptcha']), expected: true, error: __('Invalid captcha options type') },
{ result: numberBody(req.body.captcha_options_generate_limit, 1), expected: true, error: __('Captcha options generate limit must be a number > 0') },
{ result: numberBody(req.body.captcha_options_grid_size, 2, 6), expected: true, error: __('Captcha options grid size must be a number from 2-6') },
{ result: numberBody(req.body.captcha_options_grid_image_size, 50, 500), expected: true, error: __('Captcha options grid image size must be a number from 50-500') },
{ result: numberBody(req.body.captcha_options_grid_icon_y_offset, 0, 50), expected: true, error: __('Captcha options icon y offset must be a number from 0-50') },
{ result: numberBody(req.body.captcha_options_num_distorts_min, 0, 10), expected: true, error: __('Captcha options min distorts must be a number from 0-10') },
{ result: numberBody(req.body.captcha_options_num_distorts_max, 0, 10), expected: true, error: __('Captcha options max distorts must be a number from 0-10') },
{ result: minmaxBody(req.body.captcha_options_num_distorts_min, req.body.captcha_options_num_distorts_max), expected: true, error: __('Captcha options distorts min must be less than max') },
{ result: numberBody(req.body.captcha_options_distortion, 0, 50), expected: true, error: __('Captcha options distortion must be a number from 0-50') },
{ result: inArrayBody(req.body.captcha_options_font, fontPaths), expected: true, error: __('Invalid captcha options font') },
{ result: numberBody(req.body.captcha_options_text_wave, 0, 10), expected: true, error: __('Captcha options text wave effect strength must be a number form 0-10') },
{ result: numberBody(req.body.captcha_options_text_paint, 0, 10), expected: true, error: __('Captcha options text paint effect strength must be a number from 0-10') },
{ result: numberBody(req.body.captcha_options_text_noise, 0, 10), expected: true, error: __('Captcha options text noise effect strength must be a number from 0-10') },
{ result: numberBody(req.body.captcha_options_grid_noise, 0, 10), expected: true, error: __('Captcha options grid noise effect strength must be a number from 0-10') },
{ result: numberBody(req.body.captcha_options_grid_edge, 0, 50), expected: true, error: __('Captcha options grid edge effect strength must be a number from 0-50') },
{ result: numberBody(req.body.dnsbl_cache_time), expected: true, error: __('Invalid dnsbl cache time') },
{ result: numberBody(req.body.flood_timers_same_content_same_ip), expected: true, error: __('Invalid flood time same content same ip') },
{ result: numberBody(req.body.flood_timers_same_content_any_ip), expected: true, error: __('Invalid flood time same contenet any ip') },
{ result: numberBody(req.body.flood_timers_any_content_same_ip), expected: true, error: __('Invalid flood time any content same ip') },
{ 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.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') },
{ result: numberBody(req.body.hot_threads_limit), expected: true, error: __('Invalid hot threads limit') },
{ result: numberBody(req.body.hot_threads_threshold), expected: true, error: __('Invalid hot threads threshold') },
{ result: numberBody(req.body.hot_threads_max_age), expected: true, error: __('Invalid hot threads max age') },
{ result: numberBody(req.body.overboard_limit), expected: true, error: __('Invalid overboard limit') },
{ result: numberBody(req.body.overboard_catalog_limit), expected: true, error: __('Invalid overboard catalog limit') },
{ result: numberBody(req.body.lock_wait), expected: true, error: __('Invalid lock wait') },
{ result: numberBody(req.body.prune_modlogs), expected: true, error: __('Prune modlogs must be a number of days') },
{ result: numberBody(req.body.prune_ips), expected: true, error: __('Prune ips must be a number of days') },
{ result: lengthBody(req.body.thumb_extension, 1), expected: false, error: __('Thumbnail extension must be at least 1 character') },
{ result: numberBody(req.body.thumb_size), expected: true, error: __('Invalid thumbnail size') },
{ result: numberBody(req.body.video_thumb_percentage, 0, 100), expected: true, error: __('Video thumbnail percentage must be a number from 1-100') },
{ result: numberBody(req.body.default_ban_duration), expected: true, error: __('Invalid default ban duration') },
{ result: numberBody(req.body.quote_limit), expected: true, error: __('Quote limit must be a number') },
{ result: numberBody(req.body.preview_replies), expected: true, error: __('Preview replies must be a number') },
{ result: numberBody(req.body.sticky_preview_replies), expected: true, error: __('Sticky preview replies must be a number') },
{ result: numberBody(req.body.early_404_fraction), expected: true, error: __('Early 404 fraction must be a number') },
{ result: numberBody(req.body.early_404_replies), expected: true, error: __('Early 404 fraction must be a number') },
{ result: numberBody(req.body.max_recent_news), expected: true, error: __('Max recent news must be a number') },
{ result: lengthBody(req.body.space_file_name_replacement, 1, 1), expected: false, error: __('Space file name replacement must be 1 character') },
{ result: lengthBody(req.body.highlight_options_language_subset, 0, 10000), expected: false, error: __('Highlight options language subset must not exceed 10000 characters') },
{ result: lengthBody(req.body.highlight_options_threshold), expected: false, error: __('Highlight options threshold must be a number') },
{ result: numberBody(req.body.global_limits_thread_limit_min), expected: true, error: __('Global thread limit minimum must be a number') },
{ result: numberBody(req.body.global_limits_thread_limit_max), expected: true, error: __('Global thread limit maximum must be a number') },
{ result: minmaxBody(req.body.global_limits_thread_limit_min, req.body.global_limits_thread_limit_max), expected: true, error: __('Global thread limit min must be less than max') },
{ result: numberBody(req.body.global_limits_reply_limit_min), expected: true, error: __('Global reply limit minimum must be a number') },
{ result: numberBody(req.body.global_limits_reply_limit_max), expected: true, error: __('Global reply limit maximum must be a number') },
{ result: minmaxBody(req.body.global_limits_reply_limit_min, req.body.global_limits_reply_limit_max), expected: true, error: __('Global reply limit min must be less than max') },
{ result: numberBody(req.body.global_limits_bump_limit_min), expected: true, error: __('Global bump limit minimum must be a number') },
{ result: numberBody(req.body.global_limits_bump_limit_max), expected: true, error: __('Global bump limit minimum must be a number') },
{ result: minmaxBody(req.body.global_limits_bump_limit_min, req.body.global_limits_bump_limit_max), expected: true, error: __('Global bump limit min must be less than max') },
{ result: numberBody(req.body.global_limits_post_files_max), expected: true, error: __('Post files max must be a number') },
{ result: numberBody(req.body.global_limits_post_files_size_max), expected: true, error: __('Post files size must be a number') },
{ result: numberBody(req.body.global_limits_post_files_size_image_resolution), expected: true, error: __('Image resolution max must be a number') },
{ result: numberBody(req.body.global_limits_post_files_size_video_resolution), expected: true, error: __('Video resolution max must be a number') },
{ result: numberBody(req.body.global_limits_banner_files_width, 1), expected: true, error: __('Banner files height must be a number > 0') },
{ result: numberBody(req.body.global_limits_banner_files_height, 1), expected: true, error: __('Banner files width must be a number > 0') },
{ result: numberBody(req.body.global_limits_banner_files_size_max), expected: true, error: __('Banner files size must be a number') },
{ result: numberBody(req.body.global_limits_banner_files_max), expected: true, error: __('Banner files max must be a number') },
{ result: numberBody(req.body.global_limits_banner_files_total), expected: true, error: __('Banner files total must be a number') },
{ result: numberBody(req.body.global_limits_flag_files_size_max), expected: true, error: __('Flag files size must be a number') },
{ result: numberBody(req.body.global_limits_flag_files_max), expected: true, error: __('Flag files max must be a number') },
{ result: numberBody(req.body.global_limits_flag_files_total), expected: true, error: __('Flag files total must be a number') },
{ result: numberBody(req.body.global_limits_asset_files_size_max), expected: true, error: __('Asset files size must be a number') },
{ result: numberBody(req.body.global_limits_asset_files_max), expected: true, error: __('Asset files max must be a number') },
{ result: numberBody(req.body.global_limits_asset_files_total), expected: true, error: __('Asset files total must be a number') },
{ result: numberBody(req.body.global_limits_field_length_name), expected: true, error: __('Global limit name field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_email), expected: true, error: __('Global limit email field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_subject), expected: true, error: __('Global limit subject field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_postpassword, 20), expected: true, error: __('Global limit postpassword field length must be a number >=20') },
{ result: numberBody(req.body.global_limits_field_length_message), expected: true, error: __('Global limit message field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_report_reason), expected: true, error: __('Global limit report reason field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_ban_reason), expected: true, error: __('Global limit ban reason field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_log_message), expected: true, error: __('Global limit log message field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_uri), expected: true, error: __('Global limit board uri field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_boardname), expected: true, error: __('Global limit board name field length must be a number') },
{ result: numberBody(req.body.global_limits_field_length_description), expected: true, error: __('Global limit board description field length must be a number') },
{ result: numberBody(req.body.global_limits_multi_input_posts_anon), expected: true, error: __('Multi input anon limit must be a number') },
{ result: numberBody(req.body.global_limits_multi_input_posts_staff), expected: true, error: __('Multi input staff limit must be a number') },
{ result: numberBody(req.body.global_limits_custom_css_max), expected: true, error: __('Custom css max must be a number') },
{ result: lengthBody(req.body.global_limits_custom_css_filters, 0, 10000), expected: false, error: __('Custom css filters must not exceed 10000 characters') },
{ result: numberBody(req.body.global_limits_custom_pages_max), expected: true, error: __('Custom pages max must be a number') },
{ result: numberBody(req.body.global_limits_custom_pages_max_length), expected: true, error: __('Custom pages max length must be a number') },
{ result: inArrayBody(req.body.board_defaults_theme, themeHelper.themes), expected: true, error: __('Invalid board default theme') },
{ result: inArrayBody(req.body.board_defaults_code_theme, themeHelper.codeThemes), expected: true, error: __('Invalid board default code theme') },
{ result: numberBody(req.body.board_defaults_lock_mode, 0, 2), expected: true, error: __('Board default lock mode must be a number from 0-2') },
{ result: numberBody(req.body.board_defaults_file_r9k_mode, 0, 2), expected: true, error: __('Board default file r9k mode must be a number from 0-2') },
{ result: numberBody(req.body.board_defaults_message_r9k_mode, 0, 2), expected: true, error: __('Board default message r9k mode must be a number from 0-2') },
{ result: numberBody(req.body.board_defaults_captcha_mode, 0, 2), expected: true, error: __('Board default captcha mode must be a number from 0-2') },
{ result: numberBody(req.body.board_defaults_tph_trigger), expected: true, error: __('Board default tph trigger must be a number') },
{ result: numberBody(req.body.board_defaults_pph_trigger), expected: true, error: __('Board default pph trigger must be a number') },
{ result: numberBody(req.body.board_defaults_pph_trigger_action, 0, 4), expected: true, error: __('Board default pph trigger action must be a number from 0-4') },
{ result: numberBody(req.body.board_defaults_tph_trigger_action, 0, 4), expected: true, error: __('Board default tph trigger action must be a number from 0-4') },
{ result: numberBody(req.body.board_defaults_captcha_reset, 0, 2), expected: true, error: __('Board defaults captcha reset must be a number from 0-2') },
{ result: numberBody(req.body.board_defaults_lock_reset, 0, 2), expected: true, error: __('Board defaults lock reset must be a number from 0-2') },
{ result: numberBodyVariable(req.body.board_defaults_reply_limit, globalLimits.replyLimit.min, req.body.global_limits_reply_limit_min, globalLimits.replyLimit.max, req.body.global_limits_reply_limit_max), expected: true, error: __('Board defaults reply limit must be within global limits') },
{ result: numberBodyVariable(req.body.board_defaults_thread_limit, globalLimits.threadLimit.min, req.body.global_limits_thread_limit_min, globalLimits.threadLimit.max, req.body.global_limits_thread_limit_max), expected: true, error: __('Board defaults thread limit must be within global limits') },
{ result: numberBodyVariable(req.body.board_defaults_bump_limit, globalLimits.bumpLimit.min, req.body.global_limits_bump_limit_min, globalLimits.bumpLimit.max, req.body.global_limits_bump_limit_max), expected: true, error: __('Board defaults bump limit must be within global limits') },
{ result: numberBodyVariable(req.body.board_defaults_max_files, 0, 0, globalLimits.postFiles.max, req.body.global_limits_post_files_max), expected: true, error: __('Board defaults max files must be within global limits') },
{ result: numberBodyVariable(req.body.board_defaults_max_thread_message_length, 0, 0, globalLimits.fieldLength.message, req.body.global_limits_field_length_message), expected: true, error: __('Board defaults max thread message length must be within global limits') },
{ result: numberBodyVariable(req.body.board_defaults_max_reply_message_length, 0, 0, globalLimits.fieldLength.message, req.body.global_limits_field_length_message), expected: true, error: __('Board defaults max reply message length must be within global limits') },
{ result: numberBody(req.body.board_defaults_min_thread_message_length), expected: true, error: __('Board defaults min thread message length must be a number') },
{ result: numberBody(req.body.board_defaults_min_reply_message_length), expected: true, error: __('Board defaults min reply message length must be a number') },
{ result: minmaxBody(req.body.board_defaults_min_thread_message_length, req.body.board_defaults_max_thread_message_length), expected: true, error: __('Board defaults thread message length min must be less than max') },
{ result: minmaxBody(req.body.board_defaults_min_reply_message_length, req.body.board_defaults_max_reply_message_length), expected: true, error: __('Board defaults reply message length min must be less than max') },
{ result: numberBody(req.body.board_defaults_filter_mode, 0, 2), expected: true, error: __('Board defaults filter mode must be a number from 0-2') },
{ result: numberBody(req.body.frontend_script_default_volume, 0, 100), expected: true, error: __('Default volume must be a number from 0-100') },
{ result: numberBody(req.body.frontend_script_default_tegaki_width), expected: true, error: __('Tegaki width must be a number') },
{ result: numberBody(req.body.frontend_script_default_tegaki_height), expected: true, error: __('Tegaki height must be a number') },
{ result: numberBody(req.body.board_defaults_filter_ban_duration), expected: true, error: __('Board defaults filter ban duration must be a number') },
{ result: numberBody(req.body.board_defaults_delete_protection_age, 0), expected: true, error: __('Invalid board defaults OP thread age delete protection') },
{ result: numberBody(req.body.board_defaults_delete_protection_count, 0), expected: true, error: __('Invalid board defaults OP thread reply count delete protection') },
{ result: lengthBody(req.body.webring_following, 0, 10000), expected: false, error: __('Webring following list must not exceed 10000 characters') },
{ result: lengthBody(req.body.webring_blacklist, 0, 10000), expected: false, error: __('Webring blacklist must not exceed 10000 characters') },
{ result: lengthBody(req.body.webring_logos, 0, 10000), expected: false, error: __('Webring logos list must not exceed 10000 characters') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/globalmanage/settings.html'
});

@ -13,17 +13,19 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' },
{ result: existsBody(req.body.password), expected: true, error: 'Missing password' },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: 'Username must be 1-50 characters' },
{ result: lengthBody(req.body.password, 0, 100), expected: false, error: 'Password must be 1-100 characters' },
{ result: lengthBody(req.body.twofactor, 0, 6), expected: false, error: 'Invalid 2FA code' },
{ result: existsBody(req.body.username), expected: true, error: __('Missing username') },
{ result: existsBody(req.body.password), expected: true, error: __('Missing password') },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: __('Username must be 1-50 characters') },
{ result: lengthBody(req.body.password, 0, 100), expected: false, error: __('Password must be 1-100 characters') },
{ result: lengthBody(req.body.twofactor, 0, 6), expected: false, error: __('Invalid 2FA code') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/login.html'
});

@ -20,39 +20,41 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits, disableAnonymizerFilePosting } = config.get;
const hasNoMandatoryFile = globalLimits.postFiles.max !== 0 && res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0;
//maybe add more duplicates here?
const errors = await checkSchema([
{ result: (lengthBody(req.body.message, 1) && res.locals.numFiles === 0), expected: false, error: 'Posts must include a message or file' },
{ result: (lengthBody(req.body.message, 1) && res.locals.numFiles === 0), expected: false, error: __('Posts must include a message or file') },
{ result: (res.locals.anonymizer && (disableAnonymizerFilePosting || res.locals.board.settings.disableAnonymizerFilePosting)
&& res.locals.numFiles > 0), expected: false, error: `Posting files through anonymizers has been disabled ${disableAnonymizerFilePosting ? 'globally' : 'on this board'}` },
{ result: res.locals.numFiles > res.locals.board.settings.maxFiles, blocking: true, expected: false, error: `Too many files. Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}` },
&& res.locals.numFiles > 0), expected: false, error: __(`Posting files through anonymizers has been disabled ${disableAnonymizerFilePosting ? 'globally' : 'on this board'}`) },
{ result: res.locals.numFiles > res.locals.board.settings.maxFiles, blocking: true, expected: false, error: __(`Too many files. Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is %s`, res.locals.board.settings.maxFiles) },
{ result: (lengthBody(req.body.subject, 1) && (!existsBody(req.body.thread)
&& res.locals.board.settings.forceThreadSubject)), expected: false, error: 'Threads must include a subject' },
&& res.locals.board.settings.forceThreadSubject)), expected: false, error: __('Threads must include a subject') },
{ result: lengthBody(req.body.message, 1) && (!existsBody(req.body.thread)
&& res.locals.board.settings.forceThreadMessage), expected: false, error: 'Threads must include a message' },
&& res.locals.board.settings.forceThreadMessage), expected: false, error: __('Threads must include a message') },
{ result: lengthBody(req.body.message, 1) && (existsBody(req.body.thread)
&& res.locals.board.settings.forceReplyMessage), expected: false, error: 'Replies must include a message' },
{ result: hasNoMandatoryFile && !existsBody(req.body.thread) && res.locals.board.settings.forceThreadFile , expected: false, error: 'Threads must include a file' },
{ result: hasNoMandatoryFile && existsBody(req.body.thread) && res.locals.board.settings.forceReplyFile , expected: false, error: 'Replies must include a file' },
{ result: lengthBody(req.body.message, 0, globalLimits.fieldLength.message), expected: false, blocking: true, error: `Message must be ${globalLimits.fieldLength.message} characters or less` },
&& res.locals.board.settings.forceReplyMessage), expected: false, error: __('Replies must include a message') },
{ result: hasNoMandatoryFile && !existsBody(req.body.thread) && res.locals.board.settings.forceThreadFile , expected: false, error: __('Threads must include a file') },
{ result: hasNoMandatoryFile && existsBody(req.body.thread) && res.locals.board.settings.forceReplyFile , expected: false, error: __('Replies must include a file') },
{ result: lengthBody(req.body.message, 0, globalLimits.fieldLength.message), expected: false, blocking: true, error: __('Message must be %s characters or less', globalLimits.fieldLength.message) },
{ result: existsBody(req.body.message) && existsBody(req.body.thread) && lengthBody(req.body.message, res.locals.board.settings.minReplyMessageLength, res.locals.board.settings.maxReplyMessageLength),
expected: false, error: `Reply messages must be ${res.locals.board.settings.minReplyMessageLength}-${res.locals.board.settings.maxReplyMessageLength} characters` },
expected: false, error: __('Reply messages must be %s-%s characters', res.locals.board.settings.minReplyMessageLength, res.locals.board.settings.maxReplyMessageLength) },
{ result: existsBody(req.body.message) && !existsBody(req.body.thread) && lengthBody(req.body.message, res.locals.board.settings.minThreadMessageLength, res.locals.board.settings.maxThreadMessageLength),
expected: false, error: `Thread messages must be ${res.locals.board.settings.minThreadMessageLength}-${res.locals.board.settings.maxThreadMessageLength} characters` },
{ result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: `Password must be ${globalLimits.fieldLength.postpassword} characters or less` },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.name), expected: false, error: `Name must be ${globalLimits.fieldLength.name} characters or less` },
{ result: lengthBody(req.body.subject, 0, globalLimits.fieldLength.subject), expected: false, error: `Subject must be ${globalLimits.fieldLength.subject} characters or less` },
{ result: lengthBody(req.body.email, 0, globalLimits.fieldLength.email), expected: false, error: `Email must be ${globalLimits.fieldLength.email} characters or less` },
expected: false, error: __('Thread messages must be %s-%s characters', res.locals.board.settings.minThreadMessageLength, res.locals.board.settings.maxThreadMessageLength) },
{ result: lengthBody(req.body.postpassword, 0, globalLimits.fieldLength.postpassword), expected: false, error: __('Password must be %s characters or less', globalLimits.fieldLength.postpassword) },
{ result: lengthBody(req.body.name, 0, globalLimits.fieldLength.name), expected: false, error: __('Name must be %s characters or less', globalLimits.fieldLength.name) },
{ result: lengthBody(req.body.subject, 0, globalLimits.fieldLength.subject), expected: false, error: __('Subject must be %s characters or less', globalLimits.fieldLength.subject) },
{ result: lengthBody(req.body.email, 0, globalLimits.fieldLength.email), expected: false, error: __('Email must be %s characters or less', globalLimits.fieldLength.email) },
]);
if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread + '.html' : ''}`
});

@ -14,21 +14,23 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: res.locals.permissions.get(Permissions.CREATE_ACCOUNT), blocking: true, expected: true, error: 'No permission' },
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: 'Username must be 50 characters or less' },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: 'Username must contain a-z 0-9 only'},
{ result: existsBody(req.body.password), expected: true, error: 'Missing password' },
{ result: lengthBody(req.body.password, 0, 50), expected: false, error: 'Password must be 50 characters or less' },
{ result: existsBody(req.body.passwordconfirm), expected: true, error: 'Missing password confirmation' },
{ result: lengthBody(req.body.passwordconfirm, 0, 100), expected: false, error: 'Password confirmation must be 100 characters or less' },
{ result: (req.body.password === req.body.passwordconfirm), expected: true, error: 'Password and password confirmation must match' },
{ result: res.locals.permissions.get(Permissions.CREATE_ACCOUNT), blocking: true, expected: true, error: __('No permission') },
{ result: existsBody(req.body.username), expected: true, error: __('Missing username') },
{ result: lengthBody(req.body.username, 0, 50), expected: false, error: __('Username must be 50 characters or less') },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: __('Username must contain a-z 0-9 only') },
{ result: existsBody(req.body.password), expected: true, error: __('Missing password') },
{ result: lengthBody(req.body.password, 0, 50), expected: false, error: __('Password must be 50 characters or less') },
{ result: existsBody(req.body.passwordconfirm), expected: true, error: __('Missing password confirmation') },
{ result: lengthBody(req.body.passwordconfirm, 0, 100), expected: false, error: __('Password confirmation must be 100 characters or less') },
{ result: (req.body.password === req.body.passwordconfirm), expected: true, error: __('Password and password confirmation must match') },
], res.locals.permissions);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/register.html'
});

@ -14,19 +14,21 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.confirm), expected: true, error: 'Missing confirmation' },
{ result: existsBody(req.body.board), expected: true, error: 'You did not select a board' },
{ result: alphaNumericRegex.test(req.body.board), expected: true, error: 'URI must contain a-z 0-9 only' },
{ result: existsBody(req.body.confirm), expected: true, error: __('Missing confirmation') },
{ result: existsBody(req.body.board), expected: true, error: __('You did not select a board') },
{ result: alphaNumericRegex.test(req.body.board), expected: true, error: __('URI must contain a-z 0-9 only') },
{ result: async () => {
res.locals.board = await Boards.findOne(req.body.board);
return res.locals.board != null;
}, expected: true, error: `Board /${req.body.board}/ does not exist` },
}, expected: true, error: __('Board /%s/ does not exist', req.body.board) },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/account.html'
});

@ -14,20 +14,22 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing new owner username' },
{ result: lengthBody(req.body.username, 1, 50), expected: false, error: 'New owner username must be 50 characters or less' },
{ result: (req.body.username === res.locals.board.owner), expected: false, error: 'New owner must be different from current owner' },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: 'New owner username must contain a-z 0-9 only' },
{ result: existsBody(req.body.username), expected: true, error: __('Missing new owner username') },
{ result: lengthBody(req.body.username, 1, 50), expected: false, error: __('New owner username must be 50 characters or less') },
{ result: (req.body.username === res.locals.board.owner), expected: false, error: __('New owner must be different from current owner') },
{ result: alphaNumericRegex.test(req.body.username), expected: true, error: __('New owner username must contain a-z 0-9 only') },
{ result: async () => {
res.locals.newOwner = await Accounts.findOne(req.body.username.toLowerCase());
return res.locals.newOwner != null;
}, expected: true, error: 'Cannot transfer to account that does not exist' },
}, expected: true, error: __('Cannot transfer to account that does not exist') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/settings.html`
});

@ -13,15 +13,17 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([
{ result: res.locals.user.twofactor === false, expected: true, error: 'You already have 2FA setup' },
{ result: existsBody(req.body.twofactor), expected: true, error: 'Missing 2FA code' },
{ result: lengthBody(req.body.twofactor, 6, 6), expected: false, error: '2FA code must be 6 characters' },
{ result: res.locals.user.twofactor === false, expected: true, error: __('You already have 2FA setup') },
{ result: existsBody(req.body.twofactor), expected: true, error: __('Missing 2FA code') },
{ result: lengthBody(req.body.twofactor, 6, 6), expected: false, error: __('2FA code must be 6 characters') },
]);
if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': '/twofactor.html'
});

@ -12,18 +12,20 @@ module.exports = {
controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
const errors = await checkSchema([
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: 'Must provide a file' },
{ result: numberBody(res.locals.numFiles, 0, globalLimits.bannerFiles.max), expected: true, error: `Exceeded max banner uploads in one request of ${globalLimits.bannerFiles.max}` },
{ result: numberBody(res.locals.board.banners.length+res.locals.numFiles, 0, globalLimits.bannerFiles.total), expected: true, error: `Total number of banners would exceed global limit of ${globalLimits.bannerFiles.total}` },
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: __('Must provide a file') },
{ result: numberBody(res.locals.numFiles, 0, globalLimits.bannerFiles.max), expected: true, error: __('Exceeded max banner uploads in one request of %s', globalLimits.bannerFiles.max) },
{ result: numberBody(res.locals.board.banners.length+res.locals.numFiles, 0, globalLimits.bannerFiles.total), expected: true, error: __('Total number of banners would exceed global limit of %s', globalLimits.bannerFiles.total) },
]);
if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'title': __('Bad request'),
'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html`
});

@ -16,6 +16,7 @@ const express = require('express')
, sessionRefresh = require(__dirname+'/../lib/middleware/permission/sessionrefresh.js')
, csrf = require(__dirname+'/../lib/middleware/misc/csrfmiddleware.js')
, setMinimal = require(__dirname+'/../lib/middleware/misc/setminimal.js')
, { setBoardLanguage, setQueryLanguage } = require(__dirname+'/../lib/middleware/locale/locale.js')
//page models
, { manageRecent, manageReports, manageAssets, manageSettings, manageBans, editCustomPage, manageMyPermissions,
manageBoard, manageThread, manageLogs, manageCatalog, manageCustomPages, manageStaff, editStaff, editPost } = require(__dirname+'/../models/pages/manage/')
@ -44,47 +45,47 @@ router.get('/overboard.(html|json)', overboard); //overboard
router.get('/catalog.(html|json)', overboardCatalog); //overboard catalog view
//board pages
router.get('/:board/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).(html|json)', Boards.exists, board); //index
router.get('/:board/thread/:id([1-9][0-9]{0,}).(html|json)', Boards.exists, threadParamConverter, Posts.threadExistsMiddleware, thread); //thread view
router.get('/:board/catalog.(html|json)', Boards.exists, catalog); //catalog
router.get('/:board/logs.(html|json)', Boards.exists, modloglist);//modlog list
router.get('/:board/logs/:date(\\d{2}-\\d{2}-\\d{4}).(html|json)', Boards.exists, logParamConverter, modlog); //daily log
router.get('/:board/custompage/:page.(html|json)', Boards.exists, customPage); //board custom page
router.get('/:board/banners.(html|json)', Boards.exists, banners); //banners
router.get('/:board/settings.json', Boards.exists, boardSettings); //public board settings
router.get('/:board/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).(html|json)', Boards.exists, setBoardLanguage, board); //index
router.get('/:board/thread/:id([1-9][0-9]{0,}).(html|json)', Boards.exists, setBoardLanguage, threadParamConverter, Posts.threadExistsMiddleware, thread); //thread view
router.get('/:board/catalog.(html|json)', Boards.exists, setBoardLanguage, catalog); //catalog
router.get('/:board/logs.(html|json)', Boards.exists, setBoardLanguage, modloglist);//modlog list
router.get('/:board/logs/:date(\\d{2}-\\d{2}-\\d{4}).(html|json)', Boards.exists, setBoardLanguage, logParamConverter, modlog); //daily log
router.get('/:board/custompage/:page.(html|json)', Boards.exists, setBoardLanguage, customPage); //board custom page
router.get('/:board/banners.(html|json)', Boards.exists, setBoardLanguage, banners); //banners
router.get('/:board/settings.json', Boards.exists, setBoardLanguage, boardSettings); //public board settings
router.get('/settings.json', globalSettings); //public global settings
router.get('/randombanner', randombanner); //random banner
//board manage pages
router.get('/:board/manage/catalog.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/catalog.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), csrf, manageCatalog);
router.get('/:board/manage/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/:page(1[0-9]{1,}|[2-9][0-9]{0,}|index).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), csrf, manageBoard);
router.get('/:board/manage/thread/:id([1-9][0-9]{0,}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, threadParamConverter, calcPerms,
router.get('/:board/manage/thread/:id([1-9][0-9]{0,}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, threadParamConverter, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), csrf, Posts.threadExistsMiddleware, manageThread);
router.get('/:board/manage/editpost/:id([1-9][0-9]{0,}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, threadParamConverter, calcPerms,
router.get('/:board/manage/editpost/:id([1-9][0-9]{0,}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, threadParamConverter, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), csrf, Posts.postExistsMiddleware, editPost);
router.get('/:board/manage/reports.(html|json)', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/reports.(html|json)', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), csrf, manageReports);
router.get('/:board/manage/recent.(html|json)', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/recent.(html|json)', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), csrf, manageRecent);
router.get('/:board/manage/mypermissions.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/mypermissions.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), manageMyPermissions);
router.get('/:board/manage/logs.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/logs.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_LOGS), csrf, manageLogs);
router.get('/:board/manage/bans.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/bans.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_BANS), csrf, manageBans);
router.get('/:board/manage/settings.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/settings.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_SETTINGS), csrf, manageSettings);
router.get('/:board/manage/assets.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/assets.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), csrf, manageAssets);
router.get('/:board/manage/custompages.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/custompages.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), csrf, manageCustomPages);
router.get('/:board/manage/editcustompage/:custompageid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/editcustompage/:custompageid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_CUSTOMISATION), csrf, custompageParamConverter, editCustomPage);
router.get('/:board/manage/staff.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/staff.html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_STAFF), csrf, manageStaff);
router.get('/:board/manage/editstaff/:staffusername([a-zA-Z0-9]{1,50}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, calcPerms,
router.get('/:board/manage/editstaff/:staffusername([a-zA-Z0-9]{1,50}).html', useSession, sessionRefresh, isLoggedIn, Boards.exists, setBoardLanguage, calcPerms,
hasPerms.one(Permissions.MANAGE_BOARD_STAFF), csrf, editStaff);
//global manage pages
@ -112,13 +113,12 @@ router.get('/globalmanage/editaccount/:accountusername([a-zA-Z0-9]{1,50}).html',
hasPerms.one(Permissions.MANAGE_GLOBAL_ACCOUNTS), csrf, editAccount);
router.get('/globalmanage/editrole/:roleid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, calcPerms,
hasPerms.one(Permissions.MANAGE_GLOBAL_ROLES), csrf, roleParamConverter, editRole);
//TODO: edit post edit page form, like editnews/editaccount/editrole endpoint
//captcha
router.get('/captcha', geoIp, processIp, captcha); //get captcha image and cookie
router.get('/captcha.html', captchaPage); //iframed for noscript users
router.get('/bypass.html', blockBypass); //block bypass page
router.get('/bypass_minimal.html', setMinimal, blockBypass); //block bypass page
router.get('/bypass_minimal.html', setMinimal, setQueryLanguage, blockBypass); //block bypass page
//accounts
router.get('/account.html', useSession, sessionRefresh, isLoggedIn, calcPerms, csrf, account); //page showing boards you are mod/owner of, links to password rese, logout, etc

@ -209,7 +209,7 @@ pre {
cursor: pointer;
}
#settings::after {
content: "Settings";
content: attr(data-label);
}
.expand-omitted {
background-color: var(--post-color);
@ -1113,7 +1113,7 @@ input:invalid, textarea:invalid {
.you:after {
margin-left: 3px;
content: '(You)';
content: '(' attr(data-label) ')';
font-weight: lighter;
font-style: italic;
}
@ -1640,7 +1640,7 @@ row.wrap.sb .col {
content:attr(title);
}
.user-id[title]:hover:after {
content:"Double tap to highlight" attr(data-count);
content: attr(title-mobile);
}
[title]:hover:before {
content: '';

@ -1,4 +1,4 @@
/* globals captchaOptions captchaformsection */
/* globals __ captchaOptions captchaformsection */
const captchaCookieRegex = /captchaid=(.[^;]*)/ig;
class CaptchaController {
@ -46,7 +46,7 @@ class CaptchaController {
//captcha.parentElement.previousSibling.previousSibling.tagName === 'SUMMARY' ? captcha.parentElement.previousSibling.previousSibling : captcha.parentElement;
hoverListener.addEventListener('mouseover', () => this.loadCaptcha(captcha), { once: true });
} else { //captchaOptions.type === 'text'
captcha.placeholder = 'focus to load captcha';
captcha.placeholder = __('focus to load captcha');
captcha.addEventListener('focus', () => this.loadCaptcha(captcha), { once: true });
}
}
@ -117,12 +117,12 @@ class CaptchaController {
refreshDiv.addEventListener('click', (e) => this.refreshCaptchas(e), true);
refreshDiv.textContent = '↻';
if (captchaOptions.type === 'text') {
field.placeholder = 'loading';
field.placeholder = __('loading');
}
captchaImg.src = imgSrc;
captchaImg.onload = () => {
if (captchaOptions.type === 'text') {
field.placeholder = 'Captcha text';
field.placeholder = __('Captcha text');
}
captchaDiv.appendChild(captchaImg);
captchaDiv.appendChild(refreshDiv);

@ -1,4 +1,4 @@
/* globals setLocalStorage pugfilters isCatalog captchaController threadWatcher */
/* globals __ setLocalStorage pugfilters isCatalog captchaController threadWatcher */
const getFiltersFromLocalStorage = () => {
const savedFilters = JSON.parse(localStorage.getItem('filters1'));
return savedFilters.reduce((acc, filter) => {
@ -79,7 +79,7 @@ const togglePostsHidden = (posts, state, single) => {
} else {
elem.classList['add']('hidden');
}
elem.querySelector('.postmenu').children[0].textContent = (showing ? 'Hide' : 'Show');
elem.querySelector('.postmenu').children[0].textContent = (showing ? __('Hide') : __('Show'));
}
};

@ -1,4 +1,4 @@
/* globals modal Tegaki grecaptcha hcaptcha captchaController appendLocalStorageArray socket isThread setLocalStorage forceUpdate captchaController uploaditem */
/* globals __n modal Tegaki grecaptcha hcaptcha captchaController appendLocalStorageArray socket isThread setLocalStorage forceUpdate captchaController uploaditem */
async function videoThumbnail(file) {
return new Promise((resolve, reject) => {
const hiddenVideo = document.createElement('video');
@ -517,9 +517,9 @@ class postFormHandler {
if (this.files && this.files.length === 0) {
this.fileUploadList.textContent = '';
this.fileUploadList.style.display = 'none';
this.fileLabelText.nodeValue = `Select/Drop/Paste file${this.multipleFiles ? 's' : ''}`;
this.fileLabelText.nodeValue = __n('Select/Drop/Paste files', this.multipleFiles ? 2 : 1);
} else {
this.fileLabelText.nodeValue = `${this.files.length} file${this.files.length > 1 ? 's' : ''} selected`;
this.fileLabelText.nodeValue = __n('%s files selected', this.files.length);
}
this.fileInput.value = null;
}

@ -1,4 +1,4 @@
/* globals setLocalStorage */
/* globals __ setLocalStorage */
let imageSources = new Set(JSON.parse(localStorage.getItem('hiddenimages')));
let imageSourcesList;
@ -8,7 +8,7 @@ const toggleSource = (source, state) => {
const images = document.querySelectorAll(`img.file-thumb[src="${source}"], img.catalog-thumb[src="${source}"]`);
images.forEach(i => i.classList[state?'add':'remove']('vh'));
const buttons = document.querySelectorAll(`a.hide-image[data-src="${source}"]`);
buttons.forEach(b => b.textContent = state ? 'Show' : 'Hide');
buttons.forEach(b => b.textContent = state ? __('Show') : __('Hide'));
};
toggleAllHidden(true);

@ -0,0 +1,24 @@
/* eslint-disable no-unused-vars */
/* globals TRANSLATIONS */
const pluralMap = {
1: 'one',
// two, three, few, many, ...
};
//simple translation
const __ = (key, replacement=null) => {
const translation = TRANSLATIONS[key] || key;
return replacement !== null ? translation.replace('%s', replacement) : translation;
};
//pluralisation
const __n = (key, count) => {
const pluralKey = pluralMap[count] || 'other';
const translationObj = TRANSLATIONS[key];
if (!translationObj) {
return key;
}
const translationPlural = translationObj[pluralKey] || translationObj['other'];
return translationPlural.replace('%s', count);
};

@ -1,4 +1,4 @@
/* globals isRecent isGlobalRecent isThread post extraLocals isModView io setLocalStorage */
/* globals __ isRecent isGlobalRecent isThread post extraLocals isModView io setLocalStorage */
let liveEnabled = localStorage.getItem('live') == 'true';
let scrollEnabled = localStorage.getItem('scroll') == 'true';
let socket;
@ -34,31 +34,34 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
console.log('got mark post message', data);
const anchor = document.getElementById(data.postId);
const postContainer = anchor.nextSibling;
postContainer.classList.add('marked');
postContainer.setAttribute('data-mark', data.mark);
//handle any special cases for different marks
let dataMark = '';
switch (data.type) {
case 'delete':
dataMark = __('Deleted');
break;
case 'move':
if (postContainer.classList.contains('op')) {
//moved or delete OPs then apply to whole thread
const postContainers = document.getElementsByClassName('post-container');
Array.from(postContainers).forEach(e => {
e.classList.add('marked');
e.setAttribute('data-mark', data.mark);
});
//remove new reply buttons and postform
document.getElementById('postform').remove();
const postButtons = document.getElementsByClassName('post-button');
Array.from(postButtons).forEach(e => e.remove());
//and disconnect socket
if (socket.connected === true) {
socket.disconnect();
}
}
dataMark = __('Moved');
break;
default:
//nothing special
return;
}
postContainer.classList.add('marked');
postContainer.setAttribute('data-mark', dataMark);
if (postContainer.classList.contains('op')) {
//moved or delete OPs then apply to whole thread
const postContainers = document.getElementsByClassName('post-container');
Array.from(postContainers).forEach(e => {
e.classList.add('marked');
e.setAttribute('data-mark', dataMark);
});
//remove new reply buttons and postform
document.getElementById('postform').remove();
const postButtons = document.getElementsByClassName('post-button');
Array.from(postButtons).forEach(e => e.remove());
//and disconnect socket
if (socket.connected === true) {
socket.disconnect();
}
}
};
@ -159,7 +162,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
jsonPath = jsonParts.join('/');
const fetchNewPosts = async () => {
console.log('fetching posts from api');
updateLive('Fetching posts...', 'yellow');
updateLive(__('Fetching posts...'), 'yellow');
let json;
let newPosts = [];
try {
@ -178,7 +181,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
}
}
}
updateLive('Updated', 'green');
updateLive(__('Updated'), 'green');
return newPosts.length;
};
@ -225,7 +228,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
const pingStart = Date.now();
socket.volatile.emit('ping', () => {
const latency = Date.now() - pingStart;
updateLive(`Connected for live posts (${latency}ms)`, '#0de600');
updateLive(__('Connected for live posts (%sms)', latency), '#0de600');
});
};
const fallbackToPolling = () => {
@ -249,36 +252,36 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
socket.on('message', (message) => {
console.log(message, room);
if (message === 'joined') {
updateLive('Connected for live posts', '#0de600');
updateLive(__('Connected for live posts'), '#0de600');
socketPing();
}
});
socket.on('reconnect_attempt', () => {
updateLive('Attempting to reconnect...', 'yellow');
updateLive(__('Attempting to reconnect...'), 'yellow');
});
socket.on('disconnect', () => {
console.log('lost connection to room');
updateLive('Disconnected', 'red');
updateLive(__('Disconnected'), 'red');
});
socket.on('reconnect', () => {
console.log('reconnected to room');
fetchNewPosts();
});
socket.on('error', (e) => {
updateLive('Socket error', 'orange');
updateLive(__('Socket error'), 'orange');
console.error(e);
});
socket.on('connect_error', (e) => {
updateLive('Error connecting', 'orange');
updateLive(__('Error connecting'), 'orange');
console.error(e);
fallbackToPolling();
});
socket.on('reconnect_error', (e) => {
updateLive('Error reconnecting', 'orange');
updateLive(__('Error reconnecting'), 'orange');
console.error(e);
});
socket.on('reconnect_failed', (e) => {
updateLive('Failed reconnecting', 'orange');
updateLive(__('Failed reconnecting'), 'orange');
console.error(e);
fallbackToPolling();
});
@ -298,7 +301,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
if (socket && supportsWebSockets) {
socket.disconnect();
}
updateLive('Live posts off', 'darkgray');
updateLive(__('Live posts off'), 'darkgray');
};
const liveSetting = document.getElementById('live-setting');

@ -1,3 +1,4 @@
/* globals __ */
//https://github.com/ussaohelcim/ptchina-playlist/tree/bookmarklet-let
async function threadToPlaylist(board, postId) {
async function getThread() {
@ -56,7 +57,7 @@ async function threadToPlaylist(board, postId) {
if (playlist.split('\n').length > 1) {
downloadPlaylist(`${thread.board}-${thread.postId}.m3u`, playlist);
} else {
console.log('No video/audio files in this thread.');
alert(__('No video/audio files in this thread.'));
}
} catch (error) {
console.log(error);

File diff suppressed because it is too large Load Diff

@ -1,4 +1,4 @@
/* globals isThread */
/* globals __n isThread */
window.addEventListener('DOMContentLoaded', () => {
const statsElem = document.getElementById('threadstats');
@ -16,7 +16,8 @@ window.addEventListener('DOMContentLoaded', () => {
if (updateId && updateId !== idString) { continue; }
const count = idMap.get(idString);
idElems[i].setAttribute('data-count', ` (${count})`);
idElems[i].setAttribute('title', `Double click to highlight (${count})`);
idElems[i].setAttribute('title', __n('Double click to highlight (%s)', count));
idElems[i].setAttribute('title-mobile', __n('Double tap to highlight (%s)', count));
}
};
@ -49,8 +50,8 @@ window.addEventListener('DOMContentLoaded', () => {
const numFiles = +statsElem.children[1].innerText.match(/^(\d+)/g);
const filesTotal = numFiles + newFiles;
const postTotal = numPosts + 1;
statsElem.children[0].innerText = `${postTotal} repl${postTotal === 1 ? 'y' : 'ies'}`;
statsElem.children[1].innerText = `${filesTotal} file${filesTotal === 1 ? '' : 's'}`;
statsElem.children[0].innerText = __n('%s replies', postTotal);
statsElem.children[1].innerText = __n('%s files', filesTotal);
if (e.detail.json.userId) {
const userId = e.detail.post.querySelector('.user-id');
idElems.push(userId);
@ -67,7 +68,7 @@ window.addEventListener('DOMContentLoaded', () => {
statsElem.appendChild(spacer);
statsElem.appendChild(uidSpan);
}
statsElem.children[2].innerText = `${idMap.size} UID${idMap.size === 1 ? '' : 's'}`;
statsElem.children[2].innerText = __n('%s UIDs', idMap.size);
}
});
}

@ -1,4 +1,4 @@
/* globals SERVER_TIMEZONE setLocalStorage */
/* globals __ __n LANG SERVER_TIMEZONE setLocalStorage */
let relativeTime = localStorage.getItem('relative') == 'true';
let hour24 = localStorage.getItem('24hour') == 'true';
let localTime = localStorage.getItem('localtime') == 'true';
@ -19,34 +19,34 @@ const YEAR = 31536000000
const relativeTimeString = (date) => {
let difference = Date.now() - new Date(date).getTime();
let amount = 0;
let ret = '';
let unit = '';
let isFuture = false;
if (difference < 0) {
difference = Math.abs(difference);
isFuture = true;
}
if (difference < MINUTE) {
return 'Now';
return __('Now');
} else if (difference < MINUTE*59.5) {
amount = Math.round(difference / MINUTE);
ret = `${amount} minute`;
unit = 'minute';
} else if (difference < HOUR*23.5) {
amount = Math.round(difference / HOUR);
ret = `${amount} hour`;
unit = 'hour';
} else if (difference < DAY*6.5) {
amount = Math.round(difference / DAY);
ret = `${amount} day`;
unit = 'day';
} else if (difference < WEEK*3.5) {
amount = Math.round(difference / WEEK);
ret = `${amount} week`;
unit = 'week';
} else if (difference < MONTH*11.5) {
amount = Math.round(difference / MONTH);
ret = `${amount} month`;
unit = 'month';
} else {
amount = Math.round(difference / YEAR);
ret = `${amount} year`;
unit = 'year';
}
return `${ret}${amount > 1 ? 's' : ''} ${isFuture ? 'from now' : 'ago'}`;
return __n(`%s ${unit} ${isFuture ? 'from now' : 'ago'}`, amount);
};
const changeDateFormat = (date) => {
@ -56,8 +56,7 @@ const changeDateFormat = (date) => {
if (!localTime) {
options.timeZone = SERVER_TIMEZONE;
}
const locale = hour24 ? 'en-US-u-hc-h23' : 'en-US';
const dateString = new Date(date.dateTime).toLocaleString(locale, options);
const dateString = new Date(date.dateTime).toLocaleString(LANG, options);
if (relativeTime) {
date.innerText = relativeTimeString(date.dateTime);
date.title = dateString;

@ -1,4 +1,4 @@
/* globals setLocalStorage */
/* globals __ setLocalStorage */
let notificationsEnabled = localStorage.getItem('notifications') == 'true';
let notificationYousOnly = localStorage.getItem('notification-yous-only') == 'true';
let yousEnabled = localStorage.getItem('yous-setting') == 'true';
@ -9,6 +9,7 @@ const toggleAllYous = (state) => savedYous.forEach(y => toggleOne(y, state));
const toggleQuotes = (quotes, state) => {
quotes.forEach(q => {
q[state?'setAttribute':'removeAttribute']('data-label', __('You'));
q.classList[state?'add':'remove']('you');
});
};
@ -19,6 +20,7 @@ const toggleOne = (you, state) => {
if (post) {
const postName = post.querySelector('.post-name');
if (postName) {
postName[state?'setAttribute':'removeAttribute']('data-label', __('You'));
postName.classList[state?'add':'remove']('you');
}
}

@ -409,7 +409,31 @@ function deletehtml() {
}
async function custompages() {
const formatSize = require(__dirname+'/lib/converter/formatsize.js');
const formatSize = require(__dirname+'/lib/converter/formatsize.js')
, i18n = require(__dirname+'/lib/locale/locale.js')
, locals = {
Permissions,
early404Fraction: config.get.early404Fraction,
early404Replies: config.get.early404Replies,
meta: config.get.meta,
archiveLinksURL: config.get.archiveLinksURL,
reverseImageLinksURL: config.get.reverseImageLinksURL,
enableWebring: config.get.enableWebring,
globalLimits: config.get.globalLimits,
codeLanguages: config.get.highlightOptions.languageSubset,
defaultTheme: config.get.boardDefaults.theme,
defaultCodeTheme: config.get.boardDefaults.codeTheme,
postFilesSize: formatSize(config.get.globalLimits.postFilesSize.max),
googleRecaptchaSiteKey: google.siteKey,
hcaptchaSiteKey: hcaptcha.siteKey,
globalAnnouncement: config.get.globalAnnouncement,
captchaOptions: config.get.captchaOptions,
commit,
version,
globalLanguage: config.get.language,
};
i18n.init(locals);
locals.setLocale(locals, config.get.language);
return gulp.src([
`${paths.pug.src}/custompages/*.pug`,
`${paths.pug.src}/pages/404.pug`,
@ -418,31 +442,28 @@ async function custompages() {
`${paths.pug.src}/pages/503.pug`,
`${paths.pug.src}/pages/504.pug`
])
.pipe(gulppug({
locals: {
Permissions,
early404Fraction: config.get.early404Fraction,
early404Replies: config.get.early404Replies,
meta: config.get.meta,
archiveLinksURL: config.get.archiveLinksURL,
reverseImageLinksURL: config.get.reverseImageLinksURL,
enableWebring: config.get.enableWebring,
globalLimits: config.get.globalLimits,
codeLanguages: config.get.highlightOptions.languageSubset,
defaultTheme: config.get.boardDefaults.theme,
defaultCodeTheme: config.get.boardDefaults.codeTheme,
postFilesSize: formatSize(config.get.globalLimits.postFilesSize.max),
googleRecaptchaSiteKey: google.siteKey,
hcaptchaSiteKey: hcaptcha.siteKey,
globalAnnouncement: config.get.globalAnnouncement,
captchaOptions: config.get.captchaOptions,
commit,
version,
}
}))
.pipe(gulppug({ locals }))
.pipe(gulp.dest(paths.pug.dest));
}
async function langs() {
const i18n = require(__dirname+'/lib/locale/locale.js');
await del([ 'static/js/lang/' ]);
fs.mkdirSync(`${paths.scripts.dest}lang/`);
const feStrings = require(__dirname+'/tools/festrings.json');
Object.entries(i18n.getCatalog())
.forEach(entry => {
const [lang, dict] = entry;
const minimalDict = feStrings.reduce((acc, key) => {
acc[key] = dict[key];
return acc;
}, {});
const langScript = `const LANG = '${lang}';
const TRANSLATIONS = ${JSON.stringify(minimalDict)};`;
fs.writeFileSync(`${paths.scripts.dest}lang/${lang}.js`, langScript);
});
}
async function scripts() {
const { themes, codeThemes } = require(__dirname+'/lib/misc/themes.js');
try {
@ -497,6 +518,7 @@ const extraLocals = ${JSON.stringify({ meta: config.get.meta, reverseImageLinksU
gulp.src([
//put scripts in order for dependencies
`${paths.scripts.src}/locals.js`,
`${paths.scripts.src}/i18n.js`,
`${paths.scripts.src}/localstorage.js`,
// `${paths.scripts.src}/pugruntime.js`,
`${paths.scripts.src}/modal.js`,
@ -596,7 +618,7 @@ async function closeConnections() {
}
}
const build = gulp.parallel(gulp.series(scripts, css), images, icons, gulp.series(deletehtml, custompages));
const build = gulp.parallel(gulp.series(scripts, langs, css), images, icons, gulp.series(deletehtml, custompages));
//godhelpme
module.exports = {
@ -611,6 +633,7 @@ module.exports = {
cache: gulp.series(cache, closeConnections),
migrate: gulp.series(init, migrate, closeConnections),
password: gulp.series(init, password, closeConnections),
langs: gulp.series(init, langs, closeConnections),
ips: gulp.series(init, ips, closeConnections),
default: gulp.series(init, build, closeConnections),
buildTasks: { //dont include init, etc

@ -12,14 +12,15 @@ const { outputFile } = require('fs-extra')
, { version } = require(__dirname+'/../../package.json')
, templateDirectory = path.join(__dirname+'/../../views/pages/')
, { Permissions } = require(__dirname+'/../permission/permissions.js')
, i18n = require(__dirname+'/../locale/locale.js')
, config = require(__dirname+'/../../lib/misc/config.js');
let { archiveLinksURL, lockWait, globalLimits, boardDefaults, cacheTemplates,
let { language, archiveLinksURL, lockWait, globalLimits, boardDefaults, cacheTemplates,
reverseImageLinksURL, meta, enableWebring, captchaOptions, globalAnnouncement } = config.get
, renderLocals = null;
const updateLocals = () => {
({ archiveLinksURL, lockWait, globalLimits, boardDefaults, cacheTemplates,
({ language, archiveLinksURL, lockWait, globalLimits, boardDefaults, cacheTemplates,
reverseImageLinksURL, meta, enableWebring, captchaOptions, globalAnnouncement } = config.get);
renderLocals = {
Permissions,
@ -38,7 +39,10 @@ const updateLocals = () => {
hcaptchaSiteKey: hcaptcha.siteKey,
captchaOptions,
globalAnnouncement,
globalLanguage: language,
};
i18n.init(renderLocals);
renderLocals.setLocale(renderLocals, language);
};
updateLocals();
@ -49,10 +53,17 @@ module.exports = async (htmlName=null, templateName=null, options=null, json=nul
//generate html if applicable
let html = null;
if (templateName !== null) {
html = pug.renderFile(`${templateDirectory}${templateName}`, {
const mergedLocals = {
...options,
...renderLocals,
});
};
//NOTE: will this cause issues with global locale?
if (options && options.board && options.board.settings) {
renderLocals.setLocale(renderLocals, options.board.settings.language);
} else {
renderLocals.setLocale(renderLocals, language);
}
html = pug.renderFile(`${templateDirectory}${templateName}`, mergedLocals);
}
//lock to prevent concurrent disk write

@ -60,6 +60,7 @@ module.exports = {
maxThreadMessageLength: bs.maxThreadMessageLength,
maxReplyMessageLength: bs.maxReplyMessageLength,
defaultName: bs.defaultName,
language: bs.language,
};
const { json } = await render(null, null, null, {
'name': `/${options.board._id}/settings.json`,
@ -73,9 +74,10 @@ module.exports = {
buildGlobalSettings: async () => {
const label = '/settings.json';
const start = process.hrtime();
const { captchaOptions: co } = config.get;
const { captchaOptions: co, language } = config.get;
const projectedSettings = {
captchaOptions: {
language: language,
type: co.type,
grid: {
size: co.grid.size,
@ -418,10 +420,8 @@ module.exports = {
return html;
},
buildBypass: async (minimal=false) => {
const { html } = await render(`bypass${minimal ? '_minimal' : ''}.html`, 'bypass.pug', {
minimal,
});
buildBypass: async () => {
const { html } = await render('bypass.html', 'bypass.pug');
return html;
},

@ -73,6 +73,7 @@ module.exports = async (captchaInput, captchaId) => {
body: form,
}).then(res => res.json());
} catch (e) {
console.error(e);
throw 'Captcha error occurred';
}
if (!recaptchaResponse || !recaptchaResponse.success) {
@ -92,6 +93,7 @@ module.exports = async (captchaInput, captchaId) => {
body: form,
}).then(res => res.json());
} catch (e) {
console.error(e);
throw 'Captcha error occurred';
}
if (!hcaptchaResponse || !hcaptchaResponse.success) {

@ -18,37 +18,37 @@ module.exports = {
},
//string representing how long since date A to date B
'relativeString': (now, relativeTo) => {
'relativeString': (now, relativeTo, locals) => {
let difference = now.getTime() - relativeTo.getTime();
let amount = 0;
let ret = '';
let unit = '';
let isFuture = false;
if (difference < 0) {
difference = Math.abs(difference);
isFuture = true;
}
if (difference < MINUTE) {
return 'Now';
return locals.__('Now');
} else if (difference < MINUTE*59.5) {
amount = Math.round(difference / MINUTE);
ret += `${amount} minute`;
unit = 'minute';
} else if (difference < HOUR*23.5) {
amount = Math.round(difference / HOUR);
ret += `${amount} hour`;
unit = 'hour';
} else if (difference < DAY*6.5) {
amount = Math.round(difference / DAY);
ret += `${amount} day`;
unit = 'day';
} else if (difference < WEEK*3.5) {
amount = Math.round(difference / WEEK);
ret += `${amount} week`;
unit = 'week';
} else if (difference < MONTH*11.5) {
amount = Math.round(difference / MONTH);
ret += `${amount} month`;
unit = 'month';
} else {
amount = Math.round(difference / YEAR);
ret = `${amount} year`;
unit = 'year';
}
return `${ret}${amount > 1 ? 's' : ''} ${isFuture ? 'from now' : 'ago'}`;
return locals.__n(`%s ${unit} ${isFuture ? 'from now' : 'ago'}`, amount);
},
'relativeColor': (now, relativeTo) => {

@ -2,6 +2,9 @@ const { relativeString, relativeColor, durationString } = require('./timeutils.j
describe('timeutils relativeString, relativeColor, durationString', () => {
const i18n = require(__dirname+'/../locale/locale.js');
i18n.setLocale('en-GB');
const relativeStringCases = [
{ in: { start: new Date('2022-04-07T08:00:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: 'Now'},
{ in: { start: new Date('2022-04-07T08:01:00.000Z'), end: new Date('2022-04-07T08:00:00.000Z') }, out: '1 minute ago'},
@ -21,7 +24,7 @@ describe('timeutils relativeString, relativeColor, durationString', () => {
];
for(let i in relativeStringCases) {
test(`relativeString should output ${relativeStringCases[i].out} for an input of ${relativeStringCases[i].in}`, () => {
expect(relativeString(relativeStringCases[i].in.start, relativeStringCases[i].in.end)).toStrictEqual(relativeStringCases[i].out);
expect(relativeString(relativeStringCases[i].in.start, relativeStringCases[i].in.end, i18n)).toStrictEqual(relativeStringCases[i].out);
});
}

@ -16,10 +16,11 @@ module.exports = (file, geometry, timestamp) => {
});
if (timestamp === 0) {
//bypass issue with some dumb files like audio album art covert not working with .screenshots
command.inputOptions([
'-t',
0
])
command
.inputOptions([
'-t',
0
])
.outputOptions([
`-vf scale=${geometry.width > geometry.height ? thumbSize + ':-2' : '-2:' + thumbSize}`,
'-frames:v 1'

@ -0,0 +1,30 @@
'use strict';
module.exports = Object.seal(Object.freeze(Object.preventExtensions({
BAN: 'Ban',
GLOBAL_BAN: 'Global ban',
BAN_REPORTER: 'Ban reporter',
GLOBAL_BAN_REPORTER: 'Global ban reporter',
DISMISS: 'Dismiss reports',
GLOBAL_DISMISS: 'Dismiss global reports',
DELETE: 'Delete',
DELETE_BY_IP: 'Delete by IP',
GLOBAL_DELETE_BY_IP: 'Global delete by IP',
UNLINK_FILES: 'Unlink files',
DELETE_FILES: 'Delete files',
SPOILER_FILES: 'Spoiler files',
EDIT: 'Edit',
MOVE: 'Move',
BUMPLOCK: 'Bumplock',
LOCK: 'Lock',
STICKY: 'Sticky',
CYCLE: 'Cycle',
})));

@ -0,0 +1,19 @@
'use strict';
const i18n = require('i18n')
, path = require('path')
, { debugLogs } = require(__dirname+'/../../configs/secrets.js');
i18n.configure({
directory: path.join(__dirname, '/../../locales'),
defaultLocale: 'en-GB',
retryInDefaultLocale: false,
updateFiles: false, //holy FUCK why is that an option
cookie: null,
header: null,
queryParameter: null,
});
debugLogs && console.log('Locales loaded:', i18n.getLocales());
module.exports = i18n;

@ -12,6 +12,7 @@ module.exports = {
check: async (req, res, next) => {
const { __, locale } = res.locals;
const { secureCookies, blockBypass } = config.get;
//bypass captcha permission
@ -25,12 +26,12 @@ module.exports = {
if (!res.locals.solvedCaptcha && (!bypassId || bypassId.length !== 24)) {
deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Please complete a block bypass to continue',
'frame': '/bypass_minimal.html',
'title': __('Forbidden'),
'message': __('Please complete a block bypass to continue'),
'frame': `/bypass_minimal.html?language=${encodeURIComponent(locale)}`,
'link': {
'href': '/bypass.html',
'text': 'Get block bypass',
'text': __('Get block bypass'),
},
});
}
@ -69,12 +70,12 @@ module.exports = {
deleteTempFiles(req).catch(console.error);
res.clearCookie('bypassid');
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Block bypass expired or exceeded max uses',
'frame': '/bypass_minimal.html',
'title': __('Forbidden'),
'message': __('Block bypass expired or exceeded max uses'),
'frame': `/bypass_minimal.html?language=${encodeURIComponent(locale)}`,
'link': {
'href': '/bypass.html',
'text': 'Get block bypass',
'text': __('Get block bypass'),
},
});

@ -37,9 +37,10 @@ module.exports = async (req, res, next) => {
return next(err);
}
const page = (req.body.minimal || req.path === '/blockbypass' ? 'bypass' : 'message');
const { __ } = res.locals;
return dynamicResponse(req, res, 403, page, {
'title': 'Forbidden',
'message': err,
'title': __('Forbidden'),
'message': __(err),
'redirect': req.headers.referer,
});
}

@ -6,16 +6,18 @@ const { debugLogs } = require(__dirname+'/../../../configs/secrets.js')
, upload = require('@fatchan/express-fileupload')
, fileHandlers = {}
, fileSizeLimitFunction = (req, res) => {
const { __ } = res.locals;
return dynamicResponse(req, res, 413, 'message', {
'title': 'Payload Too Large',
'message': 'Your upload was too large',
'title': __('Payload Too Large'),
'message': __('Your upload was too large'),
'redirect': req.headers.referer
});
}
, missingExtensionLimitFunction = (req, res) => {
const { __ } = res.locals;
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad Request',
'message': 'Missing file extensions',
'title': __('Bad Request'),
'message': __('Missing file extensions'),
'redirect': req.headers.referer
});
}
@ -25,11 +27,14 @@ const { debugLogs } = require(__dirname+'/../../../configs/secrets.js')
const fileSizeLimit = globalLimits[`${fileType}FilesSize`];
const fileNumLimit = globalLimits[`${fileType}Files`];
const fileNumLimitFunction = (req, res) => {
const { __ } = res.locals;
const isPostform = req.path.endsWith('/post') || req.path.endsWith('/modpost');
const message = (isPostform && res.locals.board)
? __(`Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is %s`, res.locals.board.settings.maxFiles)
: __('Max files per request is %s', fileNumLimit.max);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Too many files',
'message': (isPostform && res.locals.board) ? `Max files per post ${res.locals.board.settings.maxFiles < globalLimits.postFiles.max ? 'on this board ' : ''}is ${res.locals.board.settings.maxFiles}`
: `Max files per request is ${fileNumLimit.max}`,
'title': __('Too many files'),
'message': message,
'redirect': req.headers.referer
});
};

@ -27,6 +27,8 @@ module.exports = (options) => {
return (req, res, next) => {
const { __ } = res.locals;
const { timeFields, trimFields, allowedArrays, processThreadIdParam,
processDateParam, processMessageLength, numberFields, numberArrays,
objectIdParams, objectIdFields, objectIdArrays } = options;
@ -39,8 +41,8 @@ module.exports = (options) => {
const val = req.body[key];
if (!allowedArrays.includes(key) && Array.isArray(val)) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Malformed input'
'title': __('Bad request'),
'message': __('Malformed input'),
});
} else if (allowedArrays.includes(key) && !Array.isArray(val)) {
req.body[key] = makeArrayIfSingle(req.body[key]); //convert to arrays with single item for simpler case batch handling later
@ -134,8 +136,8 @@ module.exports = (options) => {
}
} catch (e) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Malformed input'
'title': __('Bad request'),
'message': __('Malformed input'),
});
}

@ -29,9 +29,10 @@ module.exports = async (req, res, next) => {
}
//otherwise dnsbl cant be bypassed
deleteTempFiles(req).catch(console.error);
const { __ } = res.locals;
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Your request was blocked because your IP address is listed on a blacklist.',
'title': __('Forbidden'),
'message': __('Your request was blocked because your IP address is listed on a blacklist.'),
'redirect': req.headers.referer || '/',
});
}

@ -1,6 +1,6 @@
'use strict';
const { countryNamesMap, isAnonymizer } = require(__dirname+'/../../misc/countries.js')
const { isAnonymizer } = require(__dirname+'/../../misc/countries.js')
, config = require(__dirname+'/../../misc/config.js');
module.exports = (req, res, next) => {
@ -9,7 +9,6 @@ module.exports = (req, res, next) => {
res.locals.anonymizer = isAnonymizer(code);
res.locals.country = {
code,
name: countryNamesMap[code],
};
return next();
};

@ -51,10 +51,12 @@ module.exports = (req, res, next) => {
};
next();
} catch(e) {
//should never get here
console.error('Ip parse failed', e);
const { __ } = res.locals;
return res.status(400).render('message', {
'title': 'Bad request',
'message': 'Malformed IP' //should never get here
'title': __('Bad request'),
'message': __('Malformed IP'),
});
}

@ -0,0 +1,33 @@
'use strict';
const i18n = require(__dirname+'/../../locale/locale.js')
, config = require(__dirname+'/../../misc/config.js');
module.exports = {
setGlobalLanguage: (req, res, next) => {
// global settings locale
const { language } = config.get;
res.locals.setLocale(res.locals, language);
next();
},
setBoardLanguage: (req, res, next) => {
// board settings locale
const language = res.locals.board.settings.language;
res.locals.setLocale(res.locals, language);
next();
},
setQueryLanguage: (req, res, next) => {
if (req.query.language
&& typeof req.query.language === 'string'
&& i18n.getLocales().includes(req.query.language)) {
res.locals.setLocale(res.locals, req.query.language);
}
next();
},
//TODO set param language? set body language? surely not...
};

@ -24,9 +24,10 @@ module.exports = (req, res, next) => {
//referrer is invalid url
}
if (refererCheck === true && (!req.headers.referer || !validReferer)) {
const { __ } = res.locals;
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Invalid or missing "Referer" header. Are you posting from the correct URL?'
'title': __('Forbidden'),
'message': __('Invalid or missing "Referer" header. Are you posting from the correct URL?'),
});
}
next();

@ -10,9 +10,10 @@ module.exports = {
one: (requiredPermission) => {
return cache.one[requiredPermission] || (cache.one[requiredPermission] = function(req, res, next) {
if (!res.locals.permissions.get(requiredPermission)) {
const { __ } = res.locals;
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'No Permission',
'title': __('Forbidden'),
'message': __('No Permission'),
'redirect': req.headers.referer || '/',
});
}
@ -24,9 +25,10 @@ module.exports = {
//these caches working as intended with arrays?
return cache.all[requiredPermissions] || (cache.all[requiredPermissions] = function(req, res, next) {
if (!res.locals.permissions.hasAll(...requiredPermissions)) {
const { __ } = res.locals;
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'No Permission',
'title': __('Forbidden'),
'message': __('No Permission'),
'redirect': req.headers.referer || '/',
});
}
@ -38,9 +40,10 @@ module.exports = {
//these caches working as intended with arrays?
return cache.any[requiredPermissions] || (cache.any[requiredPermissions] = function(req, res, next) {
if (!res.locals.permissions.hasAny(...requiredPermissions)) {
const { __ } = res.locals;
return res.status(403).render('message', {
'title': 'Forbidden',
'message': 'No Permission',
'title': __('Forbidden'),
'message': __('No Permission'),
'redirect': req.headers.referer || '/',
});
}

@ -5,24 +5,42 @@ const countries = require('i18n-iso-countries')
, extraCountryCodes = ['EU', 'XX', 'T1']
, anonymizerCountryCodes = ['TOR', 'LOKI']
, anonymizerCountryCodesSet = new Set(anonymizerCountryCodes)
, countryCodes = Object.keys(countryNamesMap)
.concat(extraCountryCodes, anonymizerCountryCodes);
, countryCodes = Object.keys(countryNamesMap).concat(extraCountryCodes, anonymizerCountryCodes)
, i18n = require(__dirname+'/../locale/locale.js')
, extraCountryNames = Object.seal(Object.freeze(Object.preventExtensions({
'EU': 'Europe',
'XX': 'Unknown',
'T1': 'Tor Exit Node',
'TOR': 'Tor Hidden Service',
'LOKI': 'Lokinet SNApp',
})));
//this dumb library conveniently includes 2 names for some countries...
Object.entries(countryNamesMap)
.filter(e => Array.isArray(e[1])) //for any country with an array of names,
.forEach(c => countryNamesMap[c[0]] = c[1][0]); //use the first name
countryNamesMap['EU'] = 'Europe';
countryNamesMap['XX'] = 'Unknown';
countryNamesMap['T1'] = 'Tor Exit Node';
countryNamesMap['TOR'] = 'Tor Hidden Service';
countryNamesMap['LOKI'] = 'Lokinet SNApp';
i18n.getLocales()
.forEach(locale => {
const localeExtraCodesMap = { ...extraCountryNames };
for (let code in localeExtraCodesMap) {
localeExtraCodesMap[code] = i18n.__({
phrase: localeExtraCodesMap[code],
locale: locale,
});
}
/* We are basically overwriting the existing locales in countries,
but with translated extra codes added */
const splitLocale = locale.split('-')[0]; //i18n-iso-countries doesnt have variants
countries.registerLocale({
locale: locale.toLowerCase(), //i18n-iso-countries toLowerCases these internally... ffs
countries: {
...countries.getNames(splitLocale, { select: 'official' }),
...localeExtraCodesMap,
},
});
});
module.exports = {
countryNamesMap,
countryCodes,
countryCodesSet: new Set(countryCodes),
getCountryNames: countries.getNames,
getCountryName: countries.getName,
isAnonymizer: (code) => {
return anonymizerCountryCodesSet.has(code);
},

@ -2,18 +2,18 @@ const OTPAuth = require('otpauth')
, redis = require(__dirname+'/../redis/redis.js');
module.exports = async (username, totpSecret, userInput) => {
if (!userInput) {
return null;
}
const totp = new OTPAuth.TOTP({
secret: totpSecret,
algorithm: 'SHA256',
});
let delta = totp.validate({
token: userInput,
algorithm: 'SHA256',
window: 1,
});
if (delta !== null) {
const key = `twofactor_success:${username}:${userInput}`;
const uses = await redis.incr(key);
@ -22,5 +22,7 @@ module.exports = async (username, totpSecret, userInput) => {
return null;
}
}
return delta;
};

@ -1,14 +1,18 @@
'use strict';
module.exports = (req, res, code, page, data) => {
res.status(code);
if (req.body.minimal) {
data.minimal = true;
}
if (req.headers && req.headers['x-using-xhr'] != null) {
//if sending header with js, and not a bypass_minimal page, show modal
return res.json(data);
} else {
return res.render(page, data);
}
};

@ -8,8 +8,10 @@ module.exports = (deletedPosts, updateQuotePosts) => {
const quotesBefore = post.quotes.length;
post.quotes = post.quotes.filter(q => q.postId !== ap.postId);
if (quotesBefore !== post.quotes.length) { //optimization, probably
post.message = post.message.replace(`<a class="quote" href="/${ap.board}/thread/${ap.thread}.html#${ap.postId}">&gt;&gt;${ap.postId}</a>`,
`<span class="invalid-quote">&gt;&gt;${ap.postId}</span>`);
post.message = post.message.replace(
`<a class="quote" href="/${ap.board}/thread/${ap.thread}.html#${ap.postId}">&gt;&gt;${ap.postId}</a>`,
`<span class="invalid-quote">&gt;&gt;${ap.postId}</span>`
);
}
});
bulkWrites.push({

@ -7,13 +7,14 @@ const { Bans } = require(__dirname+'/../../db/')
module.exports = async (req, res, hitGlobalFilter, hitLocalFilter, boardFilterMode, globalFilterMode,
boardFilterBanDuration, globalFilterBanDuration, filterBanAppealable, redirect) => {
const { __ } = res.locals;
//global filter mode takes prio
const useFilterMode = hitGlobalFilter ? globalFilterMode : boardFilterMode;
if (useFilterMode === 1) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': 'Your post was blocked by a word filter',
'title': __('Bad request'),
'message': __('Your post was blocked by a word filter'),
'redirect': redirect
});
} else {
@ -25,10 +26,10 @@ module.exports = async (req, res, hitGlobalFilter, hitLocalFilter, boardFilterMo
'ip': {
'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw,
'type': res.locals.ip.type,
},
'type': res.locals.anonymizer ? 1 : 0,
'range': 0,
'reason': `${hitGlobalFilter ? 'global ' :''}word filter auto ban`,
'reason': __(`${hitGlobalFilter ? 'global ' :''}word filter auto ban`),
'board': banBoard,
'posts': null,
'issuer': 'system', //todo: make a "system" property instead?
@ -36,7 +37,7 @@ module.exports = async (req, res, hitGlobalFilter, hitLocalFilter, boardFilterMo
'expireAt': banExpiry,
'allowAppeal': hitGlobalFilter ? filterBanAppealable : true,
'showUser': true,
'note': `${hitGlobalFilter ? 'global ' :''}filter hit: "${hitGlobalFilter || hitLocalFilter}"`,
'note': __(`${hitGlobalFilter ? 'global ' :''}filter hit: "%s"`, (hitGlobalFilter || hitLocalFilter)),
'seen': true,
};
const insertedResult = await Bans.insertOne(ban);

@ -1,6 +1,6 @@
const escape = require('./escape.js');
const simpleEscape = require('./escape.js');
describe('escape() - convert some characters to html entities', () => {
describe('simpleEscape() - convert some characters to html entities', () => {
const cases = [
{ in: '\'', out: '&#39;' },
{ in: '/', out: '&#x2F;' },
@ -13,7 +13,7 @@ describe('escape() - convert some characters to html entities', () => {
];
for(let i in cases) {
test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => {
expect(escape(cases[i].in)).toBe(cases[i].out);
expect(simpleEscape(cases[i].in)).toBe(cases[i].out);
});
}
});

@ -2,8 +2,8 @@
module.exports = {
regexPrepare: /##(?<numdice>[1-9][0-9]{0,1})d(?<numsides>1[0-9]{1,8}|[2-9][0-9]{0,8})(?:(?<operator>[+-])(?<modifier>[1-9][0-9]{0,8}))?(?<value>=[1-9][0-9]{0,8})?/gmi,
regexMarkdown: /##(?<numdice>[1-9][0-9]{0,1})d(?<numsides>1[0-9]{1,8}|[2-9][0-9]{0,8})(?:(?<operator>[+-])(?<modifier>[1-9][0-9]{0,8}))?&#x3D;(?<value>[1-9][0-9]{0,8})/gmi,
regexPrepare: /##(?<numdice>[1-9][0-9]{0,1})%(?<numsides>1[0-9]{1,8}|[2-9][0-9]{0,8})(?:(?<operator>[+-])(?<modifier>[1-9][0-9]{0,8}))?(?<value>=[1-9][0-9]{0,8})?/gmi,
regexMarkdown: /##(?<numdice>[1-9][0-9]{0,1})%(?<numsides>1[0-9]{1,8}|[2-9][0-9]{0,8})(?:(?<operator>[+-])(?<modifier>[1-9][0-9]{0,8}))?&#x3D;(?<value>[1-9][0-9]{0,8})/gmi,
prepare: (force, match, numdice, numsides, operator, modifier, value) => {
if (!force && value) {
@ -34,7 +34,6 @@ module.exports = {
numdice = parseInt(numdice);
numsides = parseInt(numsides);
value = parseInt(value);
let matchWithoutValue = match.replace(/&#x3D;.*/, '');
return `<img src='/file/dice.png' height='16' width='16' /><span class='dice'>(${matchWithoutValue}) Rolled ${numdice} dice with ${numsides} sides${modifier ? ' and modifier '+operator+modifier : '' } = ${value}</span>`;
return `<img src='/file/dice.png' height='16' width='16' /><span class='dice'>(##${numdice}%${numsides}${modifier ? operator+modifier : '' }) = ${value}</span>`;
},
};

@ -3,12 +3,12 @@ const diceroll = require('./diceroll.js');
describe('diceroll markdown', () => {
const prepareCases = [
{ in: '##3d6', out: '##3d6=' },
{ in: '##99d99', out: '##99d99=' },
{ in: '##999d999', out: '##999d999' },
{ in: '##3d8+5', out: '##3d8+5=' },
{ in: '##3d8-5', out: '##3d8-5=' },
{ in: '##0d0', out: '##0d0' },
{ in: '##3%6', out: '##3%6=' },
{ in: '##99%99', out: '##99%99=' },
{ in: '##999%999', out: '##999%999' },
{ in: '##3%8+5', out: '##3%8+5=' },
{ in: '##3%8-5', out: '##3%8-5=' },
{ in: '##0%0', out: '##0%0' },
];
for(let i in prepareCases) {
test(`should contain ${prepareCases[i].out} for an input of ${prepareCases[i].in}`, () => {
@ -18,10 +18,11 @@ describe('diceroll markdown', () => {
}
const markdownCases = [
{ in: '##3d6&#x3D;10', out: 'Rolled 3 dice with 6 sides =' },
{ in: '##99d99&#x3D;5138', out: 'Rolled 99 dice with 99 sides =' },
{ in: '##999d999&#x3D;10000', out: '##999d999&#x3D;10000' },
{ in: '##0d0', out: '##0d0' },
{ in: '##3%6&#x3D;10', out: '(##3%6)' },
{ in: '##99%99&#x3D;5138', out: '(##99%99)' },
{ in: '##999%999&#x3D;10000', out: '##999%999&#x3D;' },
{ in: '##0%0&#x3D;10', out: '##0%0&#x3D;' },
{ in: '##0%0', out: '##0%0' },
];
for(let i in markdownCases) {
test(`should contain ${markdownCases[i].out} for an input of ${markdownCases[i].in}`, () => {

@ -16,7 +16,7 @@ const greentextRegex = /^&gt;((?!&gt;\d+|&gt;&gt;&#x2F;\w+(&#x2F;\d*)?|&gt;&gt;#
, includeSplitRegex = /(\[code\][\s\S]+?\[\/code\])/gm
, splitRegex = /\[code\]([\s\S]+?)\[\/code\]/gm
, trimNewlineRegex = /^(\s*\r?\n)*/g
, escape = require(__dirname+'/escape.js')
, simpleEscape = require(__dirname+'/escape.js')
, { highlight, highlightAuto, listLanguages } = require('highlight.js')
, validLanguages = listLanguages() //precompute
, { addCallback } = require(__dirname+'/../../redis/redis.js')
@ -72,7 +72,7 @@ module.exports = {
for (let i = 0; i < chunks.length; i++) {
//every other chunk will be a code block
if (i % 2 === 0) {
const escaped = escape(chunks[i]);
const escaped = simpleEscape(chunks[i]);
const newlineFix = escaped.replace(/^\r?\n/,''); //fix ending newline because of codeblock
chunks[i] = module.exports.processRegularChunk(newlineFix, permissions);
} else if (permissions.get(Permissions.USE_MARKDOWN_CODE)){
@ -91,18 +91,18 @@ module.exports = {
}
if (!lang) {
//no language specified, try automatic syntax highlighting
const { language, relevance, value } = highlightAuto(trimFix, highlightOptions.languageSubset);
const { relevance, value } = highlightAuto(trimFix, highlightOptions.languageSubset);
if (relevance > highlightOptions.threshold) {
return `<span class='code hljs'><small>possible language: ${language}, relevance: ${relevance}</small>\n${value}</span>`;
return `<span class='code hljs'>${value}</span>`;
}
} else if (lang === 'aa') {
return `<span class='aa'>${escape(matches.groups.code)}</span>`;
return `<span class='aa'>${simpleEscape(matches.groups.code)}</span>`;
} else if (validLanguages.includes(lang)) {
const { value } = highlight(trimFix, { language: lang, ignoreIllegals: true });
return `<span class='code hljs'><small>language: ${lang}</small>\n${value}</span>`;
return `<span class='code hljs'>${value}</span>`;
}
//else, auto highlight relevance threshold was too low, lang was not a valid language, or lang was 'plain'
return `<span class='code'>${escape(trimFix)}</span>`;
return `<span class='code'>${simpleEscape(trimFix)}</span>`;
},
processRegularChunk: (text, permissions) => {

@ -3,18 +3,17 @@
const { getInsecureTrip, getSecureTrip } = require(__dirname+'/tripcode.js')
, { Permissions } = require(__dirname+'/../permission/permissions.js')
, nameRegex = /^(?<name>[^#].*?)?(?:(?<tripcode>##(?<strip>[^ ].*?)|#(?<itrip>[^#].*?)))?(?<capcode>##(?<capcodetext> .*?)?)?$/
, staffLevels = ['Admin', 'Global Staff', 'Board Owner', 'Board Mod']
, staffLevelsRegex = new RegExp(`(${staffLevels.join('|')})+`, 'igm');
, staffLevels = ['Admin', 'Global Staff', 'Board Owner', 'Board Staff'];
module.exports = async (inputName, permissions, boardSettings, boardOwner, boardStaff, username) => {
module.exports = async (inputName, permissions, boardSettings, boardOwner, boardStaff, username, __) => {
const { forceAnon, defaultName } = boardSettings;
const isBoardOwner = username === boardOwner; //why not just check staffboards and ownedboards?
const staffUsernames = Object.keys(boardStaff);
const isBoardMod = staffUsernames.includes(username);
const staffPermissions = [permissions.get(Permissions.ROOT),
permissions.get(Permissions.MANAGE_GLOBAL_GENERAL),
isBoardOwner, isBoardMod];
const staffPermissions = [permissions.get(Permissions.ROOT), permissions.get(Permissions.MANAGE_GLOBAL_GENERAL), isBoardOwner, isBoardMod];
const langStaffLevels = staffLevels.map(l => __(l));
const staffLevelsRegex = new RegExp(`(${langStaffLevels.join('|')})+`, 'igm');
let name = defaultName;
let tripcode = null;
@ -51,18 +50,18 @@ module.exports = async (inputName, permissions, boardSettings, boardOwner, board
const { staffLevelDirect, staffLevelFallback } = staffPermissions.reduce((acc, sp, i) => {
if (sp === true) {
if (!acc.staffLevelFallback) {
acc.staffLevelFallback = staffLevels[i];
acc.staffLevelFallback = langStaffLevels[i];
}
if (!acc.staffLevelDirect && capcodeInput.toLowerCase().startsWith(staffLevels[i].toLowerCase())) {
acc.staffLevelDirect = staffLevels[i];
acc.staffLevelDirect = langStaffLevels[i];
}
}
return acc;
}, { 'staffLevelDirect': null, 'staffLevelFallback': null });
//we still get the same fallbacks as before, its just more annoying without the direct permlevel mapping
const staffLevel = staffLevelDirect ||
(isBoardOwner ? staffLevels[2]
: isBoardMod ? staffLevels[3]
(isBoardOwner ? langStaffLevels[2]
: isBoardMod ? langStaffLevels[3]
: staffLevelFallback);
capcode = staffLevel;
if (capcodeInput && capcodeInput.toLowerCase() !== staffLevel.toLowerCase()) {

@ -1,5 +1,6 @@
const name = require('./name.js');
const Permission = require('../permission/permission.js');
const i18n = require('../locale/locale.js');
const ROOT = new Permission();
ROOT.setAll(Permission.allPermissions);
const NO_PERMISSION = new Permission();
@ -10,7 +11,7 @@ describe('name/trip/capcode handler', () => {
{ in: '## Admin', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '## Global Staff', out: { name: 'Anon', tripcode: null, capcode: '## Global Staff' } },
{ in: '## Board Owner', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '## Board Mod', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '## Board Staff', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '##', out: { name: 'Anon', tripcode: null, capcode: '## Admin' } },
{ in: '', out: { name: 'Anon', tripcode: null, capcode: null } },
{ in: 'test', out: { name: 'test', tripcode: null, capcode: null } },
@ -24,7 +25,7 @@ describe('name/trip/capcode handler', () => {
for(let i in cases) {
test(`should contain ${cases[i].out.capcode} for an input of ${cases[i].in}`, async () => {
const output = await name(cases[i].in, (cases[i].perm || ROOT), {forceAnon: false, defaultName: 'Anon'}, 'a', {a:ROOT}, 'b');
const output = await name(cases[i].in, (cases[i].perm || ROOT), {forceAnon: false, defaultName: 'Anon'}, 'a', {a:ROOT}, 'b', i18n.__);
expect(output).toStrictEqual(cases[i].out);
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,34 @@
'use strict';
module.exports = async(db, redis) => {
console.log('Updating edited posts username property for "hidden users" to support localisation');
await db.collection('posts').updateMany({
'edited.username': 'Hidden User',
}, {
'$set': {
'edited.username': null,
},
});
console.log('Updating db for language settings');
await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
'$set': {
'language': 'en-GB',
},
});
await db.collection('boards').updateMany({}, {
'$set': {
'settings.language': 'en-GB',
},
});
await db.collection('modlog').updateMany({
'actions': 'Edit',
}, {
'$set': {
'actions': ['Edit'],
},
});
console.log('Clearing globalsettings cache');
await redis.deletePattern('globalsettings');
console.log('Clearing boards cache');
await redis.deletePattern('board:*');
};

@ -15,6 +15,7 @@ const { Posts, Boards, Modlogs } = require(__dirname+'/../../db/')
, movePosts = require(__dirname+'/moveposts.js')
, { remove } = require('fs-extra')
, uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js')
, ModlogActions = require(__dirname+'/../../lib/input/modlogactions.js')
, getAffectedBoards = require(__dirname+'/../../lib/misc/affectedboards.js')
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, { Permissions } = require(__dirname+'/../../lib/permission/permissions.js')
@ -25,6 +26,8 @@ const { Posts, Boards, Modlogs } = require(__dirname+'/../../db/')
module.exports = async (req, res, next) => {
const { __ } = res.locals;
//try to set a good redirect
let redirect = req.headers.referer;
if (!redirect) {
@ -54,8 +57,8 @@ module.exports = async (req, res, next) => {
}
if (passwordPosts.length === 0) {
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'error': 'Password did not match any selected posts',
'title': __('Forbidden'),
'error': __('Password did not match any selected posts'),
redirect,
});
}
@ -89,14 +92,14 @@ module.exports = async (req, res, next) => {
if (req.body.ban || req.body.global_ban || req.body.report_ban || req.body.global_report_ban) {
const { message, action, query } = await banPoster(req, res, next);
if (req.body.ban) {
modlogActions.push('Ban');
modlogActions.push(ModlogActions.BAN);
} else if (req.body.global_ban) {
modlogActions.push('Global Ban');
modlogActions.push(ModlogActions.GLOBAL_BAN);
}
if (req.body.report_ban) {
modlogActions.push('Ban reporter');
modlogActions.push(ModlogActions.BAN_REPORTER);
} else if (req.body.global_report_ban) {
modlogActions.push('Global ban reporter');
modlogActions.push(ModlogActions.GLOBAL_BAN_REPORTER);
}
if (action) {
combinedQuery[action] = { ...combinedQuery[action], ...query};
@ -117,8 +120,8 @@ module.exports = async (req, res, next) => {
});
if (protectedThread === true) {
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'error': 'You cannot delete old threads or threads with too many replies',
'title': __('Forbidden'),
'error': __('You cannot delete old threads or threads with too many replies'),
redirect,
});
}
@ -166,18 +169,18 @@ module.exports = async (req, res, next) => {
}
if (req.body.delete_file) {
const { message } = await deletePostsFiles(res.locals.posts, false); //delete files, not just unlink
const { message } = await deletePostsFiles(res.locals, false); //delete files, not just unlink
messages.push(message);
}
const { action, message } = await deletePosts(res.locals.posts, req.body.delete_ip_global ? null : req.params.board);
const { action, message } = await deletePosts(res.locals.posts, req.body.delete_ip_global ? null : req.params.board, res.locals);
messages.push(message);
if (action) {
if (req.body.delete) {
modlogActions.push('Delete');
modlogActions.push(ModlogActions.DELETE);
} else if (req.body.delete_ip_board) {
modlogActions.push('Delete by IP');
modlogActions.push(ModlogActions.DELETE_BY_IP);
} else if (req.body.delete_ip_global) {
modlogActions.push('Global delete by IP');
modlogActions.push(ModlogActions.GLOBAL_DELETE_BY_IP);
}
recalculateThreadMetadata = true;
}
@ -196,7 +199,7 @@ module.exports = async (req, res, next) => {
}
const { message, action } = await movePosts(req, res);
if (action) {
modlogActions.push('Moved');
modlogActions.push(ModlogActions.MOVE);
recalculateThreadMetadata = true;
if (res.locals.destinationBoard && res.locals.destinationThread) {
res.locals.posts.push(res.locals.destinationThread);
@ -210,54 +213,54 @@ module.exports = async (req, res, next) => {
// if it was getting deleted/moved, dont do these actions
if (req.body.unlink_file || req.body.delete_file) {
const { message, action, query } = await deletePostsFiles(res.locals.posts, req.body.unlink_file);
const { message, action, query } = await deletePostsFiles(res.locals, req.body.unlink_file);
if (action) {
if (req.body.unlink_file) {
modlogActions.push('Unlink files');
modlogActions.push(ModlogActions.UNLINK_FILES);
} else if (req.body.delete_file) {
modlogActions.push('Delete files');
modlogActions.push(ModlogActions.DELETE_FILES);
}
recalculateThreadMetadata = true;
combinedQuery[action] = { ...combinedQuery[action], ...query};
}
messages.push(message);
} else if (req.body.spoiler) {
const { message, action, query } = spoilerPosts(res.locals.posts);
const { message, action, query } = spoilerPosts(res.locals);
if (action) {
modlogActions.push('Spoiler files');
modlogActions.push(ModlogActions.SPOILER_FILES);
combinedQuery[action] = { ...combinedQuery[action], ...query};
}
messages.push(message);
}
//lock, sticky, bumplock, cyclic
if (req.body.bumplock) {
const { message, action, query } = bumplockPosts(res.locals.posts);
const { message, action, query } = bumplockPosts(res.locals);
if (action) {
modlogActions.push('Bumplock');
modlogActions.push(ModlogActions.BUMPLOCK);
combinedQuery[action] = { ...combinedQuery[action], ...query};
}
messages.push(message);
}
if (req.body.lock) {
const { message, action, query } = lockPosts(res.locals.posts);
const { message, action, query } = lockPosts(res.locals);
if (action) {
modlogActions.push('Lock');
modlogActions.push(ModlogActions.LOCK);
combinedQuery[action] = { ...combinedQuery[action], ...query};
}
messages.push(message);
}
if (req.body.sticky != null) {
const { message, action, query } = stickyPosts(res.locals.posts, req.body.sticky);
const { message, action, query } = stickyPosts(res.locals, req.body.sticky);
if (action) {
modlogActions.push('Sticky');
modlogActions.push(ModlogActions.STICKY);
combinedQuery[action] = { ...combinedQuery[action], ...query};
}
messages.push(message);
}
if (req.body.cyclic) {
const { message, action, query } = cyclePosts(res.locals.posts);
const { message, action, query } = cyclePosts(res.locals);
if (action) {
modlogActions.push('Cycle');
modlogActions.push(ModlogActions.CYCLE);
combinedQuery[action] = { ...combinedQuery[action], ...query};
}
messages.push(message);
@ -274,9 +277,9 @@ module.exports = async (req, res, next) => {
const { message, action, query } = dismissReports(req, res);
if (action) {
if (req.body.dismiss) {
modlogActions.push('Dismiss reports');
modlogActions.push(ModlogActions.DISMISS);
} else if (req.body.global_dismiss) {
modlogActions.push('Dismiss global reports');
modlogActions.push(ModlogActions.GLOBAL_DISMISS);
}
combinedQuery[action] = { ...combinedQuery[action], ...query};
}
@ -316,12 +319,10 @@ module.exports = async (req, res, next) => {
const modlog = {};
const logDate = new Date(); //all events current date
const message = req.body.log_message || null;
let logUser;
let logUser = null;
//could even do if (req.session.user) {...}, but might cause cross-board log username contamination
if (isStaffOrGlobal) {
logUser = req.session.user;
} else {
logUser = 'Unregistered User';
}
for (let i = 0; i < res.locals.posts.length; i++) {
const post = res.locals.posts[i];
@ -332,7 +333,7 @@ module.exports = async (req, res, next) => {
postLinks: [],
actions: modlogActions,
date: logDate,
showUser: !req.body.hide_name || logUser === 'Unregistered User' ? true : false,
showUser: !req.body.hide_name || logUser === null ? true : false,
message: message,
user: logUser,
ip: {
@ -630,7 +631,7 @@ module.exports = async (req, res, next) => {
}
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'title': __('Success'),
'messages': messages,
redirect,
});

@ -11,6 +11,7 @@ const { remove, pathExists } = require('fs-extra')
module.exports = async (req, res) => {
const { __, __n } = res.locals;
const { checkRealMimeTypes } = config.get;
const redirect = `/${req.params.board}/manage/assets.html`;
@ -24,8 +25,8 @@ module.exports = async (req, res) => {
})) {
await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': `Invalid file type for ${req.files.file[i].name}. Mimetype ${req.files.file[i].mimetype} not allowed.`,
'title': __('Bad request'),
'message': __('Invalid file type for %s. Mimetype %s not allowed.', req.files.file[i].name, req.files.file[i].mimetype),
'redirect': redirect
});
}
@ -34,8 +35,8 @@ module.exports = async (req, res) => {
if (!(await mimeTypes.realMimeCheck(req.files.file[i]))) {
deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': `Mime type mismatch for file "${req.files.file[i].name}"`,
'title': __('Bad request'),
'message': __('Mime type mismatch for file "%s"', req.files.file[i].name),
'redirect': redirect
});
}
@ -71,8 +72,8 @@ module.exports = async (req, res) => {
// no new assets
if (filenames.length === 0) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': `Asset${res.locals.numFiles > 1 ? 's' : ''} already exist${res.locals.numFiles > 1 ? '' : 's'}`,
'title': __('Bad request'),
'message': __n('Asset already exist', res.locals.numFiles),
'redirect': redirect
});
}
@ -84,8 +85,8 @@ module.exports = async (req, res) => {
res.locals.board.assets = res.locals.board.assets.concat(filenames);
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': `Uploaded ${filenames.length} new assets.`,
'title': __('Success'),
'message': __n('Uploaded %s new assets.', filenames.length),
'redirect': redirect
});

@ -8,6 +8,7 @@ const { CustomPages } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __ } = res.locals;
const message = prepareMarkdown(req.body.message, false);
const { message: markdownMessage } = await messageHandler(message, null, null, res.locals.permissions);
@ -36,8 +37,8 @@ module.exports = async (req, res) => {
});
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Added custom page',
'title': __('Success'),
'message': __('Added custom page'),
'redirect': `/${req.params.board}/manage/custompages.html`
});

@ -14,6 +14,7 @@ const path = require('path')
module.exports = async (req, res) => {
const { __ } = res.locals;
const { checkRealMimeTypes } = config.get;
const redirect = `/${req.params.board}/manage/assets.html`;
@ -28,8 +29,8 @@ module.exports = async (req, res) => {
})) {
await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': `Invalid file type for ${req.files.file[i].name}. Mimetype ${req.files.file[i].mimetype} not allowed.`,
'title': __('Bad request'),
'message': __('Invalid file type for %s. Mimetype %s not allowed.', req.files.file[i].name, req.files.file[i].mimetype),
'redirect': redirect
});
}
@ -41,8 +42,8 @@ module.exports = async (req, res) => {
if (!(await mimeTypes.realMimeCheck(req.files.file[i]))) {
deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request',
'message': `Mime type mismatch for file "${req.files.file[i].name}"`,
'title': __('Bad request'),
'message': __('Mime type mismatch for file "%s"', req.files.file[i].name),
'redirect': redirect
});
}
@ -95,8 +96,8 @@ module.exports = async (req, res) => {
});
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': `Uploaded ${res.locals.numFiles} new flags.`,
'title': __('Success'),
'message': __('Uploaded %s new flags.', res.locals.numFiles),
'redirect': redirect
});

@ -8,6 +8,7 @@ const { News } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __ } = res.locals;
const message = prepareMarkdown(req.body.message, false);
const { message: markdownNews } = await messageHandler(message, null, null, res.locals.permissions);
@ -29,8 +30,8 @@ module.exports = async (req, res) => {
});
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Added newspost',
'title': __('Success'),
'message': __('Added newspost'),
'redirect': '/globalmanage/news.html'
});

@ -6,14 +6,16 @@ const { Boards, Accounts } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __ } = res.locals;
await Promise.all([
Accounts.addStaffBoard([req.body.username], res.locals.board._id),
Boards.addStaff(res.locals.board._id, req.body.username, roleManager.roles.BOARD_STAFF)
]);
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Added staff',
'title': __('Success'),
'message': __('Added staff'),
'redirect': `/${req.params.board}/manage/staff.html`,
});

@ -5,10 +5,11 @@ const { Bans } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __, __n } = res.locals;
const { defaultBanDuration } = config.get;
const banDate = new Date();
const banExpiry = new Date(banDate.getTime() + (req.body.ban_duration || defaultBanDuration)); //uses config default if missing or malformed
const banReason = req.body.ban_reason || req.body.log_message || 'No reason specified';
const banReason = req.body.ban_reason || req.body.log_message || __('No reason specified');
const allowAppeal = (req.body.no_appeal || req.body.ban_q || req.body.ban_h) ? false : true; //dont allow appeals for range bans
const bans = [];
@ -105,7 +106,7 @@ module.exports = async (req, res) => {
const numBans = await Bans.insertMany(bans).then(result => result.insertedCount);
const query = {
message: `Added ${numBans} bans`,
message: __n('Added %s bans', numBans),
};
if ((req.body.ban || req.body.global_ban ) && req.body.ban_reason) {

@ -7,6 +7,7 @@ const { Bypass } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __ } = res.locals;
const { secureCookies, blockBypass } = config.get;
const existingBypassId = req.signedCookies.bypassid || res.locals.pseudoIp;
const bypass = await Bypass.getBypass(res.locals.anonymizer, existingBypassId, blockBypass.expireAfterUses);
@ -22,8 +23,8 @@ module.exports = async (req, res) => {
return dynamicResponse(req, res, 200, 'message', {
'minimal': req.body.minimal,
'title': 'Success',
'message': 'Completed block bypass, you may go back and make your post.',
'title': __('Success'),
'message': __('Completed block bypass, you may go back and make your post.'),
});
};

@ -2,7 +2,9 @@
const { NumberInt } = require(__dirname+'/../../db/db.js');
module.exports = (posts) => {
module.exports = (locals) => {
const { posts, __, __n } = locals;
const filteredposts = posts.filter(post => {
return !post.thread;
@ -10,12 +12,12 @@ module.exports = (posts) => {
if (filteredposts.length === 0) {
return {
message: 'No thread(s) to bumplock',
message: __('No threads selected to Bumplock'),
};
}
return {
message: `Toggled bumplock for ${filteredposts.length} thread(s)`,
message: __n('Toggled Bumplock for %s threads', filteredposts.length),
action: '$bit',
query: {
'bumplocked': {

@ -10,10 +10,9 @@ const { Boards, Posts } = require(__dirname+'/../../db/')
, deletePosts = require(__dirname+'/deletepost.js')
, { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js')
, messageHandler = require(__dirname+'/../../lib/post/message.js')
, { countryCodes } = require(__dirname+'/../../lib/misc/countries.js')
, { countryCodesSet } = require(__dirname+'/../../lib/misc/countries.js')
, { trimSetting, numberSetting, booleanSetting, arraySetting } = require(__dirname+'/../../lib/input/setting.js')
, { compareSettings } = require(__dirname+'/../../lib/input/settingsdiff.js')
, validCountryCodes = new Set(countryCodes)
, settingChangeEntries = Object.entries({
'userPostDelete': ['board', 'catalog', 'threads'],
'userPostSpoiler': ['board', 'catalog', 'threads'],
@ -27,6 +26,7 @@ const { Boards, Posts } = require(__dirname+'/../../db/')
'codetheme': ['board', 'threads', 'catalog', 'other'],
'announcement.raw': ['board', 'threads', 'catalog', 'other'],
'customCss': ['board', 'threads', 'catalog', 'other'],
'language': ['board', 'threads', 'catalog', 'other'],
'allowedFileTypes.other': ['board', 'threads', 'catalog'],
'allowedFileTypes.image': ['board', 'threads', 'catalog'],
'enableTegaki': ['board', 'threads', 'catalog'],
@ -35,6 +35,7 @@ const { Boards, Posts } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __ } = res.locals;
const { globalLimits } = config.get;
//oldsettings before changes
@ -51,14 +52,15 @@ module.exports = async (req, res) => {
if (req.body.countries) {
req.body.countries = [...new Set(req.body.countries)] //prevents submitting multiple of same code, not like it matters, but meh
.filter(code => validCountryCodes.has(code))
.slice(0,countryCodes.length);
.filter(code => countryCodesSet.has(code))
.slice(0, countryCodesSet.size);
}
const newSettings = {
'name': trimSetting(req.body.name, oldSettings.name),
'description': trimSetting(req.body.description, oldSettings.description),
'defaultName': trimSetting(req.body.default_name, oldSettings.defaultName),
'language': trimSetting(req.body.language, oldSettings.language),
'theme': req.body.theme || oldSettings.theme,
'codeTheme': req.body.code_theme || oldSettings.codeTheme,
'sfw': booleanSetting(req.body.sfw),
@ -164,7 +166,7 @@ module.exports = async (req, res) => {
//prune old threads
const prunedThreads = await Posts.pruneThreads(res.locals.board);
if (prunedThreads.length > 0) {
await deletePosts(prunedThreads, req.params.board);
await deletePosts(prunedThreads, req.params.board, res.locals);
//remove board page html/json for pages > newMaxPage
for (let i = newMaxPage+1; i <= oldMaxPage; i++) {
promises.push(remove(`${uploadDirectory}/html/${req.params.board}/${i}.html`));
@ -231,8 +233,8 @@ module.exports = async (req, res) => {
debugLogs && console.log(req.params.board, 'board settings changed');
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Updated settings.',
'title': __('Success'),
'message': __('Updated settings.'),
'redirect': `/${req.params.board}/manage/settings.html`
});

@ -29,6 +29,7 @@ const { Boards } = require(__dirname+'/../../db/')
'codeThemes': ['scripts'],
'globalLimits.postFiles.max': ['deletehtml', 'custompages'],
'globalLimits.postFilesSize.max': ['deletehtml', 'custompages'],
'language': ['deletehtml', 'css', 'scripts', 'custompages'],
//these will make it easier to keep updated and include objects where any/all property change needs tasks
//basically, it expands to all of globalLimits.fieldLength.* or frontendScriptDefault.*
//it could be calculated in compareSettings with *, but im just precompiling it now. probably a tiny bit faster not doing it each time
@ -39,6 +40,7 @@ const { Boards } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __ } = res.locals;
const promises = [];
const oldSettings = config.get;
@ -65,6 +67,7 @@ module.exports = async (req, res) => {
siteName: trimSetting(req.body.meta_site_name, oldSettings.meta.siteName),
url: trimSetting(req.body.meta_url, oldSettings.meta.url),
},
language: trimSetting(req.body.language, oldSettings.language),
captchaOptions: {
type: trimSetting(req.body.captcha_options_type, oldSettings.captchaOptions.type),
generateLimit: numberSetting(req.body.captcha_options_generate_limit, oldSettings.captchaOptions.generateLimit),
@ -371,8 +374,8 @@ module.exports = async (req, res) => {
});
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Updated settings.',
'title': __('Success'),
'message': __('Updated settings.'),
'redirect': '/globalmanage/settings.html'
});

@ -8,6 +8,7 @@ const bcrypt = require('bcrypt')
module.exports = async (req, res) => {
const { __ } = res.locals;
const username = req.body.username.toLowerCase();
const password = req.body.password;
const newPassword = req.body.newpassword;
@ -18,8 +19,8 @@ module.exports = async (req, res) => {
//if the account doesnt exist, reject
if (!account) {
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Incorrect account credentials',
'title': __('Forbidden'),
'message': __('Incorrect account credentials'),
'redirect': '/changepassword.html'
});
}
@ -30,8 +31,8 @@ module.exports = async (req, res) => {
//if hashes matched
if (passwordMatch === false) {
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Incorrect account credentials',
'title': __('Forbidden'),
'message': __('Incorrect account credentials'),
'redirect': '/changepassword.html'
});
}
@ -40,8 +41,8 @@ module.exports = async (req, res) => {
const delta = await doTwoFactor(username, account.twofactor, req.body.twofactor);
if (delta === null) {
return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden',
'message': 'Incorrect account credentials',
'title': __('Forbidden'),
'message': __('Incorrect account credentials'),
'redirect': '/changepassword.html'
});
}
@ -54,8 +55,8 @@ module.exports = async (req, res) => {
]);
return dynamicResponse(req, res, 200, 'message', {
'title': 'Success',
'message': 'Changed password',
'title': __('Success'),
'message': __('Password updated successfully'),
'redirect': '/login.html'
});

@ -11,6 +11,7 @@ const { Boards, Accounts } = require(__dirname+'/../../db/')
module.exports = async (req, res) => {
const { __ } = res.locals;
const { boardDefaults } = config.get;
const { name, description } = req.body
@ -20,8 +21,8 @@ module.exports = async (req, res) => {
if (restrictedURIs.has(uri)) {
return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad Request',
'message': 'That URI is not available for board creation',
'title': __('Bad Request'),
'message': __('URI "%s" is reserved', uri),
'redirect': '/create.html'
});
}
@ -31,8 +32,8 @@ module.exports = async (req, res) => {
// if board exists reject
if (board != null) {
return dynamicResponse(req, res, 409, 'message', {
'title': 'Conflict',
'message': 'Board with this URI already exists',
'title': __('Conflict'),
'message': __('Board with this URI already exists'),
'redirect': '/create.html'
});
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save