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. 43
      gulp/res/js/live.js
  49. 3
      gulp/res/js/ptchina-playlist.js
  50. 111
      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. 51
      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. 3
      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 ### 0.11.4
- Bugfix for the message stating how many banners were deleted when deleting banners. - 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). - 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) Join the IRC: [open in client](ircs://irc.fatpeople.lol:6697/general) OR: [webchat](https://irc.fatpeople.lol/#general)
## Features ## Features
- [x] Multiple language support (currently English & Portuguese)
- [x] User created boards ala [infinity](https://github.com/ctrlcctrlv/infinity) - [x] User created boards ala [infinity](https://github.com/ctrlcctrlv/infinity)
- [x] Multiple files per post - [x] Multiple files per post
- [x] Antispam/Anti-flood & DNSBL - [x] Antispam/Anti-flood & DNSBL

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

@ -20,6 +20,7 @@ const express = require('express')
, dnsblCheck = require(__dirname+'/../lib/middleware/ip/dnsbl.js') , dnsblCheck = require(__dirname+'/../lib/middleware/ip/dnsbl.js')
, blockBypass = require(__dirname+'/../lib/middleware/captcha/blockbypass.js') , blockBypass = require(__dirname+'/../lib/middleware/captcha/blockbypass.js')
, fileMiddlewares = require(__dirname+'/../lib/middleware/file/filemiddlewares.js') , fileMiddlewares = require(__dirname+'/../lib/middleware/file/filemiddlewares.js')
, { setBoardLanguage } = require(__dirname+'/../lib/middleware/locale/locale.js')
//controllers //controllers
, { deleteBoardController, editBansController, appealController, globalActionController, twofactorController, , { deleteBoardController, editBansController, appealController, globalActionController, twofactorController,
actionController, addCustomPageController, deleteCustomPageController, addNewsController, actionController, addCustomPageController, deleteCustomPageController, addNewsController,
@ -31,14 +32,14 @@ const express = require('express')
editRoleController, newCaptchaForm, blockBypassForm, logoutForm, deleteSessionsController } = require(__dirname+'/forms/index.js'); editRoleController, newCaptchaForm, blockBypassForm, logoutForm, deleteSessionsController } = require(__dirname+'/forms/index.js');
//make new post //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); 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 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 //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/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, calcPerms, banCheck, isLoggedIn, 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 hasPerms.one(Permissions.MANAGE_BOARD_GENERAL), actionController.paramConverter, actionController.controller); //board manage page
router.post('/global/actions', geoIp, processIp, useSession, sessionRefresh, csrf, calcPerms, isLoggedIn, 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 //appeal ban
router.post('/appeal', geoIp, processIp, useSession, sessionRefresh, appealController.paramConverter, verifyCaptcha, appealController.controller); router.post('/appeal', geoIp, processIp, useSession, sessionRefresh, appealController.paramConverter, verifyCaptcha, appealController.controller);
//edit post //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); hasPerms.any(Permissions.MANAGE_GLOBAL_GENERAL, Permissions.MANAGE_BOARD_GENERAL), editPostController.controller);
//board management forms //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); 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); 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 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 hasPerms.any(Permissions.MANAGE_BOARD_OWNER, Permissions.MANAGE_GLOBAL_BOARDS), deleteBoardController.controller); //delete board
//board crud banners, flags, assets, custompages //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 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 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 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 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 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 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 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 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 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 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 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 hasPerms.one(Permissions.MANAGE_BOARD_STAFF), deleteStaffController.paramConverter, deleteStaffController.controller); //delete board staff
//global management forms //global management forms

@ -21,30 +21,32 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
res.locals.actions = actionChecker(req, res); res.locals.actions = actionChecker(req, res);
const errors = await checkSchema([ const errors = await checkSchema([
{ result: lengthBody(req.body.checkedposts, 1), expected: false, blocking: true, error: 'Must select at least one post' }, { 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(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.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 >${globalLimits.multiInputs.posts.staff} posts per request` }, { 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.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.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) && !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: (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: 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.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.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.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: (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.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 ${globalLimits.fieldLength.report_reason} characters or less` }, { 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 ${globalLimits.fieldLength.ban_reason} characters or less` }, { 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 ${globalLimits.fieldLength.log_message} characters or less` }, { 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.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: (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 () => { { result: async () => {
if (req.body.move && req.body.move_to_thread) { if (req.body.move && req.body.move_to_thread) {
const moveBoard = req.body.move_to_board || req.params.board; const moveBoard = req.body.move_to_board || req.params.board;
@ -52,7 +54,7 @@ module.exports = {
return res.locals.destinationThread != null; return res.locals.destinationThread != null;
} }
return true; return true;
}, expected: true, error: 'Destination for move does not exist' }, }, expected: true, error: __('Destination for move does not exist') },
{ result: async () => { { result: async () => {
if (req.body.move && req.body.move_to_board if (req.body.move && req.body.move_to_board
&& req.body.move_to_board !== req.params.board) { && req.body.move_to_board !== req.params.board) {
@ -69,12 +71,12 @@ module.exports = {
return res.locals.destinationBoard != null; return res.locals.destinationBoard != null;
} }
return true; 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); ], res.locals.permissions);
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/` 'redirect': `/${req.params.board}/`
}); });
@ -88,8 +90,8 @@ module.exports = {
if (!res.locals.posts || res.locals.posts.length === 0) { if (!res.locals.posts || res.locals.posts.length === 0) {
return dynamicResponse(req, res, 404, 'message', { return dynamicResponse(req, res, 404, 'message', {
'title': 'Not found', 'title': __('Not found'),
'error': 'Selected posts not found', 'error': __('Selected posts not found'),
'redirect': `/${req.params.board}/` 'redirect': `/${req.params.board}/`
}); });
} }
@ -100,8 +102,8 @@ module.exports = {
} else if (req.body.move) { } else if (req.body.move) {
if (!res.locals.destinationBoard && !res.locals.destinationThread) { if (!res.locals.destinationBoard && !res.locals.destinationThread) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad Request', 'title': __('Bad Request'),
'error': 'Invalid post move destination', 'error': __('Invalid post move destination'),
'redirect': `/${req.params.board}/` 'redirect': `/${req.params.board}/`
}); });
} }
@ -113,8 +115,8 @@ module.exports = {
}); });
if (res.locals.posts.length === 0) { if (res.locals.posts.length === 0) {
return dynamicResponse(req, res, 409, 'message', { return dynamicResponse(req, res, 409, 'message', {
'title': 'Conflict', 'title': __('Conflict'),
'error': 'Invalid selected posts or destination thread', 'error': __('Invalid selected posts or destination thread'),
'redirect': `/${req.params.board}/` 'redirect': `/${req.params.board}/`
}); });
} }

@ -12,18 +12,20 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: 'Must provide a file' }, { 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.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 ${globalLimits.assetFiles.total}` }, { 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) { if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error); await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });

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

@ -12,18 +12,20 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: 'Must provide a file' }, { 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(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 ${globalLimits.flagFiles.total}` }, { 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) { if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error); await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });

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

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

@ -17,17 +17,19 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.message), expected: true, error: 'Appeals must include a message' }, { 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: 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: 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. ]); //should appeals really be based off message field length global limit? minor.
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/' '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 /* 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. */ 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', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'error': 'Invalid bans selected', 'error': __('Invalid bans selected'),
'redirect': '/' 'redirect': '/'
}); });
} }
return dynamicResponse(req, res, 200, 'message', { return dynamicResponse(req, res, 200, 'message', {
'title': 'Success', 'title': __('Success'),
'message': `Appealed ${amount} bans successfully`, 'message': __('Appealed %s bans successfully', amount),
'redirect': '/' 'redirect': '/'
}); });

@ -7,6 +7,7 @@ const changeBoardSettings = require(__dirname+'/../../models/forms/changeboardse
, dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js')
, config = require(__dirname+'/../../lib/misc/config.js') , config = require(__dirname+'/../../lib/misc/config.js')
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js') , paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
, i18n = require(__dirname+'/../../lib/locale/locale.js')
, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable, , { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
inArrayBody, arrayInBody } = require(__dirname+'/../../lib/input/schema.js'); inArrayBody, arrayInBody } = require(__dirname+'/../../lib/input/schema.js');
@ -14,7 +15,7 @@ module.exports = {
paramConverter: paramConverter({ paramConverter: paramConverter({
timeFields: ['ban_duration', 'delete_protection_age'], 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'], 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', 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', '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) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits, rateLimitCost } = config.get const { globalLimits, rateLimitCost } = config.get
, maxThread = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxThreadMessageLength) || globalLimits.fieldLength.message) , 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); , maxReply = (Math.min(globalLimits.fieldLength.message, res.locals.board.settings.maxReplyMessageLength) || globalLimits.fieldLength.message);
const errors = await checkSchema([ 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.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.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.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.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: 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: "${globalLimits.customCss.filters.join('", "')}"` }, { 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.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-%s characters', globalLimits.fieldLength.boardname) },
{ 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: 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.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 %s-%s', globalLimits.bumpLimit.min, globalLimits.bumpLimit.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 %s-%s', globalLimits.threadLimit.min, globalLimits.threadLimit.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-%s', globalLimits.postFiles.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-%s', globalLimits.fieldLength.message) },
{ 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-%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-${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_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-%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-${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_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: 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, { 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, 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, { 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, 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, { 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, 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, { 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, 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})` }, 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.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.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.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, 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.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, 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.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.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.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.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_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: 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.language, i18n.getLocales()), expected: true, error: __('Invalid language') },
{ result: inArrayBody(req.body.code_theme, codeThemes), expected: true, error: 'Invalid code theme' }, { 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); ], res.locals.permissions);
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/settings.html` '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)); const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'settings', rateLimitCost.boardSettings));
if (ratelimitBoard > 100 || ratelimitIp > 100) { if (ratelimitBoard > 100 || ratelimitIp > 100) {
return dynamicResponse(req, res, 429, 'message', { return dynamicResponse(req, res, 429, 'message', {
'title': 'Ratelimited', 'title': __('Ratelimited'),
'error': 'You are changing settings too quickly, please wait a minute and try again', 'error': __('You are changing settings too quickly, please wait a minute and try again'),
'redirect': `/${req.params.board}/manage/settings.html` 'redirect': `/${req.params.board}/manage/settings.html`
}); });
} }

@ -13,22 +13,24 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' }, { 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: 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: 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: 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: 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: 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: 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: 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: (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: lengthBody(req.body.twofactor, 0, 6), expected: false, error: __('Invalid 2FA code') },
]); ]);
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/changepassword.html' 'redirect': '/changepassword.html'
}); });

@ -15,21 +15,23 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: res.locals.permissions.get(Permissions.CREATE_BOARD), blocking: true, expected: true, error: 'No permission' }, { 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: 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: 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: 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: 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: 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: lengthBody(req.body.name, 0, globalLimits.fieldLength.description), expected: false, error: __('Description must be %s characters or less', globalLimits.fieldLength.description) },
], res.locals.permissions); ], res.locals.permissions);
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/create.html' 'redirect': '/create.html'
}); });

