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).
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
{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:(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: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)},
},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: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)},
{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')},
{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.
{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'},
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)},
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)},
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)},
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: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: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)},
{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')},
{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')},
{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')},
},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: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: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.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)},
{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: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: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.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.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.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: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')},
&&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)},
&&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)},
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)},
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)},
{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)},
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
?__(`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);
returndynamicResponse(req,res,400,'message',{
returndynamicResponse(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}`