Merge branch '489-more-captcha-customisation' into 'develop'

489-more-captcha-customisation

See merge request fatchan/jschan!269
jschan
Thomas Lynch 2 years ago
commit caab78c0ef
  1. 2
      .gitlab-ci.yml
  2. 4
      CHANGELOG.md
  3. 2
      INSTALLATION.md
  4. 2
      configs/template.js.example
  5. 4
      controllers/forms/globalsettings.js
  6. BIN
      lib/captcha/font.ttf
  7. 10
      lib/captcha/generators/grid.js
  8. 24
      lib/captcha/generators/grid.test.js
  9. 10
      lib/captcha/generators/grid2.js
  10. 4
      lib/captcha/generators/text.js
  11. 18
      lib/captcha/generators/text.test.js
  12. 32
      lib/misc/fonts.js
  13. 15
      migrations/0.10.0.js
  14. 2
      models/forms/changeglobalsettings.js
  15. 4
      package.json
  16. 10
      views/pages/globalmanagesettings.pug

@ -26,7 +26,7 @@ unit-tests:
rules:
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
before_script:
- apt-get update -y && apt-get install -y fonts-urw-base35 gsfonts
- apt-get update -y && apt-get install -y fonts-urw-base35 gsfonts fonts-dejavu
script:
- 'npm install'
- 'npm install jest -g'

@ -1,3 +1,7 @@
### 0.10.0
- Allow customisable font for grid captcha.
- Default grid captcha to a common system font instead of bundling an enormous 24MB font file.
### 0.9.0
- Add board option to hide banners from header and link in board nav bar.
- Add global option to remove reverse image search links from the overboard.

@ -27,7 +27,7 @@
```bash
$ sudo apt-get update
$ sudo apt-get install nginx ffmpeg imagemagick graphicsmagick python-certbot-nginx
$ sudo apt-get install nginx ffmpeg imagemagick graphicsmagick python-certbot-nginx fonts-dejavu
```
**3. Install MongoDB**