@ -11,16 +11,18 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { staffBoards, ownedBoards } = res.locals.user; const { staffBoards, ownedBoards } = res.locals.user;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.confirm), expected: true, error: 'Missing confirmation' }, { 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: (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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/account.html', 'redirect': '/account.html',
}); });
@ -33,8 +35,8 @@ module.exports = {
} }
return dynamicResponse(req, res, 200, 'message', { return dynamicResponse(req, res, 200, 'message', {
'title': 'Success', 'title': __('Success'),
'message': 'Account deleted', 'message': __('Account deleted'),
'redirect': '/', 'redirect': '/',
}); });

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/globalmanage/accounts.html' 'redirect': '/globalmanage/accounts.html'
}); });

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });
@ -28,8 +30,8 @@ module.exports = {
for (let i = 0; i < req.body.checkedassets.length; i++) { for (let i = 0; i < req.body.checkedassets.length; i++) {
if (!res.locals.board.assets.includes(req.body.checkedassets[i])) { if (!res.locals.board.assets.includes(req.body.checkedassets[i])) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'message': 'Invalid assets selected', 'message': __('Invalid assets selected'),
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });
} }

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });
@ -28,8 +30,8 @@ module.exports = {
for (let i = 0; i < req.body.checkedbanners.length; i++) { for (let i = 0; i < req.body.checkedbanners.length; i++) {
if (!res.locals.board.banners.includes(req.body.checkedbanners[i])) { if (!res.locals.board.banners.includes(req.body.checkedbanners[i])) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'message': 'Invalid banners selected', 'message': __('Invalid banners selected'),
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });
} }

@ -14,21 +14,23 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
let board = null; let board = null;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.confirm), expected: true, error: 'Missing confirmation' }, { result: existsBody(req.body.confirm), expected: true, error: __('Missing confirmation') },
{ result: existsBody(req.body.uri), expected: true, error: 'Missing URI' }, { 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: 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: req.params.board == null || (req.params.board === req.body.uri), expected: true, error: __('URI does not match current board') },
{ result: async () => { { result: async () => {
board = await Boards.findOne(req.body.uri); board = await Boards.findOne(req.body.uri);
return board != null; 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': req.params.board ? `/${req.params.board}/manage/settings.html` : '/globalmanage/settings.html' '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', { return dynamicResponse(req, res, 200, 'message', {
'title': 'Success', 'title': __('Success'),
'message': 'Board deleted', 'message': __('Board deleted'),
'redirect': req.params.board ? '/' : '/globalmanage/settings.html' 'redirect': req.params.board ? '/' : '/globalmanage/settings.html'
}); });

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/custompages.html` 'redirect': `/${req.params.board}/manage/custompages.html`
}); });

@ -13,13 +13,15 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });
@ -28,8 +30,8 @@ module.exports = {
for (let i = 0; i < req.body.checkedflags.length; i++) { for (let i = 0; i < req.body.checkedflags.length; i++) {
if (!res.locals.board.flags[req.body.checkedflags[i]]) { if (!res.locals.board.flags[req.body.checkedflags[i]]) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'message': 'Invalid flags selected', 'message': __('Invalid flags selected'),
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });
} }

@ -14,13 +14,15 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/globalmanage/news.html' 'redirect': '/globalmanage/news.html'
}); });

@ -13,19 +13,21 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const username = res.locals.user.username; const username = res.locals.user.username;
const errors = await checkSchema([ 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: () => { { result: () => {
//return if any input "session ids" dont start with sess: or dont end with :username //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}`)); 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/sessions.html', 'redirect': '/sessions.html',
}); });
@ -38,8 +40,8 @@ module.exports = {
} }
return dynamicResponse(req, res, 200, 'message', { return dynamicResponse(req, res, 200, 'message', {
'title': 'Success', 'title': __('Success'),
'message': 'Sessions deleted', 'message': __('Sessions deleted'),
'redirect': '/sessions.html', //if deleting all, will get redirected back to login anyway 'redirect': '/sessions.html', //if deleting all, will get redirected back to login anyway
}); });

@ -14,17 +14,19 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: lengthBody(req.body.checkedstaff, 1), expected: false, error: 'Must select at least one staff to delete' }, { 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 => !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: 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 //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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`, 'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
}); });

@ -17,19 +17,21 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' }, { 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: 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: alphaNumericRegex.test(req.body.username), expected: true, error: __('Username must contain a-z 0-9 only') },
{ result: async () => { { result: async () => {
res.locals.editingAccount = await Accounts.findOne(req.body.username); res.locals.editingAccount = await Accounts.findOne(req.body.username);
return res.locals.editingAccount != null; return res.locals.editingAccount != null;
}, expected: true, blocking: true, error: 'Invalid account username' }, }, 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: (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 { 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, || 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]), 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: () => { { 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. //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)) { if (!existsBody(req.body.template)) {
@ -38,12 +40,12 @@ module.exports = {
const editingPermission = new Permission(res.locals.editingAccount.permissions); const editingPermission = new Permission(res.locals.editingAccount.permissions);
return !editingPermission.get(Permissions.ROOT); 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`, 'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
}); });

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

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

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

@ -18,25 +18,27 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { rateLimitCost, globalLimits } = config.get; const { rateLimitCost, globalLimits } = config.get;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.board), expected: true, error: 'Missing board' }, { result: existsBody(req.body.board), expected: true, error: __('Missing board') },
{ result: numberBody(req.body.postId, 1), expected: true, error: 'Missing postId' }, { 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.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 ${globalLimits.fieldLength.name} characters or less` }, { 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 ${globalLimits.fieldLength.subject} characters or less` }, { 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 ${globalLimits.fieldLength.email} characters or less` }, { 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 ${globalLimits.fieldLength.log_message} characters or less` }, { 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 () => { { result: async () => {
res.locals.post = await Posts.getPost(req.body.board, req.body.postId); res.locals.post = await Posts.getPost(req.body.board, req.body.postId);
return res.locals.post != null; return res.locals.post != null;
}, expected: true, error: 'Post doesn\'t exist' } }, expected: true, error: __('Post doesn\'t exist') },
]); ]);
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
}); });
} }
@ -46,8 +48,8 @@ module.exports = {
const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'edit', rateLimitCost.editPost)); const ratelimitIp = res.locals.anonymizer ? 0 : (await Ratelimits.incrmentQuota(res.locals.ip.cloak, 'edit', rateLimitCost.editPost));
if (ratelimitUser > 100 || ratelimitIp > 100) { if (ratelimitUser > 100 || ratelimitIp > 100) {
return dynamicResponse(req, res, 429, 'message', { return dynamicResponse(req, res, 429, 'message', {
'title': 'Ratelimited', 'title': __('Ratelimited'),
'error': 'You are editing posts too quickly, please wait a minute and try again', '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) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ 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 () => { { result: async () => {
res.locals.editingRole = await Roles.findOne(req.body.roleid); res.locals.editingRole = await Roles.findOne(req.body.roleid);
return res.locals.editingRole != null && res.locals.editingRole.name !== 'ROOT'; 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/roles.html`, 'redirect': req.headers.referer || `/${req.params.board}/manage/roles.html`,
}); });

