Merge branch 'develop' into 'master'

v0.9.1

Closes #489 and #495

See merge request fatchan/jschan!270
indiachan-spamvector v0.9.1
Thomas Lynch 2 years ago
commit aed1abf36a
  1. 2
      .gitlab-ci.yml
  2. 5
      CHANGELOG.md
  3. 2
      INSTALLATION.md
  4. 2
      configs/template.js.example
  5. 4
      controllers/forms/globalsettings.js
  6. 11
      gulp/res/js/forms.js
  7. BIN
      lib/captcha/font.ttf
  8. 14
      lib/captcha/generators/grid.js
  9. 24
      lib/captcha/generators/grid.test.js
  10. 14
      lib/captcha/generators/grid2.js
  11. 4
      lib/captcha/generators/text.js
  12. 18
      lib/captcha/generators/text.test.js
  13. 32
      lib/misc/fonts.js
  14. 15
      migrations/0.9.1.js
  15. 2
      models/forms/changeglobalsettings.js
  16. 16
      package-lock.json
  17. 4
      package.json
  18. 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,8 @@
### 0.9.1
- Allow customisable font for grid captcha.
- Default grid captcha to a common system font instead of bundling an enormous 24MB font file.
- Bugfix some tegaki issues.
### 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' },

@ -162,7 +162,7 @@ class postFormHandler {
}
doTegaki() {
const saveReplay = this.recordTegaki.checked;
const saveReplay = this.recordTegaki && this.recordTegaki.checked;
Tegaki.open({
saveReplay,
onCancel: () => {},
@ -171,11 +171,11 @@ class postFormHandler {
//add replay file if box was checked
if (saveReplay) {
const blob = Tegaki.replayRecorder.toBlob();
this.addFile(new File([blob], `${now}-tegaki.tgkr`, { type: 'tegaki/replay' }));
this.addFile(new File([blob], `${now}-tegaki.tgkr`, { type: 'tegaki/replay' }), { stripFilenames: false });
}
//add tegaki image
Tegaki.flatten().toBlob(b => {
this.addFile(new File([b], `${now}-tegaki.png`, { type: 'image/png' }));
this.addFile(new File([b], `${now}-tegaki.png`, { type: 'image/png' }), { stripFilenames: false });
}, 'image/png');
//update file list
this.updateFilesText();
@ -444,12 +444,12 @@ class postFormHandler {
this.updateFilesText();
}
async addFile(file) {
async addFile(file, fileOptions = {}) {
if (this.fileRequired) { //prevent drag+drop issues by removing required
this.fileInput.removeAttribute('required');
}
this.files.push(file);
console.log('got file', file.name, );
console.log('got file', file.name);
let fileHash;
if (window.crypto.subtle) {
let fileBuffer;
@ -477,6 +477,7 @@ class postFormHandler {
stripFilenames: this.fileUploadList.dataset.stripFilenames === 'true',
name: file.name,
hash: fileHash,
...fileOptions,
};
switch (file.type.split('/')[0]) {
case 'image':

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,11 +26,18 @@ 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;
const fontMinSize = Math.floor(width*0.16);
const fontMaxSize = Math.floor(width*0.25);
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)
@ -44,7 +52,7 @@ module.exports = async (captchaOptions) => {
} else {
character = falses[(await randomRange(0, falses.length))];
}
captcha.fontSize((await randomRange(20, 30)));
captcha.fontSize((await randomRange(fontMinSize, fontMaxSize)));
captcha.drawText(
(spaceSize * i) + cxOffset,
(spaceSize * (j + 1)) + cyOffset,

@ -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,15 +89,22 @@ 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);
const fontMaxSize = Math.floor(width*0.25);
for (let row = 0; row < size; row++) {
let cxOffset = Math.floor(spaceSize * 1.5);
for (let col = 0; col < size; col++) {
const cyOffset = captchaOptions.grid.iconYOffset;
captcha.fontSize((await randomRange(20, 30)));
captcha.fontSize((await randomRange(fontMinSize, fontMaxSize)));
captcha.drawText(
(spaceSize * col) + cyOffset,
(spaceSize * row) + cxOffset,

@ -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),

16
package-lock.json generated

@ -1,12 +1,12 @@
{
"name": "jschan",
"version": "0.9.0",
"version": "0.9.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "jschan",
"version": "0.9.0",
"version": "0.9.1",
"license": "AGPL-3.0-only",
"dependencies": {
"@fatchan/express-fileupload": "^1.4.2",
@ -9091,9 +9091,9 @@
}
},
"node_modules/moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.37",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"dependencies": {
"moment": ">= 2.9.0"
},
@ -21145,9 +21145,9 @@
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"moment-timezone": {
"version": "0.5.34",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.34.tgz",
"integrity": "sha512-3zAEHh2hKUs3EXLESx/wsgw6IQdusOT8Bxm3D9UrHPQR7zlMmzwybC8zHEM1tQ4LJwP7fcxrWr8tuBg05fFCbg==",
"version": "0.5.37",
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.37.tgz",
"integrity": "sha512-uEDzDNFhfaywRl+vwXxffjjq1q0Vzr+fcQpQ1bU0kbzorfS7zVtZnCnGc8mhWmF39d4g4YriF6kwA75mJKE/Zg==",
"requires": {
"moment": ">= 2.9.0"
}

@ -1,7 +1,7 @@
{
"name": "jschan",
"version": "0.9.0",
"migrateVersion": "0.9.0",
"version": "0.9.1",
"migrateVersion": "0.9.1",
"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