Improvement to grid v1 to allow customising, allowing for something like this.

-true characters
-false characters
-question text
Make optional (and add additional options for) some filters/effects
-paint
-line
-wave
merge-requests/341/head
Thomas Lynch 2 years ago
parent 0461842d14
commit c8ebf9a579
  1. 8
      configs/template.js.example
  2. 3
      controllers/forms/globalsettings.js
  3. 3
      gulpfile.js
  4. 3
      lib/build/render.js
  5. 30
      lib/captcha/generators/grid.js
  6. 32
      lib/captcha/generators/text.js
  7. 19
      migrations/0.8.0.js
  8. 8
      models/forms/changeglobalsettings.js
  9. 2
      package.json
  10. 3
      server.js
  11. 9
      views/custompages/faq.pug
  12. 2
      views/includes/banform.pug
  13. 7
      views/includes/captcha.pug
  14. 2
      views/includes/head.pug
  15. 2
      views/includes/postform.pug
  16. 2
      views/pages/changepassword.pug
  17. 2
      views/pages/create.pug
  18. 43
      views/pages/globalmanagesettings.pug
  19. 2
      views/pages/register.pug

@ -34,7 +34,15 @@ module.exports = {
captchaOptions: {
type: 'text',
generateLimit: 250,
text: {
wave: false,
line: false,
paint: false,
},
grid: {
falses: ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧'],
trues: ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣'],
question: 'Select the solid/filled icons',
size: 4,
imageSize: 120,
iconYOffset: 15,

@ -12,7 +12,8 @@ module.exports = {
paramConverter: paramConverter({
timeFields: ['ban_duration', 'board_defaults_filter_ban_duration', 'default_ban_duration', 'block_bypass_expire_after_time', 'dnsbl_cache_time', 'board_defaults_delete_protection_age'],
trimFields: ['allowed_hosts', 'dnsbl_blacklists', 'other_mime_types', 'highlight_options_language_subset', 'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'archive_links', 'reverse_links'],
trimFields: ['captcha_options_grid_question', 'captcha_options_grid_trues', 'captcha_options_grid_falses','allowed_hosts', 'dnsbl_blacklists', 'other_mime_types',
'highlight_options_language_subset', 'global_limits_custom_css_filters', 'board_defaults_filters', 'filters', 'archive_links', 'reverse_links'],
numberFields: ['filter_mode', 'auth_level',
'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', 'flood_timers_any_content_same_ip',

@ -414,11 +414,10 @@ async function custompages() {
defaultTheme: config.get.boardDefaults.theme,
defaultCodeTheme: config.get.boardDefaults.codeTheme,
postFilesSize: formatSize(config.get.globalLimits.postFilesSize.max),
captchaType: config.get.captchaOptions.type,
googleRecaptchaSiteKey: google.siteKey,
hcaptchaSiteKey: hcaptcha.siteKey,
captchaGridSize: config.get.captchaOptions.grid.size,
globalAnnouncement: config.get.globalAnnouncement,
captchaOptions: config.get.captchaOptions,
commit,
version,
}

@ -34,10 +34,9 @@ const updateLocals = () => {
postFilesSize: formatSize(globalLimits.postFilesSize.max),
globalLimits,
enableWebring,
captchaType: captchaOptions.type,
googleRecaptchaSiteKey: google.siteKey,
hcaptchaSiteKey: hcaptcha.siteKey,
captchaGridSize: captchaOptions.grid.size,
captchaOptions,
globalAnnouncement,
};
};

@ -8,20 +8,18 @@ const gm = require('@fatchan/gm').subClass({ imageMagick: true })
, uploadDirectory = require(__dirname+'/../../file/uploaddirectory.js')
, getDistorts = require(__dirname+'/../getdistorts.js')
, randomRange = promisify(require('crypto').randomInt)
, padding = 30 //pad edge of image to account for character size + distortion
, zeros = ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧']
, ones = ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣'];
, padding = 30; //pad edge of image to account for character size + distortion
module.exports = async () => {
const { captchaOptions } = config.get;
const gridSize = captchaOptions.grid.size;
const width = captchaOptions.grid.imageSize+padding;
const height = captchaOptions.grid.imageSize+padding;
const { size, trues, falses, imageSize } = captchaOptions.grid;
const width = imageSize+padding; //TODO: these will never be different, right?
const height = imageSize+padding;
//number of inputs in grid, just square of gridSize
const numInputs = gridSize ** 2;
//number of inputs in grid, just square of size
const numInputs = size ** 2;
//create an array of true/false for grid from random bytes
const randBuffer = await randomBytes(numInputs);
@ -39,17 +37,21 @@ module.exports = async () => {
.font(__dirname+'/../font.ttf');
//divide the space by grid size, accounting for padding
const spaceSize = (width-padding)/gridSize;
for(let j = 0; j < gridSize; j++) { //for each row
const spaceSize = (width-padding)/size;
for(let j = 0; j < size; j++) { //for each row
//x offset for whole row (not per character or it gets way too difficult to solve)
let cxOffset = await randomRange(0, spaceSize * 1.5);
for(let i = 0; i < gridSize; i++) { //for character in row
const index = (j*gridSize)+i;
for(let i = 0; i < size; i++) { //for character in row
const index = (j*size)+i;
const cyOffset = await randomRange(0, captchaOptions.grid.iconYOffset);
const charIndex = await randomRange(0, ones.length);
const character = (boolArray[index] ? ones : zeros)[charIndex];
let character;
if (boolArray[index] === true) {
character = trues[(await randomRange(0, trues.length))];
} else {
character = falses[(await randomRange(0, falses.length))];
}
captcha.fontSize((await randomRange(20, 30)));
captcha.drawText(
(spaceSize * i) + cxOffset,

@ -20,7 +20,7 @@ const gm = require('@fatchan/gm').subClass({ imageMagick: true })
return 30;
}
}
, totalWidth = (text) => {
, totalTextWidth = (text) => {
return text.split('').reduce((acc, char) => {
return characterWidth(char) + acc + 1;
}, 0);
@ -45,22 +45,40 @@ module.exports = async () => {
//insert the captcha to db and get id
const captchaId = await Captchas.insertOne(text).then(r => r.insertedId);
//y position for line through text
const lineY = await randomRange(35,45);
const captcha = gm(width,height,'#ffffff')
.fill('#000000')
.fontSize(65);
//draw each character at their x based on the characterWidth()
const startX = (width-totalWidth(text))/2;
const textWidth = totalTextWidth(text);
const startX = (width-textWidth)/2;
let charX = startX;
for (let i = 0; i < 6; i++) {
captcha.drawText(charX, 60, text[i]);
charX += characterWidth(text[i]);
}
captcha.drawRectangle(startX, lineY, charX, lineY+4);
/*
TODO: possibilities for customising some of the values in these options.
Some can be converted to number inputs and 0=off?
*/
//draw optional line/strikethrough
if (captchaOptions.text.line) {
const lineY = await randomRange(35,45);
captcha.drawRectangle(startX, lineY, startX+textWidth, lineY+4);
}
//add optional wave effect
if (captchaOptions.text.wave) {
captcha.wave(5, width/4);
}
//add optional paint effect
if (captchaOptions.text.paint) {
captcha.paint(2);
}
//TODO: noise effect
//create an array of distortions and apply to the image, if distortion is enabled
const { distortion, numDistorts } = captchaOptions;
@ -69,8 +87,6 @@ module.exports = async () => {
captcha.distort(distorts, 'Shepards');
}
captcha.paint(2); //paint effect makes a bit harder
return new Promise((resolve, reject) => {
captcha
.write(`${uploadDirectory}/captcha/${captchaId}.jpg`, (err) => {

@ -0,0 +1,19 @@
'use strict';
module.exports = async(db, redis) => {
console.log('add more captcha options');
await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
'$set': {
'captchaOptions.text': {
'line': false,
'wave': false,
'paint': false,
},
'captchaOptions.grid.falses': ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧'],
'captchaOptions.grid.trues': ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣'],
'captchaOptions.grid.question': 'Select the solid/filled icons',
},
});
console.log('Clearing globalsettings cache');
await redis.deletePattern('globalsettings');
};

@ -73,6 +73,14 @@ module.exports = async (req, res) => {
size: numberSetting(req.body.captcha_options_grid_size, oldSettings.captchaOptions.grid.size),
imageSize: numberSetting(req.body.captcha_options_grid_image_size, oldSettings.captchaOptions.grid.imageSize),
iconYOffset: numberSetting(req.body.captcha_options_grid_icon_y_offset, oldSettings.captchaOptions.grid.iconYOffset),
question: trimSetting(req.body.captcha_options_grid_question, oldSettings.captchaOptions.grid.question),
trues: arraySetting(req.body.captcha_options_grid_trues, oldSettings.captchaOptions.grid.trues),
falses: arraySetting(req.body.captcha_options_grid_falses, oldSettings.captchaOptions.grid.falses),
},
text: {
line: booleanSetting(req.body.captcha_options_text_line, oldSettings.captchaOptions.text.line),
wave: booleanSetting(req.body.captcha_options_text_wave, oldSettings.captchaOptions.text.wave),
paint: booleanSetting(req.body.captcha_options_text_paint, oldSettings.captchaOptions.text.paint),
},
numDistorts: {
min: numberSetting(req.body.captcha_options_num_distorts_min, oldSettings.captchaOptions.numDistorts.min),

@ -1,7 +1,7 @@
{
"name": "jschan",
"version": "0.7.1",
"migrateVersion": "0.6.5",
"migrateVersion": "0.8.0",
"description": "",
"main": "server.js",
"dependencies": {

@ -85,12 +85,11 @@ const config = require(__dirname+'/lib/misc/config.js')
app.locals.commit = commit;
app.locals.version = version;
app.locals.meta = meta;
app.locals.captchaType = captchaOptions.type;
app.locals.postFilesSize = formatSize(globalLimits.postFilesSize.max);
app.locals.googleRecaptchaSiteKey = google.siteKey;
app.locals.hcaptchaSiteKey = hcaptcha.siteKey;
app.locals.captchaGridSize = captchaOptions.grid.size;
app.locals.globalAnnouncement = globalAnnouncement;
app.locals.captchaOptions = captchaOptions;
};
loadAppLocals();
redis.addCallback('config', loadAppLocals);

@ -21,7 +21,6 @@ block content
li: a(href='#contact') How can I contact the administration?
b Making posts
ul.mv-0
li: a(href='#captcha') How do I solve the CAPTCHA?
li: a(href='#name-formatting') How do names, tripcodes and capcodes work?
li: a(href='#post-styling') What kind of styling options are available when making a post?
li: a(href='#post-info') What is the file size limit?
@ -42,14 +41,6 @@ block content
p
| The primary difference between imageboards and traditional forums is that anybody can make a post without registering
| an account or providing any personal information. This lowers the barrier to entry, protects user identities and focuses on what is said, rather than who says it.
.table-container.flex-center.mv-5
.anchor#captcha
table
tr
th: a(href='#captcha') How do I solve the CAPTCHA?
tr
td.post-message
| See the #[a(rel='nofollow' referrerpolicy='same-origin' target='_blank' href='http://fatchan.gitgud.site/jschan-docs/#captcha-block-bypass') API docs] for example captchas and solutions.
.table-container.flex-center.mv-5
.anchor#name-formatting
table

@ -10,7 +10,7 @@ form.form-post(action=`/forms/appeal`, enctype='application/x-www-form-urlencode
.row
.label Message
textarea(rows='5' name='message' required)
if captchaType === 'text'
if captchaOptions.type === 'text'
include ./captchasidelabel.pug
else
include ./captchafieldrow.pug

@ -1,4 +1,4 @@
case captchaType
case captchaOptions.type
when 'google'
div(class='g-recaptcha' data-sitekey=`${googleRecaptchaSiteKey}` data-theme='dark' data-size='compact' data-callback='recaptchaCallback')
noscript Please enable JavaScript to solve the captcha.
@ -11,14 +11,13 @@ case captchaType
.jsonly.captcha(style='display:none;')
input.captchafield(type='text' name='captcha' autocomplete='off' placeholder='Captcha text' pattern='.{6}' required title='6 characters')
when 'grid'
unless minimal
a.text-center(href='/faq.html#captcha') Instructions
span.text-center #{captchaOptions.grid.question}
.catalog
noscript.no-m-p
iframe.captcha(src='/captcha.html' width='150' height='150' scrolling='no' loading='lazy')
.jsonly.captcha(style='display:none')
.captchafield.noselect
- for(let i = 0; i < captchaGridSize**2; i++) {
- for(let i = 0; i < captchaOptions.grid.size**2; i++) {
label.captchachecklabel
input(type='checkbox' name='captcha' value=i)
span.captchacheckbox

@ -18,7 +18,7 @@ meta(property='og:site_name', value=meta.siteName)
meta(property='og:url', content=meta.url)
//- main stylesheet
link(rel='stylesheet' href=`/css/style.css?v=${commit}&ct=${captchaType}`)
link(rel='stylesheet' href=`/css/style.css?v=${commit}&ct=${captchaOptions.type}`)
//- theme stylesheets
- const theme = isBoard ? board.settings.theme : defaultTheme;

@ -79,7 +79,7 @@ section.form-wrapper.flex-center
option(value=flag[0] data-src=`/flag/${board._id}/${flag[1]}`) #{flag[0]}
img.jsonly#selected-flag
if ((board.settings.captchaMode === 1 && !isThread) || board.settings.captchaMode === 2) && !modview
if captchaType === 'text'
if captchaOptions.type === 'text'
include ./captchasidelabel.pug
else
include ./captchaexpand.pug

@ -19,7 +19,7 @@ block content
.row
.label Confirm New Password
input(type='password', name='newpasswordconfirm', maxlength='100' required)
if captchaType === 'text'
if captchaOptions.type === 'text'
include ../includes/captchasidelabel.pug
else
include ../includes/captchafieldrow.pug

@ -19,7 +19,7 @@ block content
.row
.label Tags
textarea(name='tags' placeholder='Newline separated, max 10')
if captchaType === 'text'
if captchaOptions.type === 'text'
include ../includes/captchasidelabel.pug
else
include ../includes/captchafieldrow.pug

@ -162,22 +162,53 @@ block content
.label Type
select(name='captcha_options_type')
option(value='text', selected=settings.captchaOptions.type === 'text') Text
option(value='grid', selected=settings.captchaOptions.type === 'grid') Grid
option(value='grid', selected=settings.captchaOptions.type === 'grid') Grid v1
option(value='grid', selected=settings.captchaOptions.type === 'grid2' disabled=true) Grid v2 (Coming soon)
option(value='google', selected=settings.captchaOptions.type === 'google') Google
option(value='hcaptcha', selected=settings.captchaOptions.type === 'hcaptcha') Hcaptcha
.row
.label Grid Image Size
.label Generate Limit
input(type='number' name='captcha_options_generate_limit' value=settings.captchaOptions.generateLimit)
.row
h4.mv-5 Grid v1 Captcha Options
.row
.label Image Size
input(type='number' name='captcha_options_grid_image_size' value=settings.captchaOptions.grid.imageSize)
.row
.label Grid Size
input(type='number' name='captcha_options_grid_size' value=settings.captchaOptions.grid.size)
.row
.label Grid Icon Offset
.label Icon Offset
input(type='number' name='captcha_options_grid_icon_y_offset' value=settings.captchaOptions.grid.iconYOffset)
.row
.label Generate Limit
input(type='number' name='captcha_options_generate_limit' value=settings.captchaOptions.generateLimit)
.label True Characters
textarea(name='captcha_options_grid_trues' placeholder='Newline separated') #{settings.captchaOptions.grid.trues.join('\n')}
.row
.label False Characters
textarea(name='captcha_options_grid_falses' placeholder='Newline separated') #{settings.captchaOptions.grid.falses.join('\n')}
.row
.label Question Text
input(type='text' name='captcha_options_grid_question' value=settings.captchaOptions.grid.question)
.row
h4.mv-5 Text Captcha Options
.row
.label Font
select(name='captcha_options_text_font' disabled)
option(selected=true) Default
.row
.label Strikethrough Effect
label.postform-style.ph-5
input(type='checkbox', name='captcha_options_text_line', value='true' checked=settings.captchaOptions.text.line)
.row
.label Wave Effect
label.postform-style.ph-5
input(type='checkbox', name='captcha_options_text_wave', value='true' checked=settings.captchaOptions.text.wave)
.row
.label Paint Effect
label.postform-style.ph-5
input(type='checkbox', name='captcha_options_text_paint', value='true' checked=settings.captchaOptions.text.text)
.row
h4.mv-5 Captcha Distortion
.row
.label Minimum Distortions
input(type='number' name='captcha_options_num_distorts_min' value=settings.captchaOptions.numDistorts.min)

@ -16,7 +16,7 @@ block content
.row
.label Confirm Password
input(type='password', name='passwordconfirm', maxlength='100' required)
if captchaType === 'text'
if captchaOptions.type === 'text'
include ../includes/captchasidelabel.pug
else
include ../includes/captchafieldrow.pug

Loading…
Cancel
Save