@ -13,18 +13,20 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing username' }, { 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: 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: 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: (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: (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: (res.locals.user.username === req.body.username), expected: false, error: __('You can\'t edit your own permissions') },
]); ]);
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`, 'redirect': req.headers.referer || `/${req.params.board}/manage/staff.html`,
}); });

@ -20,31 +20,33 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
res.locals.actions = actionChecker(req, res); res.locals.actions = actionChecker(req, res);
const errors = await checkSchema([ const errors = await checkSchema([
{ result: lengthBody(req.body.globalcheckedposts, 1), expected: false, blocking: true, error: 'Must select at least one post' }, { 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(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: 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.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.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), expected: false, error: __('Must check parent post if checking reports for report action') },
{ result: (existsBody(req.body.checkedreports) && req.body.globalcheckedposts { 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' }, && 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.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: 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: (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.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 ${globalLimits.fieldLength.ban_reason} characters or less` }, { 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 ${globalLimits.fieldLength.log_message} characters or less` }, { 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 //return the errors
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/globalmanage/reports.html' 'redirect': '/globalmanage/reports.html'
}); });
@ -58,8 +60,8 @@ module.exports = {
} }
if (!res.locals.posts || res.locals.posts.length === 0) { if (!res.locals.posts || res.locals.posts.length === 0) {
return dynamicResponse(req, res, 404, 'message', { return dynamicResponse(req, res, 404, 'message', {
'title': 'Not found', 'title': __('Not found'),
'error': 'Selected posts not found', 'error': __('Selected posts not found'),
'redirect': '/globalmanage/reports.html' 'redirect': '/globalmanage/reports.html'
}); });
} }

@ -6,6 +6,7 @@ const changeGlobalSettings = require(__dirname+'/../../models/forms/changeglobal
, config = require(__dirname+'/../../lib/misc/config.js') , config = require(__dirname+'/../../lib/misc/config.js')
, { fontPaths } = require(__dirname+'/../../lib/misc/fonts.js') , { fontPaths } = require(__dirname+'/../../lib/misc/fonts.js')
, paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js') , paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js')
, i18n = require(__dirname+'/../../lib/locale/locale.js')
, { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable, , { checkSchema, lengthBody, numberBody, minmaxBody, numberBodyVariable,
inArrayBody } = require(__dirname+'/../../lib/input/schema.js'); inArrayBody } = require(__dirname+'/../../lib/input/schema.js');
@ -14,7 +15,7 @@ module.exports = {
paramConverter: paramConverter({ 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'], 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', 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', 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_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', '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) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
const errors = await checkSchema([ const errors = await checkSchema([
@ -46,7 +49,7 @@ module.exports = {
return /\.[a-z0-9]+/i.test(req.body.thumb_extension); return /\.[a-z0-9]+/i.test(req.body.thumb_extension);
} }
return false; return false;
}, expected: true, error: 'Thumb extension must be like .xxx' }, }, expected: true, error: __('Thumb extension must be like .xxx') },
{ result: () => { { result: () => {
if (req.body.other_mime_types) { if (req.body.other_mime_types) {
return req.body.other_mime_types return req.body.other_mime_types
@ -56,155 +59,156 @@ module.exports = {
}); });
} }
return false; 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: () => { { result: () => {
if (req.body.archive_links) { if (req.body.archive_links) {
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
return /https?\:\/\/[^\s<>\[\]{}|\\^]+%s[^\s<>\[\]{}|\\^]*/i.test(req.body.archive_links); return /https?\:\/\/[^\s<>\[\]{}|\\^]+%s[^\s<>\[\]{}|\\^]*/i.test(req.body.archive_links);
} }
return false; 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: () => { { result: () => {
if (req.body.reverse_links) { if (req.body.reverse_links) {
return /https?\:\/\/[^\s<>\[\]{}|\\^]+%s[^\s<>\[\]{}|\\^]*/i.test(req.body.reverse_links); return /https?\:\/\/[^\s<>\[\]{}|\\^]+%s[^\s<>\[\]{}|\\^]*/i.test(req.body.reverse_links);
} }
return false; return false;
}, expected: true, error: 'Invalid reverse image search links URL format, must be a link containing %s where the url param belongs.' }, }, 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_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.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: 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.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: 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.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: 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.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.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.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_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: 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: inArrayBody(req.body.language, i18n.getLocales()), expected: true, error: __('Invalid language') },
{ result: numberBody(req.body.captcha_options_generate_limit, 1), expected: true, error: 'Captcha options generate limit must be a number > 0' }, { 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_grid_size, 2, 6), expected: true, error: 'Captcha options grid size must be a number from 2-6' }, { 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_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_size, 2, 6), expected: true, error: __('Captcha options grid size must be a number from 2-6') },
{ 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_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_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_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_max, 0, 10), expected: true, error: 'Captcha options max distorts must be a number from 0-10' }, { 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: 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_num_distorts_max, 0, 10), expected: true, error: __('Captcha options max distorts must be a number from 0-10') },
{ result: numberBody(req.body.captcha_options_distortion, 0, 50), expected: true, error: 'Captcha options distortion must be a number from 0-50' }, { 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: inArrayBody(req.body.captcha_options_font, fontPaths), expected: true, error: 'Invalid captcha options font' }, { result: numberBody(req.body.captcha_options_distortion, 0, 50), expected: true, error: __('Captcha options distortion must be a number from 0-50') },
{ 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: inArrayBody(req.body.captcha_options_font, fontPaths), expected: true, error: __('Invalid captcha options font') },
{ 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_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_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_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_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_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_edge, 0, 50), expected: true, error: 'Captcha options grid edge effect strength must be a number from 0-50' }, { 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.dnsbl_cache_time), expected: true, error: 'Invalid dnsbl cache time' }, { 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.flood_timers_same_content_same_ip), expected: true, error: 'Invalid flood time same content same ip' }, { result: numberBody(req.body.dnsbl_cache_time), expected: true, error: __('Invalid dnsbl cache time') },
{ 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_same_content_same_ip), expected: true, error: __('Invalid flood time same content same 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.flood_timers_same_content_any_ip), expected: true, error: __('Invalid flood time same contenet any 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.flood_timers_any_content_same_ip), expected: true, error: __('Invalid flood time any content same ip') },
{ result: numberBody(req.body.block_bypass_expire_after_time), expected: true, error: 'Invalid block bypass expire after time' }, { 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.rate_limit_cost_captcha, 1, 100), expected: true, error: 'Rate limit cost captcha must be a number from 1-100' }, { 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_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_captcha, 1, 100), expected: true, error: __('Rate limit cost captcha 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.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.hot_threads_limit), expected: true, error: 'Invalid hot threads limit' }, { 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_threshold), expected: true, error: 'Invalid hot threads threshold' }, { result: numberBody(req.body.hot_threads_limit), expected: true, error: __('Invalid hot threads limit') },
{ result: numberBody(req.body.hot_threads_max_age), expected: true, error: 'Invalid hot threads max age' }, { result: numberBody(req.body.hot_threads_threshold), expected: true, error: __('Invalid hot threads threshold') },
{ result: numberBody(req.body.overboard_limit), expected: true, error: 'Invalid overboard limit' }, { result: numberBody(req.body.hot_threads_max_age), expected: true, error: __('Invalid hot threads max age') },
{ result: numberBody(req.body.overboard_catalog_limit), expected: true, error: 'Invalid overboard catalog limit' }, { result: numberBody(req.body.overboard_limit), expected: true, error: __('Invalid overboard limit') },
{ result: numberBody(req.body.lock_wait), expected: true, error: 'Invalid lock wait' }, { result: numberBody(req.body.overboard_catalog_limit), expected: true, error: __('Invalid overboard catalog limit') },
{ result: numberBody(req.body.prune_modlogs), expected: true, error: 'Prune modlogs must be a number of days' }, { result: numberBody(req.body.lock_wait), expected: true, error: __('Invalid lock wait') },
{ result: numberBody(req.body.prune_ips), expected: true, error: 'Prune ips must be a number of days' }, { result: numberBody(req.body.prune_modlogs), expected: true, error: __('Prune modlogs 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.prune_ips), expected: true, error: __('Prune ips must be a number of days') },
{ result: numberBody(req.body.thumb_size), expected: true, error: 'Invalid thumbnail size' }, { result: lengthBody(req.body.thumb_extension, 1), expected: false, error: __('Thumbnail extension must be at least 1 character') },
{ 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.thumb_size), expected: true, error: __('Invalid thumbnail size') },
{ result: numberBody(req.body.default_ban_duration), expected: true, error: 'Invalid default ban duration' }, { 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.quote_limit), expected: true, error: 'Quote limit must be a number' }, { result: numberBody(req.body.default_ban_duration), expected: true, error: __('Invalid default ban duration') },
{ result: numberBody(req.body.preview_replies), expected: true, error: 'Preview replies must be a number' }, { result: numberBody(req.body.quote_limit), expected: true, error: __('Quote limit 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.preview_replies), expected: true, error: __('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.sticky_preview_replies), expected: true, error: __('Sticky preview replies 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.early_404_fraction), 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: numberBody(req.body.early_404_replies), expected: true, error: __('Early 404 fraction 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: numberBody(req.body.max_recent_news), expected: true, error: __('Max recent news must be a number') },
{ 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.space_file_name_replacement, 1, 1), expected: false, error: __('Space file name replacement must be 1 character') },
{ result: lengthBody(req.body.highlight_options_threshold), expected: false, error: 'Highlight options threshold must be a number' }, { result: lengthBody(req.body.highlight_options_language_subset, 0, 10000), expected: false, error: __('Highlight options language subset must not exceed 10000 characters') },
{ result: numberBody(req.body.global_limits_thread_limit_min), expected: true, error: 'Global thread limit minimum must be a number' }, { 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_max), expected: true, error: 'Global thread limit maximum 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: 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_thread_limit_max), expected: true, error: __('Global thread limit maximum must be a number') },
{ result: numberBody(req.body.global_limits_reply_limit_min), expected: true, error: 'Global reply limit minimum 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_max), expected: true, error: 'Global reply limit maximum must be a number' }, { result: numberBody(req.body.global_limits_reply_limit_min), expected: true, error: __('Global reply limit minimum 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_reply_limit_max), expected: true, error: __('Global reply limit maximum must be a number') },
{ result: numberBody(req.body.global_limits_bump_limit_min), expected: true, error: 'Global bump limit minimum 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_max), expected: true, error: 'Global bump limit minimum must be a number' }, { result: numberBody(req.body.global_limits_bump_limit_min), 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_bump_limit_max), expected: true, error: __('Global bump limit minimum must be a number') },
{ result: numberBody(req.body.global_limits_post_files_max), expected: true, error: 'Post files max 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_size_max), expected: true, error: 'Post files size must be a number' }, { 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_image_resolution), expected: true, error: 'Image resolution 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_video_resolution), expected: true, error: 'Video resolution max 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_banner_files_width, 1), expected: true, error: 'Banner files height must be a number > 0' }, { 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_height, 1), expected: true, error: 'Banner files width must be a number > 0' }, { 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_size_max), expected: true, error: 'Banner files size must be a number' }, { 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_max), expected: true, error: 'Banner files max must be a number' }, { 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_total), expected: true, error: 'Banner files total 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_flag_files_size_max), expected: true, error: 'Flag files size 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_max), expected: true, error: 'Flag files max 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_total), expected: true, error: 'Flag files total 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_asset_files_size_max), expected: true, error: 'Asset files size 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_max), expected: true, error: 'Asset files max 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_total), expected: true, error: 'Asset files total 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_field_length_name), expected: true, error: 'Global limit name field length 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_email), expected: true, error: 'Global limit email field length 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_subject), expected: true, error: 'Global limit subject 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_postpassword, 20), expected: true, error: 'Global limit postpassword field length must be a number >=20' }, { 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_message), expected: true, error: 'Global limit message 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_report_reason), expected: true, error: 'Global limit report reason field length must be a number' }, { 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_ban_reason), expected: true, error: 'Global limit ban reason 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_log_message), expected: true, error: 'Global limit log message 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_uri), expected: true, error: 'Global limit board uri 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_boardname), expected: true, error: 'Global limit board name 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_description), expected: true, error: 'Global limit board description 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_multi_input_posts_anon), expected: true, error: 'Multi input anon limit 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_staff), expected: true, error: 'Multi input staff limit 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_custom_css_max), expected: true, error: 'Custom css max 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: 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_css_max), expected: true, error: __('Custom css max must be a number') },
{ result: numberBody(req.body.global_limits_custom_pages_max), expected: true, error: 'Custom pages 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_length), expected: true, error: 'Custom pages max length must be a number' }, { result: numberBody(req.body.global_limits_custom_pages_max), expected: true, error: __('Custom pages max must be a number') },
{ result: inArrayBody(req.body.board_defaults_theme, themeHelper.themes), expected: true, error: 'Invalid board default theme' }, { 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_code_theme, themeHelper.codeThemes), expected: true, error: 'Invalid board default code theme' }, { result: inArrayBody(req.body.board_defaults_theme, themeHelper.themes), expected: true, error: __('Invalid board default 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: inArrayBody(req.body.board_defaults_code_theme, themeHelper.codeThemes), expected: true, error: __('Invalid board default code theme') },
{ 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_lock_mode, 0, 2), expected: true, error: __('Board default lock 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_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_captcha_mode, 0, 2), expected: true, error: 'Board default captcha 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_tph_trigger), expected: true, error: 'Board default tph trigger must be a number' }, { 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_pph_trigger), expected: true, error: 'Board default pph trigger must be a number' }, { 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_action, 0, 4), expected: true, error: 'Board default pph trigger action must be a number from 0-4' }, { 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_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_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_captcha_reset, 0, 2), expected: true, error: 'Board defaults captcha reset must be a number from 0-2' }, { 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_lock_reset, 0, 2), expected: true, error: 'Board defaults lock reset must be a number from 0-2' }, { 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: 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: 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_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_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_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_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_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_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_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_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_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: 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: numberBody(req.body.board_defaults_min_thread_message_length), expected: true, error: 'Board defaults min thread message length must be a number' }, { 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_reply_message_length), expected: true, error: 'Board defaults min reply message length must be a number' }, { result: numberBody(req.body.board_defaults_min_thread_message_length), expected: true, error: __('Board defaults min thread 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: 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_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: 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: numberBody(req.body.board_defaults_filter_mode, 0, 2), expected: true, error: 'Board defaults filter mode must be a number from 0-2' }, { 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.frontend_script_default_volume, 0, 100), expected: true, error: 'Default volume must be a number from 0-100' }, { 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_tegaki_width), expected: true, error: 'Tegaki width must be a number' }, { 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_height), expected: true, error: 'Tegaki height must be a number' }, { result: numberBody(req.body.frontend_script_default_tegaki_width), expected: true, error: __('Tegaki width 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.frontend_script_default_tegaki_height), expected: true, error: __('Tegaki height 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_filter_ban_duration), expected: true, error: __('Board defaults filter ban duration must be a number') },
{ result: numberBody(req.body.board_defaults_delete_protection_count, 0), expected: true, error: 'Invalid board defaults OP thread reply count delete protection' }, { result: numberBody(req.body.board_defaults_delete_protection_age, 0), expected: true, error: __('Invalid board defaults OP thread age delete protection') },
{ result: lengthBody(req.body.webring_following, 0, 10000), expected: false, error: 'Webring following list must not exceed 10000 characters' }, { 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_blacklist, 0, 10000), expected: false, error: 'Webring blacklist must not exceed 10000 characters' }, { result: lengthBody(req.body.webring_following, 0, 10000), expected: false, error: __('Webring following list must not exceed 10000 characters') },
{ result: lengthBody(req.body.webring_logos, 0, 10000), expected: false, error: 'Webring logos 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/globalmanage/settings.html' 'redirect': '/globalmanage/settings.html'
}); });

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

@ -20,39 +20,41 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits, disableAnonymizerFilePosting } = config.get; const { globalLimits, disableAnonymizerFilePosting } = config.get;
const hasNoMandatoryFile = globalLimits.postFiles.max !== 0 && res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0; const hasNoMandatoryFile = globalLimits.postFiles.max !== 0 && res.locals.board.settings.maxFiles !== 0 && res.locals.numFiles === 0;
//maybe add more duplicates here? //maybe add more duplicates here?
const errors = await checkSchema([ 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) { 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'}` }, && 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}` }, { 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) { 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) { 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) { result: lengthBody(req.body.message, 1) && (existsBody(req.body.thread)
&& res.locals.board.settings.forceReplyMessage), expected: false, error: 'Replies must include a message' }, && 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.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: 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` }, { 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), { 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), { 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` }, 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 ${globalLimits.fieldLength.postpassword} characters or less` }, { 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 ${globalLimits.fieldLength.name} characters or less` }, { 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 ${globalLimits.fieldLength.subject} characters or less` }, { 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 ${globalLimits.fieldLength.email} characters or less` }, { 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) { if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error); await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread + '.html' : ''}` 'redirect': `/${req.params.board}${req.body.thread ? '/thread/' + req.body.thread + '.html' : ''}`
}); });