@ -34,8 +34,8 @@ module.exports = {
captchaOptions: {
type: 'text',
generateLimit: 250,
font: 'default',
text: {
font: 'default',
line: true,
wave: 0,
paint: 2,

@ -13,7 +13,7 @@ module.exports = {
paramConverter: paramConverter({
timeFields: ['hot_threads_max_age', 'inactive_account_time', 'ban_duration', 'board_defaults_filter_ban_duration', 'default_ban_duration', 'block_bypass_expire_after_time', 'dnsbl_cache_time', 'board_defaults_delete_protection_age'],
trimFields: ['captcha_options_grid_question', 'captcha_options_grid_trues', 'captcha_options_grid_falses', 'captcha_options_text_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'],
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',
@ -91,7 +91,7 @@ module.exports = {
{ result: numberBody(req.body.captcha_options_num_distorts_max, 0, 10), expected: true, error: 'Captcha options max distorts must be a number from 0-10' },
{ result: minmaxBody(req.body.captcha_options_num_distorts_min, req.body.captcha_options_num_distorts_max), expected: true, error: 'Captcha options distorts min must be less than max' },
{ result: numberBody(req.body.captcha_options_distortion, 0, 50), expected: true, error: 'Captcha options distortion must be a number from 0-50' },
{ result: inArrayBody(req.body.captcha_options_text_font, fontPaths), expected: true, error: 'Invalid captcha options text font' },
{ result: inArrayBody(req.body.captcha_options_font, fontPaths), expected: true, error: 'Invalid captcha options font' },
{ result: numberBody(req.body.captcha_options_text_wave, 0, 10), expected: true, error: 'Captcha options text wave effect strength must be a number form 0-10' },
{ result: numberBody(req.body.captcha_options_text_paint, 0, 10), expected: true, error: 'Captcha options text paint effect strength must be a number from 0-10' },
{ result: numberBody(req.body.captcha_options_text_noise, 0, 10), expected: true, error: 'Captcha options text noise effect strength must be a number from 0-10' },

Binary file not shown.

@ -4,6 +4,7 @@ const gm = require('@fatchan/gm').subClass({ imageMagick: true })
, { promisify } = require('util')
, randomBytes = promisify(require('crypto').randomBytes)
, getDistorts = require(__dirname+'/../getdistorts.js')
, { DejaVuSans } = require(__dirname+'/../../misc/fonts.js')
, randomRange = promisify(require('crypto').randomInt)
, padding = 30; //pad edge of image to account for character size + distortion
@ -25,8 +26,13 @@ module.exports = async (captchaOptions) => {
}
const captcha = gm(width, height, '#ffffff')
.fill('#000000')
.font(__dirname+'/../font.ttf');
.fill('#000000');
if (captchaOptions.font !== 'default') {
captcha.font(captchaOptions.font);
} else {
captcha.font(DejaVuSans.path);
}
//divide the space by grid size, accounting for padding
const spaceSize = (width-padding)/size;

@ -4,18 +4,18 @@ const gridv1 = require('./grid.js')
, trues = ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣'];
const cases = [
{ name: '3n grid captcha', grid: { falses, trues, question: 'whatever', size: 3, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '6n grid captcha', grid: { falses, trues, question: 'whatever', size: 6, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with distortion', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '4n grid captcha with edge', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 10, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with noise', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 10 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with y offset', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 15, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with all effects', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '250px 6n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 6, imageSize: 250, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '123px 4n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 123, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '90px 3n grid captcha with all effects and distortion', grid: { falses, trues, question: 'whatever', size: 3, imageSize: 90, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '3n grid captcha', font: 'default', grid: { falses, trues, question: 'whatever', size: 3, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '6n grid captcha', font: 'default', grid: { falses, trues, question: 'whatever', size: 6, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with distortion', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '4n grid captcha with edge', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 10, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with noise', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 10 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with y offset', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 15, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with all effects', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 0, edge: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: '4n grid captcha with all effects and distortion', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 120, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '250px 6n grid captcha with all effects and distortion', font: 'default', grid: { falses, trues, question: 'whatever', size: 6, imageSize: 250, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '123px 4n grid captcha with all effects and distortion', font: 'default', grid: { falses, trues, question: 'whatever', size: 4, imageSize: 123, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: '90px 3n grid captcha with all effects and distortion', font: 'default', grid: { falses, trues, question: 'whatever', size: 3, imageSize: 90, iconYOffset: 150, edge: 10, noise: 10 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
];

@ -2,6 +2,7 @@
const gm = require('@fatchan/gm').subClass({ imageMagick: true })
, { promisify } = require('util')
, { DejaVuSans } = require(__dirname+'/../../misc/fonts.js')
, getDistorts = require(__dirname+'/../getdistorts.js')
, randomRange = promisify(require('crypto').randomInt)
, randomBytes = promisify(require('crypto').randomBytes)
@ -88,8 +89,13 @@ module.exports = async (captchaOptions) => {
}
const captcha = gm(width, height, '#ffffff')
.fill('#000000')
.font(__dirname+'/../font.ttf');
.fill('#000000');
if (captchaOptions.font !== 'default') {
captcha.font(captchaOptions.font);
} else {
captcha.font(DejaVuSans.path);
}
const spaceSize = (width-padding)/size;
const fontMinSize = Math.floor(width*0.16);

@ -41,8 +41,8 @@ module.exports = async (captchaOptions) => {
.fill('#000000')
.fontSize(65);
if (captchaOptions.text.font !== 'default') {
captcha.font(captchaOptions.text.font);
if (captchaOptions.font !== 'default') {
captcha.font(captchaOptions.font);
}
//draw each character at their x based on the characterWidth()

@ -2,15 +2,15 @@ const generateCaptcha = require('./text.js');
describe('generate text captcha', () => {
const cases = [
{ name: 'text captcha', text: { font: 'default', wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with distortion', text: { font: 'default', wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: 'text captcha with wave', text: { font: 'default', wave: 5, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with line', text: { font: 'default', wave: 0, line: true, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with paint', text: { font: 'default', wave: 0, line: false, paint: 5, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with noise', text: { font: 'default', wave: 0, line: false, paint: 0, noise: 5 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with with all effects and distortion', text: { font: 'default', wave: 5, line: true, paint: 5, noise: 5 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: 'text captcha with non-default font', text: { font: '/usr/share/fonts/type1/gsfonts/p052003l.pfb', wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with all the above', text: { font: '/usr/share/fonts/type1/gsfonts/p052003l.pfb', wave: 5, line: true, paint: 5, noise: 5 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: 'text captcha', font: 'default', text: { wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with distortion', font: 'default', text: { wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: 'text captcha with wave', font: 'default', text: { wave: 5, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with line', font: 'default', text: { wave: 0, line: true, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with paint', font: 'default', text: { wave: 0, line: false, paint: 5, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with noise', font: 'default', text: { wave: 0, line: false, paint: 0, noise: 5 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with with all effects and distortion', font: 'default', text: { wave: 5, line: true, paint: 5, noise: 5 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
{ name: 'text captcha with non-default font', font: '/usr/share/fonts/type1/gsfonts/p052003l.pfb', text: { wave: 0, line: false, paint: 0, noise: 0 }, numDistorts: { min: 0, max: 1 }, distortion: 0 },
{ name: 'text captcha with all the above', font: '/usr/share/fonts/type1/gsfonts/p052003l.pfb', text: { wave: 5, line: true, paint: 5, noise: 5 }, numDistorts: { min: 1, max: 10 }, distortion: 10 },
];
for(let captchaOptions of cases) {
test(captchaOptions.name, async () => {

@ -1,21 +1,25 @@
'use strict';
const fontList = require('child_process')
.execSync('fc-list -f "%{file}:%{family[0]} %{style[0]}\n"')
.toString()
.split('\n') //split by newlines, like here ^
.filter(line => line) //filter empty lines
.map(line => {
//map to an object with path and name
const [path, name] = line.split(':');
return { path, name };
})
.sort((a, b) => {
//alphabetical name sort
return a.name.localeCompare(b.name);
});
const { debugLogs } = require(__dirname+'/../../configs/secrets.js')
, fontList = require('child_process')
.execSync('fc-list -f "%{file}:%{family[0]} %{style[0]}\n"')
.toString()
.split('\n') //split by newlines, like here ^
.filter(line => line) //filter empty lines
.map(line => {
//map to an object with path and name
const [path, name] = line.split(':');
return { path, name };
})
.sort((a, b) => {
//alphabetical name sort
return a.name.localeCompare(b.name);
});
debugLogs && console.log(`${fontList.length} system fonts available`);
module.exports = {
fontList,
fontPaths: new Set(['default', ...fontList.map(f => f.path)]), //memoize paths
DejaVuSans: fontList.find(f => f.name === 'DejaVu Sans Book'), //default for grid captchas
};

@ -0,0 +1,15 @@
'use strict';
module.exports = async(db, redis) => {
console.log('make captcha font option apply to grid captcha too');
await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, {
'$unset': {
'captchaOptions.text.font': '',
},
'$set': {
'captchaOptions.font': 'default',
}
});
console.log('Clearing globalsettings cache');
await redis.deletePattern('globalsettings');
};

@ -68,6 +68,7 @@ module.exports = async (req, res) => {
captchaOptions: {
type: trimSetting(req.body.captcha_options_type, oldSettings.captchaOptions.type),
generateLimit: numberSetting(req.body.captcha_options_generate_limit, oldSettings.captchaOptions.generateLimit),
font: trimSetting(req.body.captcha_options_font, oldSettings.captchaOptions.font),
grid: {
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),
@ -79,7 +80,6 @@ module.exports = async (req, res) => {
noise: numberSetting(req.body.captcha_options_grid_noise, oldSettings.captchaOptions.grid.noise),
},
text: {
font: trimSetting(req.body.captcha_options_text_font, oldSettings.captchaOptions.text.font),
line: booleanSetting(req.body.captcha_options_text_line, oldSettings.captchaOptions.text.line),
wave: numberSetting(req.body.captcha_options_text_wave, oldSettings.captchaOptions.text.wave),
paint: numberSetting(req.body.captcha_options_text_paint, oldSettings.captchaOptions.text.paint),

@ -1,7 +1,7 @@
{
"name": "jschan",
"version": "0.9.0",
"migrateVersion": "0.9.0",
"version": "0.10.0",
"migrateVersion": "0.10.0",
"description": "",
"main": "server.js",
"dependencies": {

@ -332,14 +332,14 @@ block content
.row
.label Generate Limit
input(type='number' name='captcha_options_generate_limit' value=settings.captchaOptions.generateLimit)
.row
h4.mv-5 Text Captcha Options
.row
.label Font
select(name='captcha_options_text_font')
option(value='default' selected=(settings.captchaOptions.text.font === 'default')) Default
select(name='captcha_options_font')
option(value='default' selected=(settings.captchaOptions.font === 'default')) Default
each font in fontList
option(value=font.path selected=(settings.captchaOptions.text.font === font.path)) #{font.name}
option(value=font.path selected=(settings.captchaOptions.font === font.path)) #{font.name}
.row
h4.mv-5 Text Captcha Options
.row
.label Strikethrough Effect
label.postform-style.ph-5

Loading…
Cancel
Save