@ -14,21 +14,23 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: res.locals.permissions.get(Permissions.CREATE_ACCOUNT), blocking: true, expected: true, error: 'No permission' }, { 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: 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: 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: 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: 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: 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: 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: 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: (req.body.password === req.body.passwordconfirm), expected: true, error: __('Password and password confirmation must match') },
], res.locals.permissions); ], res.locals.permissions);
if (errors.length > 0) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/register.html' 'redirect': '/register.html'
}); });

@ -14,19 +14,21 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.confirm), expected: true, error: 'Missing confirmation' }, { 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: 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: alphaNumericRegex.test(req.body.board), expected: true, error: __('URI must contain a-z 0-9 only') },
{ result: async () => { { result: async () => {
res.locals.board = await Boards.findOne(req.body.board); res.locals.board = await Boards.findOne(req.body.board);
return res.locals.board != null; 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': '/account.html' 'redirect': '/account.html'
}); });

@ -14,20 +14,22 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: existsBody(req.body.username), expected: true, error: 'Missing new owner username' }, { 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: 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: (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: alphaNumericRegex.test(req.body.username), expected: true, error: __('New owner username must contain a-z 0-9 only') },
{ result: async () => { { result: async () => {
res.locals.newOwner = await Accounts.findOne(req.body.username.toLowerCase()); res.locals.newOwner = await Accounts.findOne(req.body.username.toLowerCase());
return res.locals.newOwner != null; 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) { if (errors.length > 0) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/settings.html` 'redirect': `/${req.params.board}/manage/settings.html`
}); });

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

@ -12,18 +12,20 @@ module.exports = {
controller: async (req, res, next) => { controller: async (req, res, next) => {
const { __ } = res.locals;
const { globalLimits } = config.get; const { globalLimits } = config.get;
const errors = await checkSchema([ const errors = await checkSchema([
{ result: res.locals.numFiles === 0, expected: false, blocking: true, error: 'Must provide a file' }, { 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.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 ${globalLimits.bannerFiles.total}` }, { 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) { if (errors.length > 0) {
await deleteTempFiles(req).catch(console.error); await deleteTempFiles(req).catch(console.error);
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'errors': errors, 'errors': errors,
'redirect': `/${req.params.board}/manage/assets.html` 'redirect': `/${req.params.board}/manage/assets.html`
}); });

@ -16,6 +16,7 @@ const express = require('express')
, sessionRefresh = require(__dirname+'/../lib/middleware/permission/sessionrefresh.js') , sessionRefresh = require(__dirname+'/../lib/middleware/permission/sessionrefresh.js')
, csrf = require(__dirname+'/../lib/middleware/misc/csrfmiddleware.js') , csrf = require(__dirname+'/../lib/middleware/misc/csrfmiddleware.js')
, setMinimal = require(__dirname+'/../lib/middleware/misc/setminimal.js') , setMinimal = require(__dirname+'/../lib/middleware/misc/setminimal.js')
, { setBoardLanguage, setQueryLanguage } = require(__dirname+'/../lib/middleware/locale/locale.js')
//page models //page models
, { manageRecent, manageReports, manageAssets, manageSettings, manageBans, editCustomPage, manageMyPermissions, , { manageRecent, manageReports, manageAssets, manageSettings, manageBans, editCustomPage, manageMyPermissions,
manageBoard, manageThread, manageLogs, manageCatalog, manageCustomPages, manageStaff, editStaff, editPost } = require(__dirname+'/../models/pages/manage/') 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 router.get('/catalog.(html|json)', overboardCatalog); //overboard catalog view
//board pages //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/: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, threadParamConverter, Posts.threadExistsMiddleware, thread); //thread view 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, catalog); //catalog router.get('/:board/catalog.(html|json)', Boards.exists, setBoardLanguage, catalog); //catalog
router.get('/:board/logs.(html|json)', Boards.exists, modloglist);//modlog list 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, logParamConverter, modlog); //daily log 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, customPage); //board custom page router.get('/:board/custompage/:page.(html|json)', Boards.exists, setBoardLanguage, customPage); //board custom page
router.get('/:board/banners.(html|json)', Boards.exists, banners); //banners router.get('/:board/banners.(html|json)', Boards.exists, setBoardLanguage, banners); //banners
router.get('/:board/settings.json', Boards.exists, boardSettings); //public board settings router.get('/:board/settings.json', Boards.exists, setBoardLanguage, boardSettings); //public board settings
router.get('/settings.json', globalSettings); //public global settings router.get('/settings.json', globalSettings); //public global settings
router.get('/randombanner', randombanner); //random banner router.get('/randombanner', randombanner); //random banner
//board manage pages //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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); 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); hasPerms.one(Permissions.MANAGE_BOARD_STAFF), csrf, editStaff);
//global manage pages //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); hasPerms.one(Permissions.MANAGE_GLOBAL_ACCOUNTS), csrf, editAccount);
router.get('/globalmanage/editrole/:roleid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, calcPerms, router.get('/globalmanage/editrole/:roleid([a-f0-9]{24}).html', useSession, sessionRefresh, isLoggedIn, calcPerms,
hasPerms.one(Permissions.MANAGE_GLOBAL_ROLES), csrf, roleParamConverter, editRole); hasPerms.one(Permissions.MANAGE_GLOBAL_ROLES), csrf, roleParamConverter, editRole);
//TODO: edit post edit page form, like editnews/editaccount/editrole endpoint
//captcha //captcha
router.get('/captcha', geoIp, processIp, captcha); //get captcha image and cookie router.get('/captcha', geoIp, processIp, captcha); //get captcha image and cookie
router.get('/captcha.html', captchaPage); //iframed for noscript users router.get('/captcha.html', captchaPage); //iframed for noscript users
router.get('/bypass.html', blockBypass); //block bypass page 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 //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 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; cursor: pointer;
} }
#settings::after { #settings::after {
content: "Settings"; content: attr(data-label);
} }
.expand-omitted { .expand-omitted {
background-color: var(--post-color); background-color: var(--post-color);
@ -1113,7 +1113,7 @@ input:invalid, textarea:invalid {
.you:after { .you:after {
margin-left: 3px; margin-left: 3px;
content: '(You)'; content: '(' attr(data-label) ')';
font-weight: lighter; font-weight: lighter;
font-style: italic; font-style: italic;
} }
@ -1640,7 +1640,7 @@ row.wrap.sb .col {
content:attr(title); content:attr(title);
} }
.user-id[title]:hover:after { .user-id[title]:hover:after {
content:"Double tap to highlight" attr(data-count); content: attr(title-mobile);
} }
[title]:hover:before { [title]:hover:before {
content: ''; content: '';

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

@ -1,4 +1,4 @@
/* globals setLocalStorage pugfilters isCatalog captchaController threadWatcher */ /* globals __ setLocalStorage pugfilters isCatalog captchaController threadWatcher */
const getFiltersFromLocalStorage = () => { const getFiltersFromLocalStorage = () => {
const savedFilters = JSON.parse(localStorage.getItem('filters1')); const savedFilters = JSON.parse(localStorage.getItem('filters1'));
return savedFilters.reduce((acc, filter) => { return savedFilters.reduce((acc, filter) => {
@ -79,7 +79,7 @@ const togglePostsHidden = (posts, state, single) => {
} else { } else {
elem.classList['add']('hidden'); 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) { async function videoThumbnail(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const hiddenVideo = document.createElement('video'); const hiddenVideo = document.createElement('video');
@ -517,9 +517,9 @@ class postFormHandler {
if (this.files && this.files.length === 0) { if (this.files && this.files.length === 0) {
this.fileUploadList.textContent = ''; this.fileUploadList.textContent = '';
this.fileUploadList.style.display = 'none'; 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 { } 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; this.fileInput.value = null;
} }

@ -1,4 +1,4 @@
/* globals setLocalStorage */ /* globals __ setLocalStorage */
let imageSources = new Set(JSON.parse(localStorage.getItem('hiddenimages'))); let imageSources = new Set(JSON.parse(localStorage.getItem('hiddenimages')));
let imageSourcesList; let imageSourcesList;
@ -8,7 +8,7 @@ const toggleSource = (source, state) => {
const images = document.querySelectorAll(`img.file-thumb[src="${source}"], img.catalog-thumb[src="${source}"]`); const images = document.querySelectorAll(`img.file-thumb[src="${source}"], img.catalog-thumb[src="${source}"]`);
images.forEach(i => i.classList[state?'add':'remove']('vh')); images.forEach(i => i.classList[state?'add':'remove']('vh'));
const buttons = document.querySelectorAll(`a.hide-image[data-src="${source}"]`); 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); 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 liveEnabled = localStorage.getItem('live') == 'true';
let scrollEnabled = localStorage.getItem('scroll') == 'true'; let scrollEnabled = localStorage.getItem('scroll') == 'true';
let socket; let socket;
@ -34,18 +34,25 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
console.log('got mark post message', data); console.log('got mark post message', data);
const anchor = document.getElementById(data.postId); const anchor = document.getElementById(data.postId);
const postContainer = anchor.nextSibling; const postContainer = anchor.nextSibling;
postContainer.classList.add('marked'); let dataMark = '';
postContainer.setAttribute('data-mark', data.mark);
//handle any special cases for different marks
switch (data.type) { switch (data.type) {
case 'delete': case 'delete':
dataMark = __('Deleted');
break;
case 'move': case 'move':
dataMark = __('Moved');
break;
default:
return;
}
postContainer.classList.add('marked');
postContainer.setAttribute('data-mark', dataMark);
if (postContainer.classList.contains('op')) { if (postContainer.classList.contains('op')) {
//moved or delete OPs then apply to whole thread //moved or delete OPs then apply to whole thread
const postContainers = document.getElementsByClassName('post-container'); const postContainers = document.getElementsByClassName('post-container');
Array.from(postContainers).forEach(e => { Array.from(postContainers).forEach(e => {
e.classList.add('marked'); e.classList.add('marked');
e.setAttribute('data-mark', data.mark); e.setAttribute('data-mark', dataMark);
}); });
//remove new reply buttons and postform //remove new reply buttons and postform
document.getElementById('postform').remove(); document.getElementById('postform').remove();
@ -56,10 +63,6 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
socket.disconnect(); socket.disconnect();
} }
} }
break;
default:
//nothing special
}
}; };
newPost = (data, options = {}) => { newPost = (data, options = {}) => {
@ -159,7 +162,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
jsonPath = jsonParts.join('/'); jsonPath = jsonParts.join('/');
const fetchNewPosts = async () => { const fetchNewPosts = async () => {
console.log('fetching posts from api'); console.log('fetching posts from api');
updateLive('Fetching posts...', 'yellow'); updateLive(__('Fetching posts...'), 'yellow');
let json; let json;
let newPosts = []; let newPosts = [];
try { try {
@ -178,7 +181,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
} }
} }
} }
updateLive('Updated', 'green'); updateLive(__('Updated'), 'green');
return newPosts.length; return newPosts.length;
}; };
@ -225,7 +228,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
const pingStart = Date.now(); const pingStart = Date.now();
socket.volatile.emit('ping', () => { socket.volatile.emit('ping', () => {
const latency = Date.now() - pingStart; const latency = Date.now() - pingStart;
updateLive(`Connected for live posts (${latency}ms)`, '#0de600'); updateLive(__('Connected for live posts (%sms)', latency), '#0de600');
}); });
}; };
const fallbackToPolling = () => { const fallbackToPolling = () => {
@ -249,36 +252,36 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
socket.on('message', (message) => { socket.on('message', (message) => {
console.log(message, room); console.log(message, room);
if (message === 'joined') { if (message === 'joined') {
updateLive('Connected for live posts', '#0de600'); updateLive(__('Connected for live posts'), '#0de600');
socketPing(); socketPing();
} }
}); });
socket.on('reconnect_attempt', () => { socket.on('reconnect_attempt', () => {
updateLive('Attempting to reconnect...', 'yellow'); updateLive(__('Attempting to reconnect...'), 'yellow');
}); });
socket.on('disconnect', () => { socket.on('disconnect', () => {
console.log('lost connection to room'); console.log('lost connection to room');
updateLive('Disconnected', 'red'); updateLive(__('Disconnected'), 'red');
}); });
socket.on('reconnect', () => { socket.on('reconnect', () => {
console.log('reconnected to room'); console.log('reconnected to room');
fetchNewPosts(); fetchNewPosts();
}); });
socket.on('error', (e) => { socket.on('error', (e) => {
updateLive('Socket error', 'orange'); updateLive(__('Socket error'), 'orange');
console.error(e); console.error(e);
}); });
socket.on('connect_error', (e) => { socket.on('connect_error', (e) => {
updateLive('Error connecting', 'orange'); updateLive(__('Error connecting'), 'orange');
console.error(e); console.error(e);
fallbackToPolling(); fallbackToPolling();
}); });
socket.on('reconnect_error', (e) => { socket.on('reconnect_error', (e) => {
updateLive('Error reconnecting', 'orange'); updateLive(__('Error reconnecting'), 'orange');
console.error(e); console.error(e);
}); });
socket.on('reconnect_failed', (e) => { socket.on('reconnect_failed', (e) => {
updateLive('Failed reconnecting', 'orange'); updateLive(__('Failed reconnecting'), 'orange');
console.error(e); console.error(e);
fallbackToPolling(); fallbackToPolling();
}); });
@ -298,7 +301,7 @@ window.addEventListener('settingsReady', function() { //after domcontentloaded
if (socket && supportsWebSockets) { if (socket && supportsWebSockets) {
socket.disconnect(); socket.disconnect();
} }
updateLive('Live posts off', 'darkgray'); updateLive(__('Live posts off'), 'darkgray');
}; };
const liveSetting = document.getElementById('live-setting'); const liveSetting = document.getElementById('live-setting');

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

@ -1,73 +1,74 @@
/*! tegaki.js, MIT License */'use strict';var TegakiStrings = { /*! tegaki.js, MIT License */'use strict';var TegakiStrings = {
// Messages // Messages
badDimensions: 'Invalid dimensions.', badDimensions: __('Invalid dimensions.'),
promptWidth: 'Canvas width in pixels', promptWidth: __('Canvas width in pixels'),
promptHeight: 'Canvas height in pixels', promptHeight: __('Canvas height in pixels'),
confirmDelLayers: 'Delete selected layers?', confirmDelLayers: __('Delete selected layers?'),
confirmMergeLayers: 'Merge selected layers?', confirmMergeLayers: __('Merge selected layers?'),
tooManyLayers: 'Layer limit reached.', tooManyLayers: __('Layer limit reached.'),
errorLoadImage: 'Could not load the image.', errorLoadImage: __('Could not load the image.'),
noActiveLayer: 'No active layer.', noActiveLayer: __('No active layer.'),
hiddenActiveLayer: 'The active layer is not visible.', hiddenActiveLayer: __('The active layer is not visible.'),
confirmCancel: 'Are you sure? Your work will be lost.', confirmCancel: __('Are you sure? Your work will be lost.'),
confirmChangeCanvas: 'Are you sure? Changing the canvas will clear all layers and history and disable replay recording.', confirmChangeCanvas: __('Are you sure? Changing the canvas will clear all layers and history and disable replay recording.'),
// Controls // Controls
color: 'Color', color: __('Color'),
size: 'Size', size: __('Size'),
alpha: 'Opacity', alpha: __('Opacity'),
flow: 'Flow', flow: __('Flow'),
zoom: 'Zoom', zoom: __('Zoom'),
layers: 'Layers', layers: __('Layers'),
switchPalette: 'Switch color palette', switchPalette: __('Switch color palette'),
paletteSlotReplace: 'Right click to replace with the current color', paletteSlotReplace: __('Right click to replace with the current color'),
// Layers // Layers
layer: 'Layer', layer: __('Layer'),
addLayer: 'Add layer', addLayer: __('Add layer'),
delLayers: 'Delete layers', delLayers: __('Delete layers'),
mergeLayers: 'Merge layers', mergeLayers: __('Merge layers'),
moveLayerUp: 'Move up', moveLayerUp: __('Move up'),
moveLayerDown: 'Move down', moveLayerDown: __('Move down'),
toggleVisibility: 'Toggle visibility', toggleVisibility: __('Toggle visibility'),
// Menu bar // Menu bar
newCanvas: 'New', newCanvas: __('New'),
open: 'Open', open: __('Open'),
save: 'Save', save: __('Save'),
saveAs: 'Save As', saveAs: __('Save As'),
export: 'Export', export: __('Export'),
undo: 'Undo', undo: __('Undo'),
redo: 'Redo', redo: __('Redo'),
close: 'Close', close: __('Close'),
finish: 'Finish', finish: __('Finish'),
// Tool modes // Tool modes
tip: 'Tip', tip: __('Tip'),
pressure: 'Pressure', pressure: __('Pressure'),
preserveAlpha: 'Preserve Alpha', preserveAlpha: __('Preserve Alpha'),
// Tools // Tools
pen: 'Pen', pen: __('Pen'),
pencil: 'Pencil', pencil: __('Pencil'),
airbrush: 'Airbrush', airbrush: __('Airbrush'),
pipette: 'Pipette', pipette: __('Pipette'),
blur: 'Blur', blur: __('Blur'),
eraser: 'Eraser', eraser: __('Eraser'),
bucket: 'Bucket', bucket: __('Bucket'),
tone: 'Tone', tone: __('Tone'),
// Replay // Replay
gapless: 'Gapless', gapless: __('Gapless'),
play: 'Play', play: __('Play'),
pause: 'Pause', pause: __('Pause'),
rewind: 'Rewind', rewind: __('Rewind'),
slower: 'Slower', slower: __('Slower'),
faster: 'Faster', faster: __('Faster'),
recordingEnabled: 'Recording replay', recordingEnabled: __('Recording replay'),
errorLoadReplay: 'Could not load the replay: ', errorLoadReplay: __('Could not load the replay: '),
loadingReplay: 'Loading replay…', loadingReplay: __('Loading replay…'),
}; };
class TegakiTool { class TegakiTool {
constructor() { constructor() {
this.id = 0; this.id = 0;

@ -1,4 +1,4 @@
/* globals isThread */ /* globals __n isThread */
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
const statsElem = document.getElementById('threadstats'); const statsElem = document.getElementById('threadstats');
@ -16,7 +16,8 @@ window.addEventListener('DOMContentLoaded', () => {
if (updateId && updateId !== idString) { continue; } if (updateId && updateId !== idString) { continue; }
const count = idMap.get(idString); const count = idMap.get(idString);
idElems[i].setAttribute('data-count', ` (${count})`); 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 numFiles = +statsElem.children[1].innerText.match(/^(\d+)/g);
const filesTotal = numFiles + newFiles; const filesTotal = numFiles + newFiles;
const postTotal = numPosts + 1; const postTotal = numPosts + 1;
statsElem.children[0].innerText = `${postTotal} repl${postTotal === 1 ? 'y' : 'ies'}`; statsElem.children[0].innerText = __n('%s replies', postTotal);
statsElem.children[1].innerText = `${filesTotal} file${filesTotal === 1 ? '' : 's'}`; statsElem.children[1].innerText = __n('%s files', filesTotal);
if (e.detail.json.userId) { if (e.detail.json.userId) {
const userId = e.detail.post.querySelector('.user-id'); const userId = e.detail.post.querySelector('.user-id');
idElems.push(userId); idElems.push(userId);
@ -67,7 +68,7 @@ window.addEventListener('DOMContentLoaded', () => {
statsElem.appendChild(spacer); statsElem.appendChild(spacer);
statsElem.appendChild(uidSpan); 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 relativeTime = localStorage.getItem('relative') == 'true';
let hour24 = localStorage.getItem('24hour') == 'true'; let hour24 = localStorage.getItem('24hour') == 'true';
let localTime = localStorage.getItem('localtime') == 'true'; let localTime = localStorage.getItem('localtime') == 'true';
@ -19,34 +19,34 @@ const YEAR = 31536000000
const relativeTimeString = (date) => { const relativeTimeString = (date) => {
let difference = Date.now() - new Date(date).getTime(); let difference = Date.now() - new Date(date).getTime();
let amount = 0; let amount = 0;
let ret = ''; let unit = '';
let isFuture = false; let isFuture = false;
if (difference < 0) { if (difference < 0) {
difference = Math.abs(difference); difference = Math.abs(difference);
isFuture = true; isFuture = true;
} }
if (difference < MINUTE) { if (difference < MINUTE) {
return 'Now'; return __('Now');
} else if (difference < MINUTE*59.5) { } else if (difference < MINUTE*59.5) {
amount = Math.round(difference / MINUTE); amount = Math.round(difference / MINUTE);
ret = `${amount} minute`; unit = 'minute';
} else if (difference < HOUR*23.5) { } else if (difference < HOUR*23.5) {
amount = Math.round(difference / HOUR); amount = Math.round(difference / HOUR);
ret = `${amount} hour`; unit = 'hour';
} else if (difference < DAY*6.5) { } else if (difference < DAY*6.5) {
amount = Math.round(difference / DAY); amount = Math.round(difference / DAY);
ret = `${amount} day`; unit = 'day';
} else if (difference < WEEK*3.5) { } else if (difference < WEEK*3.5) {
amount = Math.round(difference / WEEK); amount = Math.round(difference / WEEK);
ret = `${amount} week`; unit = 'week';
} else if (difference < MONTH*11.5) { } else if (difference < MONTH*11.5) {
amount = Math.round(difference / MONTH); amount = Math.round(difference / MONTH);
ret = `${amount} month`; unit = 'month';
} else { } else {
amount = Math.round(difference / YEAR); 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) => { const changeDateFormat = (date) => {
@ -56,8 +56,7 @@ const changeDateFormat = (date) => {
if (!localTime) { if (!localTime) {
options.timeZone = SERVER_TIMEZONE; options.timeZone = SERVER_TIMEZONE;
} }
const locale = hour24 ? 'en-US-u-hc-h23' : 'en-US'; const dateString = new Date(date.dateTime).toLocaleString(LANG, options);
const dateString = new Date(date.dateTime).toLocaleString(locale, options);
if (relativeTime) { if (relativeTime) {
date.innerText = relativeTimeString(date.dateTime); date.innerText = relativeTimeString(date.dateTime);
date.title = dateString; date.title = dateString;

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

@ -409,17 +409,9 @@ function deletehtml() {
} }
async function custompages() { async function custompages() {
const formatSize = require(__dirname+'/lib/converter/formatsize.js'); const formatSize = require(__dirname+'/lib/converter/formatsize.js')
return gulp.src([ , i18n = require(__dirname+'/lib/locale/locale.js')
`${paths.pug.src}/custompages/*.pug`, , locals = {
`${paths.pug.src}/pages/404.pug`,
`${paths.pug.src}/pages/500.pug`,
`${paths.pug.src}/pages/502.pug`,
`${paths.pug.src}/pages/503.pug`,
`${paths.pug.src}/pages/504.pug`
])
.pipe(gulppug({
locals: {
Permissions, Permissions,
early404Fraction: config.get.early404Fraction, early404Fraction: config.get.early404Fraction,
early404Replies: config.get.early404Replies, early404Replies: config.get.early404Replies,
@ -438,11 +430,40 @@ async function custompages() {
captchaOptions: config.get.captchaOptions, captchaOptions: config.get.captchaOptions,
commit, commit,
version, 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`,
`${paths.pug.src}/pages/500.pug`,
`${paths.pug.src}/pages/502.pug`,
`${paths.pug.src}/pages/503.pug`,
`${paths.pug.src}/pages/504.pug`
])
.pipe(gulppug({ locals }))
.pipe(gulp.dest(paths.pug.dest)); .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() { async function scripts() {
const { themes, codeThemes } = require(__dirname+'/lib/misc/themes.js'); const { themes, codeThemes } = require(__dirname+'/lib/misc/themes.js');
try { try {
@ -497,6 +518,7 @@ const extraLocals = ${JSON.stringify({ meta: config.get.meta, reverseImageLinksU
gulp.src([ gulp.src([
//put scripts in order for dependencies //put scripts in order for dependencies
`${paths.scripts.src}/locals.js`, `${paths.scripts.src}/locals.js`,
`${paths.scripts.src}/i18n.js`,
`${paths.scripts.src}/localstorage.js`, `${paths.scripts.src}/localstorage.js`,
// `${paths.scripts.src}/pugruntime.js`, // `${paths.scripts.src}/pugruntime.js`,
`${paths.scripts.src}/modal.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 //godhelpme
module.exports = { module.exports = {
@ -611,6 +633,7 @@ module.exports = {
cache: gulp.series(cache, closeConnections), cache: gulp.series(cache, closeConnections),
migrate: gulp.series(init, migrate, closeConnections), migrate: gulp.series(init, migrate, closeConnections),
password: gulp.series(init, password, closeConnections), password: gulp.series(init, password, closeConnections),
langs: gulp.series(init, langs, closeConnections),
ips: gulp.series(init, ips, closeConnections), ips: gulp.series(init, ips, closeConnections),
default: gulp.series(init, build, closeConnections), default: gulp.series(init, build, closeConnections),
buildTasks: { //dont include init, etc buildTasks: { //dont include init, etc

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

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

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

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

@ -2,6 +2,9 @@ const { relativeString, relativeColor, durationString } = require('./timeutils.j
describe('timeutils relativeString, relativeColor, durationString', () => { describe('timeutils relativeString, relativeColor, durationString', () => {
const i18n = require(__dirname+'/../locale/locale.js');
i18n.setLocale('en-GB');
const relativeStringCases = [ 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: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'}, { 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) { for(let i in relativeStringCases) {
test(`relativeString should output ${relativeStringCases[i].out} for an input of ${relativeStringCases[i].in}`, () => { 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,7 +16,8 @@ module.exports = (file, geometry, timestamp) => {
}); });
if (timestamp === 0) { if (timestamp === 0) {
//bypass issue with some dumb files like audio album art covert not working with .screenshots //bypass issue with some dumb files like audio album art covert not working with .screenshots
command.inputOptions([ command
.inputOptions([
'-t', '-t',
0 0
]) ])

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

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

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

@ -27,6 +27,8 @@ module.exports = (options) => {
return (req, res, next) => { return (req, res, next) => {
const { __ } = res.locals;
const { timeFields, trimFields, allowedArrays, processThreadIdParam, const { timeFields, trimFields, allowedArrays, processThreadIdParam,
processDateParam, processMessageLength, numberFields, numberArrays, processDateParam, processMessageLength, numberFields, numberArrays,
objectIdParams, objectIdFields, objectIdArrays } = options; objectIdParams, objectIdFields, objectIdArrays } = options;
@ -39,8 +41,8 @@ module.exports = (options) => {
const val = req.body[key]; const val = req.body[key];
if (!allowedArrays.includes(key) && Array.isArray(val)) { if (!allowedArrays.includes(key) && Array.isArray(val)) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'message': 'Malformed input' 'message': __('Malformed input'),
}); });
} else if (allowedArrays.includes(key) && !Array.isArray(val)) { } 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 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) { } catch (e) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'message': 'Malformed input' 'message': __('Malformed input'),
}); });
} }

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

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

@ -51,10 +51,12 @@ module.exports = (req, res, next) => {
}; };
next(); next();
} catch(e) { } catch(e) {
//should never get here
console.error('Ip parse failed', e); console.error('Ip parse failed', e);
const { __ } = res.locals;
return res.status(400).render('message', { return res.status(400).render('message', {
'title': 'Bad request', 'title': __('Bad request'),
'message': 'Malformed IP' //should never get here '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 //referrer is invalid url
} }
if (refererCheck === true && (!req.headers.referer || !validReferer)) { if (refererCheck === true && (!req.headers.referer || !validReferer)) {
const { __ } = res.locals;
return dynamicResponse(req, res, 403, 'message', { return dynamicResponse(req, res, 403, 'message', {
'title': 'Forbidden', 'title': __('Forbidden'),
'message': 'Invalid or missing "Referer" header. Are you posting from the correct URL?' 'message': __('Invalid or missing "Referer" header. Are you posting from the correct URL?'),
}); });
} }
next(); next();

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

@ -5,24 +5,42 @@ const countries = require('i18n-iso-countries')
, extraCountryCodes = ['EU', 'XX', 'T1'] , extraCountryCodes = ['EU', 'XX', 'T1']
, anonymizerCountryCodes = ['TOR', 'LOKI'] , anonymizerCountryCodes = ['TOR', 'LOKI']
, anonymizerCountryCodesSet = new Set(anonymizerCountryCodes) , anonymizerCountryCodesSet = new Set(anonymizerCountryCodes)
, countryCodes = Object.keys(countryNamesMap) , countryCodes = Object.keys(countryNamesMap).concat(extraCountryCodes, anonymizerCountryCodes)
.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... i18n.getLocales()
Object.entries(countryNamesMap) .forEach(locale => {
.filter(e => Array.isArray(e[1])) //for any country with an array of names, const localeExtraCodesMap = { ...extraCountryNames };
.forEach(c => countryNamesMap[c[0]] = c[1][0]); //use the first name for (let code in localeExtraCodesMap) {
localeExtraCodesMap[code] = i18n.__({
countryNamesMap['EU'] = 'Europe'; phrase: localeExtraCodesMap[code],
countryNamesMap['XX'] = 'Unknown'; locale: locale,
countryNamesMap['T1'] = 'Tor Exit Node'; });
countryNamesMap['TOR'] = 'Tor Hidden Service'; }
countryNamesMap['LOKI'] = 'Lokinet SNApp'; /* 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 = { module.exports = {
countryNamesMap,
countryCodes, countryCodes,
countryCodesSet: new Set(countryCodes), countryCodesSet: new Set(countryCodes),
getCountryNames: countries.getNames,
getCountryName: countries.getName,
isAnonymizer: (code) => { isAnonymizer: (code) => {
return anonymizerCountryCodesSet.has(code); return anonymizerCountryCodesSet.has(code);
}, },

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

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

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

@ -7,13 +7,14 @@ const { Bans } = require(__dirname+'/../../db/')
module.exports = async (req, res, hitGlobalFilter, hitLocalFilter, boardFilterMode, globalFilterMode, module.exports = async (req, res, hitGlobalFilter, hitLocalFilter, boardFilterMode, globalFilterMode,
boardFilterBanDuration, globalFilterBanDuration, filterBanAppealable, redirect) => { boardFilterBanDuration, globalFilterBanDuration, filterBanAppealable, redirect) => {
const { __ } = res.locals;
//global filter mode takes prio //global filter mode takes prio
const useFilterMode = hitGlobalFilter ? globalFilterMode : boardFilterMode; const useFilterMode = hitGlobalFilter ? globalFilterMode : boardFilterMode;
if (useFilterMode === 1) { if (useFilterMode === 1) {
return dynamicResponse(req, res, 400, 'message', { return dynamicResponse(req, res, 400, 'message', {
'title': 'Bad request', 'title': __('Bad request'),
'message': 'Your post was blocked by a word filter', 'message': __('Your post was blocked by a word filter'),
'redirect': redirect 'redirect': redirect
}); });
} else { } else {
@ -25,10 +26,10 @@ module.exports = async (req, res, hitGlobalFilter, hitLocalFilter, boardFilterMo
'ip': { 'ip': {
'cloak': res.locals.ip.cloak, 'cloak': res.locals.ip.cloak,
'raw': res.locals.ip.raw, 'raw': res.locals.ip.raw,
'type': res.locals.ip.type,
}, },
'type': res.locals.anonymizer ? 1 : 0,
'range': 0, 'range': 0,
'reason': `${hitGlobalFilter ? 'global ' :''}word filter auto ban`, 'reason': __(`${hitGlobalFilter ? 'global ' :''}word filter auto ban`),
'board': banBoard, 'board': banBoard,
'posts': null, 'posts': null,
'issuer': 'system', //todo: make a "system" property instead? 'issuer': 'system', //todo: make a "system" property instead?
@ -36,7 +37,7 @@ module.exports = async (req, res, hitGlobalFilter, hitLocalFilter, boardFilterMo
'expireAt': banExpiry, 'expireAt': banExpiry,
'allowAppeal': hitGlobalFilter ? filterBanAppealable : true, 'allowAppeal': hitGlobalFilter ? filterBanAppealable : true,
'showUser': true, 'showUser': true,
'note': `${hitGlobalFilter ? 'global ' :''}filter hit: "${hitGlobalFilter || hitLocalFilter}"`, 'note': __(`${hitGlobalFilter ? 'global ' :''}filter hit: "%s"`, (hitGlobalFilter || hitLocalFilter)),
'seen': true, 'seen': true,
}; };
const insertedResult = await Bans.insertOne(ban); 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 = [ const cases = [
{ in: '\'', out: '&#39;' }, { in: '\'', out: '&#39;' },
{ in: '/', out: '&#x2F;' }, { in: '/', out: '&#x2F;' },
@ -13,7 +13,7 @@ describe('escape() - convert some characters to html entities', () => {
]; ];
for(let i in cases) { for(let i in cases) {
test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => { 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 = { 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, 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})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, 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) => { prepare: (force, match, numdice, numsides, operator, modifier, value) => {
if (!force && value) { if (!force && value) {
@ -34,7 +34,6 @@ module.exports = {
numdice = parseInt(numdice); numdice = parseInt(numdice);
numsides = parseInt(numsides); numsides = parseInt(numsides);
value = parseInt(value); value = parseInt(value);
let matchWithoutValue = match.replace(/&#x3D;.*/, ''); return `<img src='/file/dice.png' height='16' width='16' /><span class='dice'>(##${numdice}%${numsides}${modifier ? operator+modifier : '' }) = ${value}</span>`;
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>`;
}, },
}; };

@ -3,12 +3,12 @@ const diceroll = require('./diceroll.js');
describe('diceroll markdown', () => { describe('diceroll markdown', () => {
const prepareCases = [ const prepareCases = [
{ in: '##3d6', out: '##3d6=' }, { in: '##3%6', out: '##3%6=' },
{ in: '##99d99', out: '##99d99=' }, { in: '##99%99', out: '##99%99=' },
{ in: '##999d999', out: '##999d999' }, { in: '##999%999', out: '##999%999' },
{ in: '##3d8+5', out: '##3d8+5=' }, { in: '##3%8+5', out: '##3%8+5=' },
{ in: '##3d8-5', out: '##3d8-5=' }, { in: '##3%8-5', out: '##3%8-5=' },
{ in: '##0d0', out: '##0d0' }, { in: '##0%0', out: '##0%0' },
]; ];
for(let i in prepareCases) { for(let i in prepareCases) {
test(`should contain ${prepareCases[i].out} for an input of ${prepareCases[i].in}`, () => { test(`should contain ${prepareCases[i].out} for an input of ${prepareCases[i].in}`, () => {
@ -18,10 +18,11 @@ describe('diceroll markdown', () => {
} }
const markdownCases = [ const markdownCases = [
{ in: '##3d6&#x3D;10', out: 'Rolled 3 dice with 6 sides =' }, { in: '##3%6&#x3D;10', out: '(##3%6)' },
{ in: '##99d99&#x3D;5138', out: 'Rolled 99 dice with 99 sides =' }, { in: '##99%99&#x3D;5138', out: '(##99%99)' },
{ in: '##999d999&#x3D;10000', out: '##999d999&#x3D;10000' }, { in: '##999%999&#x3D;10000', out: '##999%999&#x3D;' },
{ in: '##0d0', out: '##0d0' }, { in: '##0%0&#x3D;10', out: '##0%0&#x3D;' },
{ in: '##0%0', out: '##0%0' },
]; ];
for(let i in markdownCases) { for(let i in markdownCases) {
test(`should contain ${markdownCases[i].out} for an input of ${markdownCases[i].in}`, () => { 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 , includeSplitRegex = /(\[code\][\s\S]+?\[\/code\])/gm
, splitRegex = /\[code\]([\s\S]+?)\[\/code\]/gm , splitRegex = /\[code\]([\s\S]+?)\[\/code\]/gm
, trimNewlineRegex = /^(\s*\r?\n)*/g , trimNewlineRegex = /^(\s*\r?\n)*/g
, escape = require(__dirname+'/escape.js') , simpleEscape = require(__dirname+'/escape.js')
, { highlight, highlightAuto, listLanguages } = require('highlight.js') , { highlight, highlightAuto, listLanguages } = require('highlight.js')
, validLanguages = listLanguages() //precompute , validLanguages = listLanguages() //precompute
, { addCallback } = require(__dirname+'/../../redis/redis.js') , { addCallback } = require(__dirname+'/../../redis/redis.js')
@ -72,7 +72,7 @@ module.exports = {
for (let i = 0; i < chunks.length; i++) { for (let i = 0; i < chunks.length; i++) {
//every other chunk will be a code block //every other chunk will be a code block
if (i % 2 === 0) { 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 const newlineFix = escaped.replace(/^\r?\n/,''); //fix ending newline because of codeblock
chunks[i] = module.exports.processRegularChunk(newlineFix, permissions); chunks[i] = module.exports.processRegularChunk(newlineFix, permissions);
} else if (permissions.get(Permissions.USE_MARKDOWN_CODE)){ } else if (permissions.get(Permissions.USE_MARKDOWN_CODE)){
@ -91,18 +91,18 @@ module.exports = {
} }
if (!lang) { if (!lang) {
//no language specified, try automatic syntax highlighting //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) { 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') { } 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)) { } else if (validLanguages.includes(lang)) {
const { value } = highlight(trimFix, { language: lang, ignoreIllegals: true }); 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' //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) => { processRegularChunk: (text, permissions) => {

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

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

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

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

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

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

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

@ -5,10 +5,11 @@ const { Bans } = require(__dirname+'/../../db/')
module.exports = async (req, res) => { module.exports = async (req, res) => {
const { __, __n } = res.locals;
const { defaultBanDuration } = config.get; const { defaultBanDuration } = config.get;
const banDate = new Date(); const banDate = new Date();
const banExpiry = new Date(banDate.getTime() + (req.body.ban_duration || defaultBanDuration)); //uses config default if missing or malformed 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 allowAppeal = (req.body.no_appeal || req.body.ban_q || req.body.ban_h) ? false : true; //dont allow appeals for range bans
const bans = []; const bans = [];
@ -105,7 +106,7 @@ module.exports = async (req, res) => {
const numBans = await Bans.insertMany(bans).then(result => result.insertedCount); const numBans = await Bans.insertMany(bans).then(result => result.insertedCount);
const query = { const query = {
message: `Added ${numBans} bans`, message: __n('Added %s bans', numBans),
}; };
if ((req.body.ban || req.body.global_ban ) && req.body.ban_reason) { 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) => { module.exports = async (req, res) => {
const { __ } = res.locals;
const { secureCookies, blockBypass } = config.get; const { secureCookies, blockBypass } = config.get;
const existingBypassId = req.signedCookies.bypassid || res.locals.pseudoIp; const existingBypassId = req.signedCookies.bypassid || res.locals.pseudoIp;
const bypass = await Bypass.getBypass(res.locals.anonymizer, existingBypassId, blockBypass.expireAfterUses); 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', { return dynamicResponse(req, res, 200, 'message', {
'minimal': req.body.minimal, 'minimal': req.body.minimal,
'title': 'Success', 'title': __('Success'),
'message': 'Completed block bypass, you may go back and make your post.', 'message': __('Completed block bypass, you may go back and make your post.'),
}); });
}; };

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

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

@ -29,6 +29,7 @@ const { Boards } = require(__dirname+'/../../db/')
'codeThemes': ['scripts'], 'codeThemes': ['scripts'],
'globalLimits.postFiles.max': ['deletehtml', 'custompages'], 'globalLimits.postFiles.max': ['deletehtml', 'custompages'],
'globalLimits.postFilesSize.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 //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.* //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 //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) => { module.exports = async (req, res) => {
const { __ } = res.locals;
const promises = []; const promises = [];
const oldSettings = config.get; const oldSettings = config.get;
@ -65,6 +67,7 @@ module.exports = async (req, res) => {
siteName: trimSetting(req.body.meta_site_name, oldSettings.meta.siteName), siteName: trimSetting(req.body.meta_site_name, oldSettings.meta.siteName),
url: trimSetting(req.body.meta_url, oldSettings.meta.url), url: trimSetting(req.body.meta_url, oldSettings.meta.url),
}, },
language: trimSetting(req.body.language, oldSettings.language),
captchaOptions: { captchaOptions: {
type: trimSetting(req.body.captcha_options_type, oldSettings.captchaOptions.type), type: trimSetting(req.body.captcha_options_type, oldSettings.captchaOptions.type),
generateLimit: numberSetting(req.body.captcha_options_generate_limit, oldSettings.captchaOptions.generateLimit), 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', { return dynamicResponse(req, res, 200, 'message', {
'title': 'Success', 'title': __('Success'),
'message': 'Updated settings.', 'message': __('Updated settings.'),
'redirect': '/globalmanage/settings.html' 'redirect': '/globalmanage/settings.html'
}); });

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

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

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

Loading…
Cancel
Save