├── collage.gif ├── views ├── includes │ ├── modal.pug │ ├── pugfilters.pug │ ├── banmessage.pug │ ├── captchaformsection.pug │ ├── uploaditem.pug │ ├── post.pug │ ├── tegakiwindow.pug │ ├── threadwatcher.pug │ ├── watchedthread.pug │ ├── web3signature.pug │ ├── captchafieldrow.pug │ ├── stickynav.pug │ ├── captchaexpand.pug │ ├── captchasidelabel.pug │ ├── forceactiontwofactor.pug │ ├── globalpermissionsform.pug │ ├── staffpermissionsform.pug │ ├── boardpages.pug │ ├── postflag.pug │ ├── subjectfield.pug │ ├── footer.pug │ ├── bantable.pug │ ├── 2charisocountries.pug │ ├── navbar.pug │ ├── posticons.pug │ ├── pages.pug │ ├── addbanform.pug │ ├── registration.pug │ ├── banform.pug │ └── permissionsform.pug ├── mixins │ ├── captchaexpand.pug │ ├── filelabel.pug │ ├── banmessage.pug │ ├── threadwatcher.pug │ ├── tegakiwindow.pug │ ├── web3signature.pug │ ├── postlink.pug │ ├── watchedthread.pug │ ├── uploaditem.pug │ ├── boardtable.pug │ ├── report.pug │ ├── boardheader.pug │ ├── filters.pug │ ├── announcements.pug │ ├── mypermissions.pug │ ├── overboardform.pug │ ├── catalogfile.pug │ ├── boardnav.pug │ ├── custompage.pug │ └── newspost.pug ├── pages │ ├── 404.pug │ ├── error.pug │ ├── 500.pug │ ├── 502.pug │ ├── 504.pug │ ├── 503.pug │ ├── captcha.pug │ ├── ban.pug │ ├── news.pug │ ├── custompage.pug │ ├── globalmanagebans.pug │ ├── managebans.pug │ ├── bypass.pug │ ├── editfilter.pug │ ├── globaleditfilter.pug │ ├── editrole.pug │ ├── globalmanageroles.pug │ ├── message.pug │ ├── banners.pug │ ├── managemypermissions.pug │ ├── managereports.pug │ ├── globalmanagereports.pug │ ├── editstaff.pug │ ├── mypermissions.pug │ ├── modloglist.pug │ ├── overboard.pug │ ├── twofactor.pug │ ├── create.pug │ ├── editnews.pug │ ├── sessions.pug │ ├── globalmanagerecent.pug │ └── overboardcatalog.pug ├── custompages │ └── rules.pug └── layout.pug ├── gulp └── res │ ├── img │ ├── dice.png │ ├── lock.png │ ├── plus.png │ ├── audio.png │ ├── cyclic.png │ ├── deleted.png │ ├── flags.png │ ├── minus.png │ ├── spoiler.png │ ├── sticky.png │ ├── video.png │ ├── bumplock.png │ ├── ratelimit.png │ ├── attachment.png │ └── defaultbanner.png │ ├── icons │ └── master.png │ ├── css │ ├── themes │ │ ├── assets │ │ │ ├── digi-bg1.png │ │ │ ├── mushroom.png │ │ │ ├── tchan-bg.png │ │ │ ├── 56chan-bg.png │ │ │ ├── favela-bg.jpg │ │ │ ├── iwakura-bg.gif │ │ │ ├── ptchan-bg.png │ │ │ ├── tchan-star.png │ │ │ ├── win95home.png │ │ │ ├── VCR-OSD-Mono.ttf │ │ │ ├── digi-cursor.png │ │ │ └── tchan-capcode.png │ │ ├── army-green.css │ │ ├── gurochan.css │ │ ├── choc.css │ │ ├── navy.css │ │ ├── rei-zero.css │ │ ├── clear.css │ │ ├── solarized-dark.css │ │ ├── yotsuba-b.css │ │ └── solarized-light.css │ └── nscaptcha.css │ └── js │ ├── renderweb3.js │ ├── hidefileinput.js │ ├── i18n.js │ ├── iscanvasblocked.js │ └── settings.js ├── lib ├── middleware │ ├── misc │ │ ├── csrfmiddleware.js │ │ ├── setminimal.js │ │ └── referrercheck.js │ ├── ip │ │ ├── iptypes.js │ │ └── geoip.js │ ├── permission │ │ ├── calcpermsmiddleware.js │ │ └── isloggedin.js │ ├── file │ │ ├── numfiles.js │ │ └── imagehash.js │ └── locale │ │ └── locale.js ├── input │ ├── escaperegexp.js │ ├── escaperegexp.test.js │ ├── pagequeryconverter.js │ ├── decodequeryip.js │ ├── setting.js │ ├── modlogactions.js │ ├── settingsdiff.js │ └── decodequeryip.test.js ├── misc │ ├── commit.js │ ├── haship.js │ ├── config.js │ ├── dynamic.js │ ├── dotwofactor.js │ ├── themes.js │ └── fonts.js ├── file │ ├── uploaddirectory.js │ ├── deletefailed.js │ ├── moveupload.js │ ├── image │ │ ├── fixgifs.js │ │ └── getdimensions.js │ ├── ffprobe.js │ ├── deletepostfiles.js │ ├── deletetempfiles.js │ ├── audio │ │ └── audiothumbnail.js │ └── deleteold.js ├── converter │ ├── makearrayifsingle.js │ ├── formatsize.js │ └── formatsize.test.js ├── redis │ └── redlock.js ├── build │ └── queue.js ├── post │ ├── markdown │ │ ├── escape.js │ │ ├── handler │ │ │ ├── fortune.js │ │ │ ├── fortune.test.js │ │ │ └── linkmatch.js │ │ ├── sanitizeoptions.js │ │ └── escape.test.js │ ├── checkfilters.js │ ├── tripcode.test.js │ ├── deletequotes.js │ └── tripcode.js ├── web3 │ └── web3.js └── locale │ └── locale.js ├── configs ├── nginx │ ├── snippets │ │ ├── security_headers.conf │ │ ├── error_pages.conf │ │ └── security_headers_nocache.conf │ └── README.md └── secrets.js.example ├── models ├── pages │ ├── csrf.js │ ├── login.js │ ├── manage │ │ ├── assets.js │ │ ├── permissions.js │ │ ├── staff.js │ │ ├── mypermissions.js │ │ ├── editpost.js │ │ ├── custompages.js │ │ ├── filters.js │ │ ├── catalog.js │ │ ├── editfilter.js │ │ ├── editcustompage.js │ │ ├── thread.js │ │ ├── reports.js │ │ ├── bans.js │ │ ├── editstaff.js │ │ ├── board.js │ │ ├── settings.js │ │ └── index.js │ ├── permissions.js │ ├── globalsettings.js │ ├── news.js │ ├── home.js │ ├── captchapage.js │ ├── register.js │ ├── boardsettings.js │ ├── create.js │ ├── changepassword.js │ ├── mypermissions.js │ ├── nonce.js │ ├── globalmanage │ │ ├── news.js │ │ ├── filters.js │ │ ├── editnews.js │ │ ├── editfilter.js │ │ ├── bans.js │ │ ├── roles.js │ │ ├── editrole.js │ │ ├── index.js │ │ ├── editaccount.js │ │ └── settings.js │ ├── banners.js │ ├── catalog.js │ ├── modloglist.js │ ├── blockbypass.js │ ├── custompage.js │ ├── sessions.js │ ├── randombanner.js │ ├── board.js │ ├── thread.js │ └── modlog.js └── forms │ ├── newcaptcha.js │ ├── logout.js │ ├── deletesessions.js │ ├── appeal.js │ ├── denybanappeals.js │ ├── editbannote.js │ ├── deleteaccount.js │ ├── upgradebans.js │ ├── editbanduration.js │ ├── dismissglobalreport.js │ ├── spoilerpost.js │ ├── removebans.js │ ├── lockposts.js │ ├── bumplockposts.js │ ├── cycleposts.js │ ├── deletenews.js │ ├── dismissreport.js │ ├── stickyposts.js │ ├── addstaff.js │ ├── reportpost.js │ ├── globalclear.js │ ├── deletestaff.js │ ├── deletefilter.js │ ├── addnews.js │ └── deleteassets.js ├── migrations ├── 0.0.11.js ├── 0.0.3.js ├── 0.0.2.js ├── 0.6.5.js ├── 1.4.2.js ├── 0.0.17.js ├── 0.1.4.js ├── 0.0.1.js ├── index.js ├── 0.0.13.js ├── 0.0.15.js ├── 0.0.7.js ├── 0.0.8.js ├── 0.0.6.js ├── 0.1.0.js ├── 0.10.0.js ├── 0.0.14.js ├── 0.0.19.js ├── 0.0.22.js ├── 0.6.1.js ├── 0.0.12.js ├── 0.1.10.js ├── 1.2.2.js ├── 0.0.18.js ├── 0.2.0.js ├── 1.5.0.js ├── 0.9.1.js ├── 0.11.4.js ├── 1.0.5.js ├── 0.6.3.js ├── 0.0.5.js ├── 0.0.20.js ├── 0.0.23.js ├── 1.6.0.js ├── 0.1.2.js ├── 0.0.10.js ├── 0.0.16.js ├── 0.1.8.js ├── 1.3.0.js ├── 0.0.21.js ├── 0.1.3.js ├── 0.5.0.js ├── 1.7.3.js ├── 0.9.0.js ├── 0.1.5.js ├── 0.1.1.js ├── 1.0.0.js ├── 0.0.4.js ├── 0.1.6.js └── 0.8.0.js ├── .dockerignore ├── tools ├── viewstrings.sh ├── festrings.sh ├── self_signed.sh ├── update_geoip.sh ├── clean_locales.js ├── backup.sh.example └── example_request.sh ├── test └── integration.test.js ├── docker ├── jschan │ ├── Dockerfile-reset │ └── Dockerfile └── nginx │ └── jschan.conf ├── schedules ├── tasks │ ├── deletecaptchas.js │ └── index.js ├── Schedule.js └── index.js ├── worker.js ├── db ├── index.js ├── captchas.js ├── ratelimits.js ├── news.js ├── roles.js └── bypass.js ├── .gitignore ├── .gitlab └── issue_templates │ └── Default.md └── controllers └── forms ├── deletenews.js └── deletecustompage.js /collage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/collage.gif -------------------------------------------------------------------------------- /views/includes/modal.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/modal.pug 2 | +modal(modal) 3 | -------------------------------------------------------------------------------- /views/includes/pugfilters.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/filters.pug 2 | +filters(filterArr) 3 | -------------------------------------------------------------------------------- /gulp/res/img/dice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/dice.png -------------------------------------------------------------------------------- /gulp/res/img/lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/lock.png -------------------------------------------------------------------------------- /gulp/res/img/plus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/plus.png -------------------------------------------------------------------------------- /gulp/res/img/audio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/audio.png -------------------------------------------------------------------------------- /gulp/res/img/cyclic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/cyclic.png -------------------------------------------------------------------------------- /gulp/res/img/deleted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/deleted.png -------------------------------------------------------------------------------- /gulp/res/img/flags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/flags.png -------------------------------------------------------------------------------- /gulp/res/img/minus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/minus.png -------------------------------------------------------------------------------- /gulp/res/img/spoiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/spoiler.png -------------------------------------------------------------------------------- /gulp/res/img/sticky.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/sticky.png -------------------------------------------------------------------------------- /gulp/res/img/video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/video.png -------------------------------------------------------------------------------- /views/includes/banmessage.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/banmessage.pug 2 | +banmessage(banmessage) 3 | -------------------------------------------------------------------------------- /views/includes/captchaformsection.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/captchaexpand.pug 2 | +captchaexpand() 3 | -------------------------------------------------------------------------------- /views/includes/uploaditem.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/uploaditem.pug 2 | +uploaditem(uploaditem) 3 | -------------------------------------------------------------------------------- /gulp/res/icons/master.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/icons/master.png -------------------------------------------------------------------------------- /gulp/res/img/bumplock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/bumplock.png -------------------------------------------------------------------------------- /gulp/res/img/ratelimit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/ratelimit.png -------------------------------------------------------------------------------- /views/includes/post.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/post.pug 2 | +post(post, false, manage, globalmanage) 3 | -------------------------------------------------------------------------------- /views/includes/tegakiwindow.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/tegakiwindow.pug 2 | +tegakiwindow(minimised) 3 | -------------------------------------------------------------------------------- /views/includes/threadwatcher.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/threadwatcher.pug 2 | +threadwatcher(minimised) 3 | -------------------------------------------------------------------------------- /views/mixins/captchaexpand.pug: -------------------------------------------------------------------------------- 1 | mixin captchaexpand() 2 | include ../includes/captchaexpand.pug 3 | -------------------------------------------------------------------------------- /gulp/res/img/attachment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/attachment.png -------------------------------------------------------------------------------- /views/includes/watchedthread.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/watchedthread.pug 2 | +watchedthread(watchedthread) 3 | -------------------------------------------------------------------------------- /gulp/res/img/defaultbanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/img/defaultbanner.png -------------------------------------------------------------------------------- /views/includes/web3signature.pug: -------------------------------------------------------------------------------- 1 | include ../mixins/web3signature.pug 2 | +web3signature(signature, address) 3 | -------------------------------------------------------------------------------- /views/pages/404.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1.board-title #{__('404 Not Found')} 5 | -------------------------------------------------------------------------------- /views/pages/error.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1 #{__('Internal Server Error')} 5 | -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/digi-bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/digi-bg1.png -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/mushroom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/mushroom.png -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/tchan-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/tchan-bg.png -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/56chan-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/56chan-bg.png -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/favela-bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/favela-bg.jpg -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/iwakura-bg.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/iwakura-bg.gif -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/ptchan-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/ptchan-bg.png -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/tchan-star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/tchan-star.png -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/win95home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/win95home.png -------------------------------------------------------------------------------- /lib/middleware/misc/csrfmiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const csrf = require('csurf')(); 4 | 5 | module.exports = csrf; 6 | -------------------------------------------------------------------------------- /views/includes/captchafieldrow.pug: -------------------------------------------------------------------------------- 1 | .row.label.mr-0 2 | .pv-5 #{__('Captcha')} 3 | span.required * 4 | include ./captcha.pug 5 | -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/VCR-OSD-Mono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/VCR-OSD-Mono.ttf -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/digi-cursor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/digi-cursor.png -------------------------------------------------------------------------------- /gulp/res/css/themes/assets/tchan-capcode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fatchan/jschan/HEAD/gulp/res/css/themes/assets/tchan-capcode.png -------------------------------------------------------------------------------- /views/includes/stickynav.pug: -------------------------------------------------------------------------------- 1 | nav.stickynav 2 | a.nav-item(href='#bottom') [▼] 3 | | 4 | a.nav-item(href='#top') [▲] 5 | -------------------------------------------------------------------------------- /lib/middleware/ip/iptypes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | IPV4: 0, 5 | IPV6: 1, 6 | BYPASS: 2, 7 | PRUNED: 3, 8 | }; 9 | -------------------------------------------------------------------------------- /views/includes/captchaexpand.pug: -------------------------------------------------------------------------------- 1 | details.row.label.mr-0 2 | summary.pv-5 #{__('Captcha')} 3 | span.required * 4 | include ./captcha.pug 5 | -------------------------------------------------------------------------------- /configs/nginx/snippets/security_headers.conf: -------------------------------------------------------------------------------- 1 | include /etc/nginx/snippets/security_headers_nocache.conf; 2 | add_header Cache-Control "public"; 3 | -------------------------------------------------------------------------------- /lib/input/escaperegexp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (string) => { 4 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 5 | }; 6 | -------------------------------------------------------------------------------- /lib/middleware/misc/setminimal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, next) => { 4 | res.locals.minimal = true; 5 | next(); 6 | }; 7 | -------------------------------------------------------------------------------- /views/includes/captchasidelabel.pug: -------------------------------------------------------------------------------- 1 | section.row 2 | .label 3 | span #{__('Captcha')} 4 | span.required * 5 | .col.p-0 6 | include ./captcha.pug 7 | -------------------------------------------------------------------------------- /views/mixins/filelabel.pug: -------------------------------------------------------------------------------- 1 | mixin filelabel(id, max) 2 | label.jsonly.postform-style.filelabel(for=id) 3 | | #{__n(`Select/Drop/Paste files`, max)} 4 | -------------------------------------------------------------------------------- /lib/misc/commit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('child_process') 4 | .execSync('git rev-parse --short HEAD') 5 | .toString() 6 | .trim(); 7 | -------------------------------------------------------------------------------- /models/pages/csrf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res) => { 4 | 5 | res.json({ 6 | token: req.csrfToken(), 7 | }); 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /views/includes/forceactiontwofactor.pug: -------------------------------------------------------------------------------- 1 | if forceActionTwofactor === true 2 | .row 3 | .label #{__('2FA Code')} 4 | input(type='text' name='twofactor' required) 5 | -------------------------------------------------------------------------------- /lib/file/uploaddirectory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path') 4 | , directory = path.join(__dirname+'/../../static'); 5 | 6 | module.exports = directory; 7 | -------------------------------------------------------------------------------- /models/forms/newcaptcha.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res) => { 4 | 5 | res.clearCookie('captchaid'); 6 | return res.redirect('/captcha.html'); 7 | 8 | }; 9 | -------------------------------------------------------------------------------- /views/includes/globalpermissionsform.pug: -------------------------------------------------------------------------------- 1 | for bit, index in Object.keys(jsonPermissions) 2 | include ../includes/permissionsform.pug 3 | input(type='submit', value=__('Save')) 4 | -------------------------------------------------------------------------------- /lib/converter/makearrayifsingle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function makeArrayIfSingle(obj) { 4 | return !Array.isArray(obj) ? [obj] : obj; 5 | } 6 | 7 | module.exports = makeArrayIfSingle; 8 | -------------------------------------------------------------------------------- /models/forms/logout.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res) => { 4 | 5 | //remove session 6 | req.session.destroy(); 7 | return res.redirect('/'); 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /views/mixins/banmessage.pug: -------------------------------------------------------------------------------- 1 | mixin banmessage(banmessage) 2 | p.ban 3 | span.message #{__('USER WAS BANNED FOR THIS POST')} 4 | | 5 | span.reason(data-reason=banmessage) #{banmessage} 6 | -------------------------------------------------------------------------------- /migrations/0.0.11.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('Expiring existing captchas, so new ones get new answer format'); 5 | await db.collection('captcha').deleteMany({}); 6 | }; 7 | -------------------------------------------------------------------------------- /models/pages/login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res) => { 4 | 5 | res.render('login', { 6 | 'goto': (typeof req.query.goto === 'string' ? req.query.goto : null) 7 | }); 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | static/ 3 | docker/jschan/Dockerfile 4 | docker/jschan/Dockerfile-reset 5 | docker/nginx/Dockerfile 6 | docker/static/ 7 | tools/ 8 | !tools/festrings.json 9 | gulp/res/js/socket.io.js 10 | -------------------------------------------------------------------------------- /gulp/res/js/renderweb3.js: -------------------------------------------------------------------------------- 1 | /* globals Web3 */ 2 | 3 | if (window.ethereum) { 4 | window.jschanweb3 = new Web3(window.ethereum); 5 | } else { 6 | document.querySelectorAll('.web3') 7 | .forEach(elem => elem.remove()); 8 | } 9 | -------------------------------------------------------------------------------- /models/forms/deletesessions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const redis = require(__dirname+'/../../lib/redis/redis.js'); 4 | 5 | module.exports = async (sessionIds) => { 6 | 7 | await redis.del(sessionIds); 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /views/includes/staffpermissionsform.pug: -------------------------------------------------------------------------------- 1 | for bit, index in Object.keys(jsonPermissions).filter(p => manageBoardBits.includes(parseInt(p))) 2 | include ../includes/permissionsform.pug 3 | input(type='submit', value=__('Save')) 4 | -------------------------------------------------------------------------------- /migrations/0.0.3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async() => { 4 | console.log('Moving user uploads from /img/ to /file/'); 5 | require('fs').renameSync(__dirname+'/../static/img/', __dirname+'/../static/file/'); 6 | }; 7 | -------------------------------------------------------------------------------- /views/pages/500.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1.board-title #{__('500 Internal Server Error')} 5 | .col.flex-center.mv-10 6 | p #{__('The server encountered an error and was unable to fulfill your request.')} 7 | -------------------------------------------------------------------------------- /views/pages/502.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1.board-title #{__('502 Bad Gateway')} 5 | .col.flex-center.mv-10 6 | p #{__('The server acting as a gateway or proxy received an invalid response from the upstream server.')} 7 | -------------------------------------------------------------------------------- /models/pages/manage/assets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res) => { 4 | 5 | res 6 | .set('Cache-Control', 'private, max-age=5') 7 | .render('manageassets', { 8 | csrf: req.csrfToken(), 9 | }); 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /tools/viewstrings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # roughly extract all translation calls used in pug view file into key: value 3 | grep -ri '__' $1 \ 4 | | sed 's/__/\n/g' \ 5 | | grep -Po "(?<=')[^',]+(?=')" \ 6 | | awk '{ print "\""$0"\""": ""\""$0"\"""," }' 7 | -------------------------------------------------------------------------------- /lib/misc/haship.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { ipHashSecret } = require(__dirname+'/../../configs/secrets.js') 4 | , { createHash } = require('crypto'); 5 | 6 | module.exports = (ip) => createHash('sha256').update(ipHashSecret + ip).digest('base64'); 7 | -------------------------------------------------------------------------------- /migrations/0.0.2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('Renaming IP fields on posts'); 5 | await db.collection('posts').updateMany({}, { 6 | '$rename': { 7 | 'ip.hash': 'ip.single' 8 | } 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /views/pages/504.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1.board-title #{__('504 Gateway Timeout')} 5 | .col.flex-center.mv-10 6 | p #{__('The server acting as a gateway or proxy did not get a response in time from the upstream server.')} 7 | -------------------------------------------------------------------------------- /lib/middleware/permission/calcpermsmiddleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const calcPerms = require(__dirname+'/../../permission/calcperms.js'); 4 | 5 | module.exports = (req, res, next) => { 6 | res.locals.permissions = calcPerms(req, res); 7 | next(); 8 | }; 9 | -------------------------------------------------------------------------------- /migrations/0.6.5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | 5 | console.log('remove older phashes'); 6 | await db.collection('posts').updateMany({}, { 7 | '$unset': { 8 | 'files.$[].phash': '' 9 | } 10 | }); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /views/pages/503.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block content 4 | h1.board-title #{__('503 Service Unavailable')} 5 | .col.flex-center.mv-10 6 | p #{__('The server is currently unable to handle the request due to a temporary overload or scheduled maintenance.')} 7 | -------------------------------------------------------------------------------- /migrations/1.4.2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | 5 | console.log('Updating modlogs to add public flag'); 6 | await db.collection('modlog').updateMany({}, { 7 | '$set': { 8 | 'public': true, 9 | }, 10 | }); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /views/custompages/rules.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title Rules 5 | 6 | block content 7 | h1.board-title Rules 8 | .table-container.flex-center.mv-10 9 | table.table-body 10 | tr.table-row 11 | td 12 | p 1. Put rules here 13 | -------------------------------------------------------------------------------- /lib/redis/redlock.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Redlock = require('redlock') 4 | , { redisClient } = require(__dirname+'/redis.js') 5 | , redlock = new Redlock([redisClient]); 6 | 7 | redlock.on('clientError', console.error); 8 | 9 | module.exports = redlock; 10 | -------------------------------------------------------------------------------- /models/pages/manage/permissions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res) => { 4 | 5 | res 6 | .set('Cache-Control', 'private, max-age=5') 7 | .render('managemypermissions', { 8 | permissions: res.locals.permissions, 9 | }); 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /views/layout.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | include includes/head.pug 5 | block head 6 | body#top 7 | include includes/navbar.pug 8 | main(class=(minimal?'minimal':'')) 9 | .container 10 | block content 11 | include includes/footer.pug 12 | 13 | -------------------------------------------------------------------------------- /models/forms/appeal.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | return Bans.appeal(res.locals.ip.cloak, req.body.checkedbans, req.body.message).then(r => r.modifiedCount); 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /migrations/0.0.17.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('add collection for board custompages'); 5 | await db.createCollection('custompages'); 6 | await db.collection('custompages').createIndex({ 'board': 1, 'page': 1 }, { unique: true }); 7 | }; 8 | -------------------------------------------------------------------------------- /models/forms/denybanappeals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | return Bans.denyAppeal(res.locals.bansBoard, req.body.checkedbans).then(result => result.modifiedCount); 8 | 9 | }; 10 | -------------------------------------------------------------------------------- /views/mixins/threadwatcher.pug: -------------------------------------------------------------------------------- 1 | mixin threadwatcher(minimised) 2 | .flex-center#threadwatcher(class=(minimised ? 'minimised' : '')) 3 | .row.noselect#threadwatcher-dragHandle 4 | span.fw.text-center #{__('Thread Watcher')} 5 | span.mr-0.close#minimise #{minimised ? '[+]' : '[−]'} 6 | -------------------------------------------------------------------------------- /views/pages/captcha.pug: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta(charset='utf-8') 5 | link(rel='stylesheet', href='/css/nscaptcha.css') 6 | body 7 | img.flexcenter(src='/captcha') 8 | form(action='/forms/newcaptcha', method='POST') 9 | input(type='submit' value='↻') 10 | -------------------------------------------------------------------------------- /models/pages/permissions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res) => { 4 | 5 | res 6 | .set('Cache-Control', 'private, max-age=5') 7 | .render('mypermissions', { 8 | user: res.locals.user, 9 | permissions: res.locals.permissions, 10 | }); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /views/pages/ban.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/ban.pug 3 | 4 | block head 5 | title #{__('Banned!')} 6 | 7 | block content 8 | h1.board-title #{__('Banned!')} 9 | h4.board-description #{__('Bans currently in place against your IP')}: 10 | include ../includes/banform.pug 11 | -------------------------------------------------------------------------------- /migrations/0.1.4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('adding markdown db entry for fortune example'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'permLevels.markdown.fortune': 0 8 | } 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /views/includes/boardpages.pug: -------------------------------------------------------------------------------- 1 | | #{__('Page')}: 2 | if maxPage === 0 3 | a.bold(href=`index.html`) [1] 4 | - for(let i = 1; i <= maxPage; i++) 5 | if page === i 6 | a.bold(href=`${i === 1 ? 'index' : i}.html`) [#{i}] 7 | else 8 | a(href=`${i === 1 ? 'index' : i}.html`) [#{i}] 9 | | 10 | | | 11 | -------------------------------------------------------------------------------- /migrations/0.0.1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('Creating bypass collection'); 5 | await db.createCollection('bypass'); 6 | console.log('Creating bypass collection index'); 7 | await db.collection('bypass').createIndex({ 'expireAt': 1 }, { expireAfterSeconds: 0 }); 8 | }; 9 | -------------------------------------------------------------------------------- /models/forms/editbannote.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | //New ban note. 8 | return Bans.editNote(res.locals.bansBoard, req.body.checkedbans, req.body.ban_note).then(result => result.modifiedCount); 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /views/includes/postflag.pug: -------------------------------------------------------------------------------- 1 | if post.country.custom === true 2 | span(title=post.country.name) 3 | img.customflag(src=`/flag/${post.board}/${post.country.src}` alt=' ' loading='lazy') 4 | | 5 | else 6 | span(class=`flag flag-${post.country.code.toLowerCase()}` title=post.country.name alt=post.country.name) 7 | | 8 | -------------------------------------------------------------------------------- /views/includes/subjectfield.pug: -------------------------------------------------------------------------------- 1 | if !isThread || !board.settings.disableReplySubject 2 | section.row 3 | .label 4 | span #{__('Subject')} 5 | if subjectRequired 6 | span.required * 7 | input(type='text', name='subject', autocomplete='off' maxlength=globalLimits.fieldLength.subject required=subjectRequired) 8 | -------------------------------------------------------------------------------- /migrations/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs') 4 | , semver = require('semver'); 5 | 6 | fs.readdirSync(__dirname).forEach(file => { 7 | const name = file.substring(0, file.length-3); 8 | if (!semver.valid(name)) { return; } 9 | module.exports[name] = require(__dirname+'/'+file); 10 | }); 11 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | describe('run integration tests', () => { 2 | require('./setup.js')(); 3 | require('./posting.js')(); 4 | require('./global.js')(); 5 | require('./board.js')(); 6 | require('./actions.js')(); 7 | require('./pages.js')(); 8 | require('./cleanup.js')(); 9 | require('./twofactor.js')(); 10 | }); 11 | -------------------------------------------------------------------------------- /tools/festrings.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # roughly extract all translation calls used in frontend scripts 3 | grep -ri '__' gulp/res/js/*.js \ 4 | | sed 's/__/\n/g' \ 5 | | awk -F'))' '{ print $1 }' \ 6 | | grep -Po "(?<=')[^',]+(?=')" \ 7 | | sort \ 8 | | uniq \ 9 | | awk '{ print "\""$0"\"""," }' 10 | 11 | # | awk -F'__' '{ print $2 }' \ 12 | -------------------------------------------------------------------------------- /models/pages/manage/staff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res) => { 4 | 5 | res 6 | // .set('Cache-Control', 'private, max-age=5') 7 | .render('managestaff', { 8 | csrf: req.csrfToken(), 9 | permissions: res.locals.permissions, 10 | board: res.locals.board, 11 | user: res.locals.user, 12 | }); 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /views/mixins/tegakiwindow.pug: -------------------------------------------------------------------------------- 1 | mixin tegakiwindow(minimised) 2 | .flex-center.resize#tegakiwindow(class=(minimised ? 'minimised' : '')) 3 | .row.noselect#tegakiwindow-dragHandle 4 | span.fw.text-center #{__('Tegaki')} 5 | span.mr-0.close#minimise #{minimised ? '[+]' : '[−]'} 6 | span.mr-0.close#maximise [⤢] 7 | span.mr-0.close#close [×] 8 | 9 | -------------------------------------------------------------------------------- /views/pages/news.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/newspost.pug 3 | 4 | block head 5 | title #{__('News')} 6 | 7 | block content 8 | h1.board-title #{__('News')} 9 | if news.length === 0 10 | p.text-center #{__('No news.')} 11 | else 12 | include ../includes/stickynav.pug 13 | each post in news 14 | +newspost(post) 15 | -------------------------------------------------------------------------------- /migrations/0.0.13.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding file r9k setting to boards db'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.fileR9KMode': 0, 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /lib/file/deletefailed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { remove } = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/uploaddirectory.js'); 5 | 6 | module.exports = async (filenames, folder) => { 7 | 8 | await Promise.all(filenames.map(async filename => { 9 | remove(`${uploadDirectory}/${folder}/${filename}`); 10 | })); 11 | 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/0.0.15.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding message r9k option to boards db'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.messageR9KMode': 0, 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/0.0.7.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('add sageOnlyEmail option to boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.sageOnlyEmail': false, 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/0.0.8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('add resetTrigger option to boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.resetTrigger': false, 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /models/forms/deleteaccount.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Accounts } = require(__dirname+'/../../db/') 4 | , redis = require(__dirname+'/../../lib/redis/redis.js'); 5 | 6 | module.exports = async (username) => { 7 | 8 | await Promise.all([ 9 | Accounts.deleteOne(username), 10 | redis.deletePattern(`sess:*:${username}`), 11 | ]); 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /migrations/0.0.6.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding blockedCountries field to boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.blockedCountries': [], 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/0.1.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('moving globalsettings from redis into mongodb'); 5 | const oldSettings = await redis.get('globalsettings'); 6 | if (oldSettings) { 7 | await db.collection('globalsettings').replaceOne({ _id: 'globalsettings' }, oldSettings, { upsert: true }); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /models/pages/globalsettings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildGlobalSettings } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let json; 8 | try { 9 | json = await buildGlobalSettings(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.json(json); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /migrations/0.10.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('setting 2fa/totp property on accounts'); 5 | await db.collection('accounts').updateMany({}, { 6 | '$set': { 7 | 'twofactor': null, 8 | } 9 | }); 10 | console.log('Clearing globalsettings cache'); 11 | await redis.deletePattern('globalsettings'); 12 | }; 13 | -------------------------------------------------------------------------------- /models/pages/news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildNews } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildNews(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.set('Cache-Control', 'max-age=0').send(html); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /lib/build/queue.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Queue = require('bull') 4 | , { redis } = require(__dirname+'/../../configs/secrets.js') 5 | , taskQueue = new Queue('task', { redis }); 6 | 7 | module.exports = { 8 | 9 | queue: taskQueue, 10 | 11 | push: (data, options) => { 12 | taskQueue.add(data, { ...options, removeOnComplete: true}); 13 | } 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /lib/converter/formatsize.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] 4 | , k = 1024; 5 | 6 | module.exports = (bytes) => { 7 | if (bytes === 0) { 8 | return '0B'; 9 | } 10 | const i = Math.min(sizes.length-1, Math.floor(Math.log(bytes) / Math.log(k))); 11 | return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))}${sizes[i]}`; 12 | }; 13 | -------------------------------------------------------------------------------- /lib/misc/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const redis = require(__dirname+'/../redis/redis.js') 4 | , Mongo = require(__dirname+'/../../db/db.js'); 5 | 6 | const load = async (message) => { 7 | module.exports.get = message || (await Mongo.getConfig()); 8 | }; 9 | 10 | redis.addCallback('config', load); 11 | 12 | module.exports = { 13 | get: null, 14 | load, 15 | }; 16 | -------------------------------------------------------------------------------- /migrations/0.0.14.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding disableOnionFilePosting setting'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'settings.disableOnionFilePosting': false, 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/0.0.19.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('fixing index for custompages'); 5 | try { 6 | await db.collection('custompages').dropIndex('board_1_url_1'); 7 | } catch (e) { 8 | // didnt have the bad index 9 | } 10 | await db.collection('custompages').createIndex({ 'board': 1, 'page': 1 }, { unique: true }); 11 | }; 12 | -------------------------------------------------------------------------------- /migrations/0.0.22.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('remove need for configs/webring'); 5 | const webring = require(__dirname+'/../configs/template.js.example'); 6 | const settings = await redis.get('globalsettings'); 7 | const newSettings = { ...settings, ...webring }; 8 | redis.set('globalsettings', newSettings); 9 | }; 10 | -------------------------------------------------------------------------------- /models/pages/home.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildHomepage } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildHomepage(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.set('Cache-Control', 'max-age=0').send(html); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /views/mixins/web3signature.pug: -------------------------------------------------------------------------------- 1 | mixin web3signature(signature, address) 2 | div.mv-5.ml-5 3 | a.web3-address(href=ethereumLinksURL.replace('%s', encodeURIComponent(address)) rel='nofollow' referrerpolicy='same-origin' target='_blank') #{address.substring(0,5)}…#{address.substring(38, 42)} 4 | details.dummy-link 5 | summary #{__('Signature')} 6 | .web3-signature #{signature} 7 | -------------------------------------------------------------------------------- /models/pages/captchapage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildCaptcha } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildCaptcha(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.set('Cache-Control', 'max-age=0').send(html); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /models/pages/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildRegister } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildRegister(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.set('Cache-Control', 'max-age=0').send(html); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /views/pages/custompage.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/custompage.pug 3 | 4 | block head 5 | title /#{board._id}/ - #{customPage.title} 6 | 7 | block content 8 | .board-header 9 | a.no-decoration(href=`/${board._id}/index.html`) 10 | h1.board-title /#{board._id}/ - #{customPage.title} 11 | include ../includes/stickynav.pug 12 | +custompage(customPage) 13 | -------------------------------------------------------------------------------- /models/pages/boardsettings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildBoardSettings } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let json; 8 | try { 9 | json = await buildBoardSettings({ board: res.locals.board }); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.json(json); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /lib/post/markdown/escape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const entities = { 4 | '\'': ''', 5 | '/': '/', 6 | '`': '`', 7 | '=': '=', 8 | '&': '&', 9 | '<': '<', 10 | '>': '>', 11 | '"': '"' 12 | }; 13 | 14 | module.exports = (string) => { 15 | /* eslint-disable no-useless-escape */ 16 | return string.replace(/[&<>"'`=\/]/g, s => entities[s]); 17 | }; 18 | -------------------------------------------------------------------------------- /lib/post/markdown/handler/fortune.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fortunes = ['example1', 'example2', 'example3']; 4 | 5 | module.exports = { 6 | 7 | fortunes, 8 | 9 | regex: /##fortune/gmi, 10 | 11 | markdown: () => { 12 | const randomFortune = fortunes[Math.floor(Math.random()*fortunes.length)]; 13 | return `${randomFortune}`; 14 | }, 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /models/pages/create.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //const { buildCreate } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | return res.render('create'); 8 | 9 | /* 10 | let html; 11 | try { 12 | html = await buildCreate(); 13 | } catch (err) { 14 | return next(err); 15 | } 16 | 17 | return res.send(html); 18 | */ 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /models/pages/changepassword.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildChangePassword } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html; 8 | try { 9 | html = await buildChangePassword(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | return res.set('Cache-Control', 'max-age=0').send(html); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /lib/file/moveupload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uploadDirectory = require(__dirname+'/uploaddirectory.js'); 4 | 5 | module.exports = (file, filename, folder) => { 6 | 7 | return new Promise((resolve, reject) => { 8 | file.mv(`${uploadDirectory}/${folder}/${filename}`, function (err) { 9 | if (err) { 10 | return reject(err); 11 | } 12 | return resolve(); 13 | }); 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /migrations/0.6.1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Add global wordfilter autoban non-appealable option'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'filterBanAppealable': true, 8 | }, 9 | }); 10 | console.log('Clearing globalsettings cache'); 11 | await redis.deletePattern('globalsettings'); 12 | }; 13 | -------------------------------------------------------------------------------- /docker/jschan/Dockerfile-reset: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /opt 4 | 5 | COPY . . 6 | 7 | RUN npm install 8 | 9 | RUN npm i -g pm2 gulp 10 | 11 | COPY ./docker/jschan/secrets.js ./configs/secrets.js 12 | 13 | #i fucking hate docker 14 | ENV MONGO_USERNAME jschan 15 | ENV MONGO_PASSWORD changeme 16 | ENV REDIS_PASSWORD changeme 17 | 18 | RUN gulp generate-favicon 19 | 20 | CMD ["/bin/sh", "-c", "gulp reset; gulp"] 21 | -------------------------------------------------------------------------------- /migrations/0.0.12.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Renamed "yotsuba b" to "yotsuba-b" in any board settings'); 5 | await db.collection('boards').updateMany({ 6 | 'settings.theme': 'yotsuba b', 7 | }, { 8 | '$set': { 9 | 'settings.theme': 'yotsuba-b', 10 | } 11 | }); 12 | console.log('Cleared boards cache'); 13 | await redis.deletePattern('board:*'); 14 | }; 15 | -------------------------------------------------------------------------------- /docker/nginx/jschan.conf: -------------------------------------------------------------------------------- 1 | upstream chan { 2 | server jschan:7000; 3 | } 4 | 5 | server { 6 | server_name _; 7 | client_max_body_size 0; 8 | 9 | listen 80; 10 | listen [::]:80; 11 | 12 | include /etc/nginx/snippets/security_headers.conf; 13 | include /etc/nginx/snippets/error_pages.conf; 14 | include /etc/nginx/snippets/jschan_clearnet_routes.conf; 15 | include /etc/nginx/snippets/jschan_common_routes.conf; 16 | } 17 | -------------------------------------------------------------------------------- /schedules/tasks/deletecaptchas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deleteOld = require(__dirname+'/../../lib/file/deleteold.js') 4 | , timeUtils = require(__dirname+'/../../lib/converter/timeutils.js'); 5 | 6 | module.exports = { 7 | 8 | func: async () => { 9 | return deleteOld('captcha', Date.now()-(timeUtils.MINUTE*5)); 10 | }, 11 | interval: timeUtils.MINUTE*5, 12 | immediate: true, 13 | condition: null 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /views/mixins/postlink.pug: -------------------------------------------------------------------------------- 1 | mixin postlink(log, postLink, manageLink=false) 2 | if postLink.thread || postLink.postId 3 | a.quote(href=`/${postLink.board || log.board}/${manageLink ? 'manage/' : ''}thread/${postLink.thread || postLink.postId}.html#${postLink.postId}`) >>#{postLink.postId} 4 | else 5 | a.quote(href=`/${postLink.board || log.board}/${manageLink ? 'manage/' : ''}index.html`) >>>/#{postLink.board}/ 6 | 7 | -------------------------------------------------------------------------------- /migrations/0.1.10.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding threadwatcher to frontend script settings'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'frontendScriptDefault.threadWatcher': true, 8 | }, 9 | }); 10 | console.log('Clearing globalsettings cache'); 11 | await redis.deletePattern('globalsettings'); 12 | }; 13 | -------------------------------------------------------------------------------- /migrations/1.2.2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | 5 | console.log('Updating globalsettings to add uriDecodeFileNames'); 6 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 7 | '$set': { 8 | 'uriDecodeFileNames': false, 9 | }, 10 | }); 11 | 12 | console.log('Clearing globalsettings cache'); 13 | await redis.deletePattern('globalsettings'); 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /lib/post/markdown/handler/fortune.test.js: -------------------------------------------------------------------------------- 1 | const fortune = require('./fortune.js'); 2 | 3 | describe('fortune markdown', () => { 4 | test('should contain a random fortune for an input of ##fortune', () => { 5 | const output = '##fortune'.replace(fortune.regex, fortune.markdown.bind(null, false)); 6 | const hasFortuneText = fortune.fortunes.some(f => output.includes(f)); 7 | expect(hasFortuneText).toBe(true); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /models/pages/mypermissions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Permissions } = require(__dirname+'/../../lib/permission/permissions.js'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | res 8 | .set('Cache-Control', 'private, max-age=5') 9 | .render('mypermissions', { 10 | user: res.locals.user, 11 | permissions: res.locals.permissions, 12 | manageBoardBits: Permissions._MANAGE_BOARD_BITS, 13 | }); 14 | 15 | }; 16 | -------------------------------------------------------------------------------- /lib/misc/dynamic.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, code, page, data) => { 4 | 5 | res.status(code); 6 | 7 | if (req.body.minimal) { 8 | data.minimal = true; 9 | } 10 | 11 | if (req.headers && req.headers['x-using-xhr'] != null) { 12 | //if sending header with js, and not a bypass_minimal page, show modal 13 | return res.json(data); 14 | } else { 15 | return res.render(page, data); 16 | } 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /migrations/0.0.18.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Renaming disable onion file posting to disable anonymizer file posting'); 5 | await db.collection('boards').updateMany({}, { 6 | '$rename': { 7 | 'settings.disableOnionFilePosting' : 'settings.disableAnonymizerFilePosting', 8 | } 9 | }); 10 | console.log('Cleared boards cache'); 11 | await redis.deletePattern('board:*'); 12 | }; 13 | -------------------------------------------------------------------------------- /models/pages/nonce.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { randomBytes } = require('crypto') 4 | , cache = require(__dirname+'/../../lib/redis/redis.js'); 5 | 6 | module.exports = async (req, res) => { 7 | 8 | const address = req.params.address; 9 | const newNonce = (await randomBytes(32)).toString('base64'); 10 | await cache.set(`nonce:${address}:${newNonce}`, 1, 60); 11 | 12 | res.json({ 13 | nonce: newNonce, 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /views/includes/footer.pug: -------------------------------------------------------------------------------- 1 | unless minimal 2 | small.footer#bottom 3 | | - 4 | a(href='/news.html') #{__('news')} 5 | | - 6 | a(href='/rules.html') #{__('rules')} 7 | | - 8 | a(href='/faq.html') #{__('faq')} 9 | | - 10 | div 11 | a(href='https://gitgud.io/fatchan/jschan/') jschan 12 | | #{version} 13 | 14 | //- "render" script - for scripted things that are blocking 15 | script(src=`/js/render.js?v=${commit}`) 16 | -------------------------------------------------------------------------------- /views/mixins/watchedthread.pug: -------------------------------------------------------------------------------- 1 | mixin watchedthread(thread) 2 | .row.watched-thread(data-id=`${thread.board}-${thread.postId}` data-unread=(thread.unread||null)) 3 | a.close × 4 | - const watchedThreadLink = `/${thread.board}/thread/${thread.postId}.html`; 5 | a(class=(thread.isCurrentThread ? 'bold' : '') href=watchedThreadLink) /#{thread.board}/ - #{thread.subject || `#${thread.postId}`} 6 | a.ml-a(href=`${watchedThreadLink}#bottom`) [▼] 7 | -------------------------------------------------------------------------------- /gulp/res/js/hidefileinput.js: -------------------------------------------------------------------------------- 1 | document.querySelectorAll('input[type="file"]').forEach(fileInput => { 2 | //not using display: none because we still want to show the browser prompt for a "required" file 3 | fileInput.style.position = 'absolute'; 4 | fileInput.style.border = 'none'; 5 | fileInput.style.height = '1px'; 6 | fileInput.style.width = '1px'; 7 | // fileInput.style.opacity = '0'; // same effect as display:none in some browsers, ugh... 8 | }); 9 | -------------------------------------------------------------------------------- /lib/file/image/fixgifs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (file) => { 4 | //handle gifs with multiple geometry and size 5 | if (Array.isArray(file.geometry)) { 6 | file.geometry = file.geometry[0]; 7 | } 8 | if (Array.isArray(file.sizeString)) { 9 | file.sizeString = file.sizeString[0]; 10 | } 11 | if (Array.isArray(file.geometryString)) { 12 | file.geometryString = file.geometryString[0]; 13 | } 14 | return file; 15 | }; 16 | -------------------------------------------------------------------------------- /lib/middleware/file/numfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res, next) => { 4 | res.locals.numFiles = 0; 5 | if (req.files && req.files.file) { 6 | if (Array.isArray(req.files.file)) { 7 | res.locals.numFiles = req.files.file.filter(file => file.size > 0).length; 8 | } else { 9 | res.locals.numFiles = req.files.file.size > 0 ? 1 : 0; 10 | req.files.file = [req.files.file]; 11 | } 12 | } 13 | next(); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/post/markdown/sanitizeoptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | before: { 6 | allowedTags: [], 7 | allowedAttributes: {} 8 | }, 9 | 10 | after: { 11 | allowedTags: [ 'span', 'a', 'img', 'small'], 12 | allowedAttributes: { 13 | 'a': [ 'href', 'rel', 'class', 'referrerpolicy', 'target' ], 14 | 'span': [ 'class' ], 15 | 'img': ['src', 'height', 'width', 'alt', 'title', 'class'] 16 | } 17 | } 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /models/forms/upgradebans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | const nReturned = await Bans.upgrade(res.locals.bansBoard, req.body.checkedbans, req.body.upgrade) 8 | .then(explain => { 9 | if (explain && explain.stages) { 10 | return explain.stages[0].nReturned; 11 | } 12 | return 0; 13 | }); 14 | 15 | return nReturned; 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /schedules/tasks/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra') 4 | , Schedule = require(__dirname+'/../Schedule.js'); 5 | 6 | fs.readdirSync(__dirname).forEach(file => { 7 | if (file === 'index.js') { return; } 8 | const name = file.substring(0, file.length-3); 9 | const { func, interval, immediate, condition } = require(__dirname+'/'+file); 10 | module.exports[name] = new Schedule(func, interval, immediate, condition); 11 | }); 12 | -------------------------------------------------------------------------------- /views/includes/bantable.pug: -------------------------------------------------------------------------------- 1 | .table-container.mv-10.text-center.horscroll 2 | table.bantable 3 | tr 4 | th 5 | th #{__('Board')} 6 | th #{__('Reason')} 7 | th #{__('IP/ID')} 8 | th #{__('Type')} 9 | th #{__('Range')} 10 | th #{__('Issuer')} 11 | th #{__('Issue Date')} 12 | th #{__('Expiry')} 13 | th #{__('Post(s)')} 14 | th #{__('Seen?')} 15 | th #{__('Appealable?')} 16 | th #{__('Appeal')} 17 | th #{__('Note')} 18 | -------------------------------------------------------------------------------- /lib/middleware/ip/geoip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isAnonymizer } = require(__dirname+'/../../misc/countries.js') 4 | , config = require(__dirname+'/../../misc/config.js'); 5 | 6 | module.exports = (req, res, next) => { 7 | const { countryCodeHeader } = config.get; 8 | const code = req.headers[countryCodeHeader] || 'XX'; 9 | res.locals.anonymizer = isAnonymizer(code); 10 | res.locals.country = { 11 | code, 12 | }; 13 | return next(); 14 | }; 15 | -------------------------------------------------------------------------------- /lib/web3/web3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //NOTE: unused (for now) 4 | const { Web3 } = require('web3') 5 | , config = require(__dirname+'/../misc/config.js') 6 | , { addCallback } = require(__dirname+'/../redis/redis.js') 7 | , web3 = new Web3(config.get.ethereumNode); 8 | 9 | const updateWeb3Provider = () => { 10 | web3.setProvider(config.get.ethereumNode); 11 | }; 12 | 13 | addCallback('config', updateWeb3Provider); 14 | 15 | module.exports = web3; 16 | -------------------------------------------------------------------------------- /migrations/0.2.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding option to hide deleted post content to frontend script settings'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'frontendScriptDefault.hideDeletedPostContent': false, 8 | }, 9 | }); 10 | console.log('Clearing globalsettings cache'); 11 | await redis.deletePattern('globalsettings'); 12 | }; 13 | -------------------------------------------------------------------------------- /models/forms/editbanduration.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | //New ban expiry date is current date + ban_duration. Not based on the original ban issue date. 8 | const newExpireAt = new Date(Date.now() + req.body.ban_duration); 9 | return Bans.editDuration(res.locals.bansBoard, req.body.checkedbans, newExpireAt).then(result => result.modifiedCount); 10 | 11 | }; 12 | -------------------------------------------------------------------------------- /migrations/1.5.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | 5 | console.log('Updating all bans to have new global true/false flag'); 6 | await db.collection('bans').updateMany({ 7 | board: null, 8 | }, { 9 | '$set': { 10 | 'global': true, 11 | }, 12 | }); 13 | 14 | await db.collection('bans').updateMany({ 15 | board: { 16 | '$ne': null, 17 | }, 18 | }, { 19 | '$set': { 20 | 'global': false, 21 | }, 22 | }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /views/includes/2charisocountries.pug: -------------------------------------------------------------------------------- 1 | - const blockedCountries = new Set(board.settings.blockedCountries); 2 | select(name='countries' size='10' multiple) 3 | optgroup(label=__('Currently blocked')) 4 | each code in board.settings.blockedCountries 5 | option(value=code selected=true) #{countryNamesMap[code]} (#{code}) 6 | optgroup(label=__('Not blocked')) 7 | each code in countryCodes.filter(c => !blockedCountries.has(c)) 8 | option(value=code) #{countryNamesMap[code]} (#{code}) 9 | -------------------------------------------------------------------------------- /migrations/0.9.1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('make captcha font option apply to grid captcha too'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$unset': { 7 | 'captchaOptions.text.font': '', 8 | }, 9 | '$set': { 10 | 'captchaOptions.font': 'default', 11 | } 12 | }); 13 | console.log('Clearing globalsettings cache'); 14 | await redis.deletePattern('globalsettings'); 15 | }; 16 | -------------------------------------------------------------------------------- /models/pages/manage/mypermissions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js'); 4 | 5 | module.exports = async (req, res) => { 6 | 7 | res 8 | .set('Cache-Control', 'private, max-age=5') 9 | .render('managemypermissions', { 10 | user: res.locals.user, 11 | board: res.locals.board, 12 | permissions: res.locals.permissions, 13 | manageBoardBits: Permissions._MANAGE_BOARD_BITS, 14 | }); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /views/mixins/uploaditem.pug: -------------------------------------------------------------------------------- 1 | mixin uploaditem(item) 2 | div 3 | .upload-item 4 | img.upload-thumb(src=item.url) 5 | p #{item.name} 6 | a.close × 7 | if item.hash 8 | .row.sb 9 | if item.spoilers 10 | label 11 | input(type='checkbox', name='spoiler', value=item.hash) 12 | | #{__('Spoiler')} 13 | if item.stripFilenames 14 | label 15 | input(type='checkbox', name='strip_filename', value=item.hash) 16 | | #{__('Strip Filename')} 17 | -------------------------------------------------------------------------------- /models/pages/manage/editpost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res) => { 4 | 5 | return res 6 | .set('Cache-Control', 'private, max-age=5') 7 | .render('editpost', { 8 | 'csrf': req.csrfToken(), 9 | 'post': res.locals.post, 10 | 'board': res.locals.board, 11 | 'referer': (req.headers.referer || `/${res.locals.post.board}/manage/thread/${res.locals.post.thread || res.locals.post.postId}.html`) + `#${res.locals.post.postId}`, 12 | }); 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /views/pages/globalmanagebans.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/ban.pug 3 | include ../mixins/globalmanagenav.pug 4 | 5 | block head 6 | title #{__('Bans')} 7 | 8 | block content 9 | h1.board-title #{__('Global Management')} 10 | br 11 | +globalmanagenav('bans') 12 | hr(size=1) 13 | h4.no-m-p #{__('Global Bans & Appeals')}: 14 | form(action=`/forms/global/editbans` method='POST' enctype='application/x-www-form-urlencoded') 15 | include ../includes/managebanform.pug 16 | -------------------------------------------------------------------------------- /lib/file/ffprobe.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ffmpeg = require('fluent-ffmpeg') 4 | , uploadDirectory = require(__dirname+'/uploaddirectory.js'); 5 | 6 | module.exports = (filename, folder, temp) => { 7 | 8 | return new Promise((resolve, reject) => { 9 | ffmpeg.ffprobe(temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`, (err, metadata) => { 10 | if (err) { 11 | return reject(err); 12 | } 13 | return resolve(metadata); 14 | }); 15 | }); 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /migrations/0.11.4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('adding image/video resolution maximum setting'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'globalLimits.postFilesSize.imageResolution': 100000000, 8 | 'globalLimits.postFilesSize.videoResolution': 77414400, 9 | }, 10 | }); 11 | console.log('Clearing globalsettings cache'); 12 | await redis.deletePattern('globalsettings'); 13 | }; 14 | -------------------------------------------------------------------------------- /models/pages/globalmanage/news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { News } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let news; 8 | try { 9 | news = await News.find(); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('globalmanagenews', { 17 | csrf: req.csrfToken(), 18 | permissions: res.locals.permissions, 19 | news, 20 | }); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /models/pages/manage/custompages.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CustomPages = require(__dirname+'/../../../db/custompages.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let customPages; 8 | try { 9 | customPages = await CustomPages.find(req.params.board); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('managecustompages', { 17 | csrf: req.csrfToken(), 18 | customPages, 19 | }); 20 | 21 | }; 22 | -------------------------------------------------------------------------------- /migrations/1.0.5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Updating db for language settings, fixes broken 1.0.0 migration'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'boardDefaults.language': 'en-GB', 8 | }, 9 | }); 10 | 11 | console.log('Clearing globalsettings cache'); 12 | await redis.deletePattern('globalsettings'); 13 | console.log('Clearing boards cache'); 14 | await redis.deletePattern('board:*'); 15 | }; 16 | -------------------------------------------------------------------------------- /views/pages/managebans.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/ban.pug 3 | include ../mixins/managenav.pug 4 | include ../mixins/boardheader.pug 5 | 6 | block head 7 | title /#{board._id}/ - #{__('Bans & Appeals')} 8 | 9 | block content 10 | +boardheader(__('Bans')) 11 | br 12 | +managenav('bans') 13 | hr(size=1) 14 | h4.mv-5 #{__('Bans & Appeals')}: 15 | form(action=`/forms/board/${board._id}/editbans` method='POST' enctype='application/x-www-form-urlencoded') 16 | include ../includes/managebanform.pug 17 | -------------------------------------------------------------------------------- /models/pages/globalmanage/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Filters } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let filters; 8 | try { 9 | filters = await Filters.findForBoard(null); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('globalmanagefilters', { 17 | csrf: req.csrfToken(), 18 | permissions: res.locals.permissions, 19 | filters, 20 | }); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /models/pages/manage/filters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Filters } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let filters; 8 | try { 9 | filters = await Filters.findForBoard(req.params.board); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('managefilters', { 17 | csrf: req.csrfToken(), 18 | permissions: res.locals.permissions, 19 | filters, 20 | }); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /views/mixins/boardtable.pug: -------------------------------------------------------------------------------- 1 | mixin boardtable(ppd=false, activity=false, owner=false) 2 | .table-container.flex-center.mv-10.text-center 3 | table(class=`boardtable${activity ? ' w900' : ''}`) 4 | tr 5 | th #{__('Board')} 6 | if owner 7 | th #{__('Owner')} 8 | th #{__('Description')} 9 | th #{__('PPH')} 10 | if ppd 11 | th.nobreak #{__('PPD')} 12 | th.nobreak #{__('Users')} 13 | th.nobreak #{__('Posts')} 14 | if activity 15 | th.nobreak #{__('Latest Activity')} 16 | block 17 | -------------------------------------------------------------------------------- /views/mixins/report.pug: -------------------------------------------------------------------------------- 1 | mixin report(r, manage=false) 2 | .reports.post-container 3 | input.post-check(type='checkbox', name='checkedreports' value=r.id) 4 | | 5 | - const ip = viewRawIp === true ? r.ip.raw : r.ip.cloak; 6 | a.bold(href=`${manage ? 'recent.html' : ''}?ip=${encodeURIComponent(ip)}`) [#{ip}] 7 | | 8 | - const reportDate = new Date(r.date); 9 | time.reltime(datetime=reportDate.toISOString()) #{reportDate.toLocaleString(pageLanguage, { hourCycle:'h23' })} 10 | | | #{__('Reason')}: #{r.reason} 11 | -------------------------------------------------------------------------------- /lib/input/escaperegexp.test.js: -------------------------------------------------------------------------------- 1 | const escapeRegExp = require('./escaperegexp.js'); 2 | 3 | describe('escape regular expression', () => { 4 | 5 | const cases = [ 6 | { in: '', out: '' }, 7 | { in: '/', out: '/' }, 8 | { in: '.*+?^${}()|[]\\', out: '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\' }, 9 | ]; 10 | 11 | for (let i in cases) { 12 | test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => { 13 | expect(escapeRegExp(cases[i].in)).toStrictEqual(cases[i].out); 14 | }); 15 | } 16 | 17 | }); 18 | -------------------------------------------------------------------------------- /models/forms/dismissglobalreport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (locals) => { 4 | 5 | const { posts, __ } = locals; 6 | 7 | const filteredposts = posts.filter(post => { 8 | return post.globalreports.length > 0; 9 | }); 10 | 11 | if (filteredposts.length === 0) { 12 | return { 13 | message: __('No global reports to dismiss'), 14 | }; 15 | } 16 | 17 | return { 18 | message: __('Dismissed global reports'), 19 | action: '$set', 20 | query: { 21 | 'globalreports': [] 22 | } 23 | }; 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /models/pages/manage/catalog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let threads; 8 | try { 9 | threads = await Posts.getCatalog(req.params.board); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | res 15 | //.set('Cache-Control', 'private, max-age=5') 16 | .render('catalog', { 17 | modview: true, 18 | threads, 19 | board: res.locals.board, 20 | csrf: req.csrfToken(), 21 | }); 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /lib/file/deletepostfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { remove } = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/uploaddirectory.js'); 5 | 6 | module.exports = (files) => { 7 | 8 | //delete all the files and thumbs 9 | return Promise.all(files.map(async file => { 10 | return Promise.all([ 11 | remove(`${uploadDirectory}/file/${file.filename}`), 12 | file.hasThumb ? remove(`${uploadDirectory}/file/thumb/${file.hash}${file.thumbextension}`) : void 0, 13 | ]).catch(console.error); 14 | })); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /migrations/0.6.3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | 5 | console.log('unfucking any broken board tags'); 6 | await db.collection('boards').updateMany({ 7 | 'webring': false, 8 | 'tags': null, 9 | }, { 10 | '$set': { 11 | 'tags': [], //null -> empty array, for broken boards 12 | } 13 | }); 14 | console.log('Clearing globalsettings cache'); 15 | await redis.deletePattern('globalsettings'); 16 | console.log('Clearing board cache'); 17 | await redis.deletePattern('board:*'); 18 | 19 | }; 20 | -------------------------------------------------------------------------------- /docker/jschan/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | RUN apt-get update -y 4 | RUN apt-get install ffmpeg imagemagick graphicsmagick -y 5 | 6 | WORKDIR /opt 7 | 8 | COPY . . 9 | 10 | RUN npm install 11 | 12 | RUN npm install -g pm2 gulp 13 | 14 | COPY ./docker/jschan/secrets.js ./configs/secrets.js 15 | 16 | #i fucking hate docker 17 | ENV MONGO_USERNAME jschan 18 | ENV MONGO_PASSWORD changeme 19 | ENV REDIS_PASSWORD changeme 20 | 21 | RUN gulp generate-favicon 22 | 23 | CMD ["/bin/sh", "-c", "gulp; pm2-runtime start ecosystem.config.js"] 24 | -------------------------------------------------------------------------------- /views/mixins/boardheader.pug: -------------------------------------------------------------------------------- 1 | mixin boardheader(pagename) 2 | .board-header 3 | if board.settings != null && board.settings.hideBanners === false 4 | img.board-banner(src=`/randombanner?board=${board._id}` loading='lazy') 5 | br 6 | if pagename 7 | h1.board-title #{pagename} (#[a.no-decoration(href=`/${board._id}/index.html`) /#{board._id}/]) 8 | else 9 | a.no-decoration(href=`/${board._id}/index.html`) 10 | h1.board-title /#{board._id}/ - #{board.settings.name} 11 | h4.board-description #{board.settings.description} 12 | -------------------------------------------------------------------------------- /lib/input/pagequeryconverter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (query, limit) => { 4 | query = query || {}; 5 | const nopage = { ...query }; 6 | delete nopage.page; 7 | const queryString = new URLSearchParams(nopage).toString(); 8 | let page; 9 | if (query.page && Number.isSafeInteger(parseInt(query.page, 10))) { 10 | page = parseInt(query.page, 10); 11 | if (page <= 0) { 12 | page = 1; 13 | } 14 | } else { 15 | page = 1; 16 | } 17 | const offset = (page-1) * limit; 18 | return { queryString, page, offset }; 19 | }; 20 | -------------------------------------------------------------------------------- /models/pages/banners.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildBanners } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html, json; 8 | try { 9 | ({ html, json } = await buildBanners({ board: res.locals.board })); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (req.path.endsWith('.json')) { 15 | return res.set('Cache-Control', 'max-age=0').json(json); 16 | } else { 17 | return res.set('Cache-Control', 'max-age=0').send(html); 18 | } 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /models/pages/catalog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildCatalog } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html, json; 8 | try { 9 | ({ html, json } = await buildCatalog({ board: res.locals.board })); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (req.path.endsWith('.json')) { 15 | return res.set('Cache-Control', 'max-age=0').json(json); 16 | } else { 17 | return res.set('Cache-Control', 'max-age=0').send(html); 18 | } 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /tools/self_signed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # for development, generate a simple self signed ssl cert and make /etc/nginx/snippets/self-signed.conf 4 | # 5 | 6 | sudo openssl req -x509 -nodes -days 10000 -newkey rsa:2048 -keyout /etc/ssl/private/nginx-selfsigned.key -out /etc/ssl/certs/nginx-selfsigned.crt 7 | sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048 8 | cat > /etc/nginx/snippets/self-signed.conf < { 6 | 7 | let html, json; 8 | try { 9 | ({ html, json } = await buildModLogList({ board: res.locals.board })); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (req.path.endsWith('.json')) { 15 | return res.set('Cache-Control', 'max-age=0').json(json); 16 | } else { 17 | return res.set('Cache-Control', 'max-age=0').send(html); 18 | } 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /lib/file/image/getdimensions.js: -------------------------------------------------------------------------------- 1 | const gm = require('@fatchan/gm') 2 | , uploadDirectory = require(__dirname+'/../uploaddirectory.js'); 3 | 4 | module.exports = (filename, folder, temp) => { 5 | 6 | return new Promise((resolve, reject) => { 7 | const filePath = temp === true ? filename : `${uploadDirectory}/${folder}/${filename}`; 8 | gm(`${filePath}[0]`) //0 for first frame of gifs, much faster 9 | .size(function (err, data) { 10 | if (err) { 11 | return reject(err); 12 | } 13 | return resolve(data); 14 | }); 15 | }); 16 | 17 | }; 18 | -------------------------------------------------------------------------------- /lib/locale/locale.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const i18n = require('i18n') 4 | , path = require('path') 5 | , { debugLogs } = require(__dirname+'/../../configs/secrets.js'); 6 | 7 | i18n.configure({ 8 | directory: path.join(__dirname, '/../../locales'), 9 | defaultLocale: 'en-GB', 10 | retryInDefaultLocale: false, 11 | updateFiles: false, //holy FUCK why is that an option 12 | cookie: null, 13 | header: null, 14 | queryParameter: null, 15 | }); 16 | 17 | debugLogs && console.log('Locales loaded:', i18n.getLocales()); 18 | 19 | module.exports = i18n; 20 | -------------------------------------------------------------------------------- /migrations/0.0.5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require(__dirname+'/../lib/misc/config.js'); 4 | 5 | module.exports = async(db, redis) => { 6 | console.log('Adding bumplimit field to boards on posts'); 7 | await db.collection('boards').updateMany({}, { 8 | '$set': { 9 | 'settings.bumpLimit': config.get.globalLimits.bumpLimit.max, 10 | } 11 | }); 12 | console.log('Cleared boards cache'); 13 | await redis.deletePattern('board:*'); 14 | console.log('Cleared globalsettings cache'); 15 | await redis.deletePattern('globalsettings'); 16 | }; 17 | -------------------------------------------------------------------------------- /lib/file/deletetempfiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { remove } = require('fs-extra'); 4 | 5 | module.exports = async (req) => { 6 | 7 | if (req.files != null) { 8 | let files = []; 9 | const keys = Object.keys(req.files); 10 | for (let i = 0; i < keys.length; i++) { 11 | const val = req.files[keys[i]]; 12 | if (Array.isArray(val)) { 13 | files = files.concat(val); 14 | } else { 15 | files.push(val); 16 | } 17 | } 18 | return Promise.all(files.map(async file => { 19 | remove(file.tempFilePath); 20 | })); 21 | } 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /models/pages/globalmanage/editnews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { News } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let news; 8 | try { 9 | news = await News.findOne(req.params.newsid); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (!news) { 15 | return next(); 16 | } 17 | 18 | res 19 | .set('Cache-Control', 'private, max-age=5') 20 | .render('editnews', { 21 | csrf: req.csrfToken(), 22 | permissions: res.locals.permissions, 23 | news, 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /views/pages/bypass.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title #{__('Block Bypass')} 5 | 6 | block content 7 | h1.board-title #{__('Block Bypass')} 8 | .form-wrapper.flex-center.mv-10 9 | form.form-post(action=`/forms/blockbypass?language=${encodeURIComponent(pageLanguage)}` method='POST' data-captcha-preload='true') 10 | .row 11 | .col 12 | include ../includes/captcha.pug 13 | if minimal 14 | input(type='hidden' name='minimal' value='1') 15 | input(type='submit', value=__('Submit')) 16 | if message 17 | p.title #{message} 18 | -------------------------------------------------------------------------------- /tools/update_geoip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script to update geoip database for nginx/jschan. Can be added as a cronjob 4 | # 5 | 6 | #go to geoip folder 7 | cd /usr/share/GeoIP 8 | #move the existing db to a .bak just in case 9 | mv GeoIP.dat GeoIP.dat.bak 10 | #try and download the DBIP database 11 | wget --retry-connrefused https://dl.miyuru.lk/geoip/dbip/country/dbip.dat.gz 12 | #extract and move it 13 | gunzip dbip.dat.gz 14 | mv dbip.dat GeoIP.dat 15 | #make sure www-data (debian nginx user:group) has permissions 16 | chown www-data:www-data /usr/share/GeoIP/GeoIP.dat 17 | -------------------------------------------------------------------------------- /lib/input/decodequeryip.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isIP } = require('net') 4 | , { Permissions } = require(__dirname+'/../permission/permissions.js'); 5 | 6 | module.exports = (query, permissions) => { 7 | if (query && query.ip && typeof query.ip === 'string') { 8 | const decoded = decodeURIComponent(query.ip); 9 | //if is IP but no permission, return null 10 | if (isIP(decoded) && !permissions.get(Permissions.VIEW_RAW_IP)) { 11 | return null; 12 | } 13 | return decoded; //otherwise return ip/cloak query 14 | } 15 | return null; //else, no ip filter 16 | }; 17 | -------------------------------------------------------------------------------- /models/forms/spoilerpost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (locals) => { 4 | 5 | const { __, __n, posts } = locals; 6 | 7 | // filter to ones not spoilered 8 | const filteredPosts = posts.filter(post => { 9 | return !post.spoiler && post.files.length > 0; 10 | }); 11 | 12 | if (filteredPosts.length === 0) { 13 | return { 14 | message: __('No files to spoiler'), 15 | }; 16 | } 17 | 18 | return { 19 | message: __n('Spoilered %s posts', filteredPosts.length), 20 | action: '$set', 21 | query: { 22 | 'spoiler': true 23 | } 24 | }; 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /models/pages/blockbypass.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildBypass } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | if (res.locals.minimal === true) { 8 | return res 9 | .set('Cache-Control', 'public, max-age=60') 10 | .render('bypass', { 11 | minimal: true 12 | }); 13 | } 14 | 15 | let html; 16 | try { 17 | html = await buildBypass(res.locals.minimal); 18 | } catch (err) { 19 | return next(err); 20 | } 21 | 22 | return res.set('Cache-Control', 'max-age=0').send(html); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /views/includes/navbar.pug: -------------------------------------------------------------------------------- 1 | unless minimal 2 | nav.navbar 3 | a.nav-item(href='/index.html') #{__('Home')} 4 | a(href='/boards.html' class=`nav-item ${enableWebring ? 'short-nav' : ''}`) 5 | | #{__('Boards')} 6 | if enableWebring 7 | .rainbow +Webring 8 | a.nav-item#overboardlink(href='/overboard.html') #{__('Overboard')} 9 | a.nav-item(href='/account.html') #{__('Account')} 10 | if !modview && board 11 | a.nav-item(href=`/${board._id}/manage/${managePage || 'index.html'}`) #{__('Manage')} 12 | a.jsonly.nav-item.right#settings(data-label=(__('Settings'))) 13 | -------------------------------------------------------------------------------- /views/includes/posticons.pug: -------------------------------------------------------------------------------- 1 | if post.sticky || post.bumplocked || post.locked || post.cyclic 2 | span.post-icons 3 | if post.sticky 4 | img(src='/file/sticky.png' height='14' width='14' title=`${__('Sticky')} (${post.sticky})`) 5 | | 6 | if post.bumplocked 7 | img(src='/file/bumplock.png' height='14' width='14' title=__('Bumplocked')) 8 | | 9 | if post.locked 10 | img(src='/file/lock.png' height='14' width='14' title=__('Locked')) 11 | | 12 | if post.cyclic 13 | img(src='/file/cyclic.png' height='14' width='14' title=__('Cyclic')) 14 | | 15 | 16 | -------------------------------------------------------------------------------- /models/forms/removebans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../db/') 4 | , { Permissions } = require(__dirname+'/../../lib/permission/permissions.js'); 5 | 6 | module.exports = async (req, res) => { 7 | 8 | const showGlobal = res.locals.permissions.get(Permissions.VIEW_BOARD_GLOBAL_BANS); 9 | const bansBoard = req.params && req.params.board 10 | ? (showGlobal 11 | ? req.params.board 12 | : { '$eq': req.params.board }) 13 | : null; 14 | return Bans.removeMany(bansBoard, req.body.checkedbans).then(result => result.deletedCount); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /views/includes/pages.pug: -------------------------------------------------------------------------------- 1 | | #{__('Page')}: 2 | - const qs = queryString ? queryString+'&' : '' 3 | if maxPage === 0 4 | a.bold(href=`?${qs}page=1`) [1] 5 | else if maxPage > 0 6 | - for(let i = 1; i <= maxPage; i++) 7 | if page === i 8 | a.bold(href=`?${qs}page=${i}`) [#{i}] 9 | else 10 | a(href=`?${qs}page=${i}`) [#{i}] 11 | | 12 | else 13 | if page > 1 14 | if page > 2 15 | a(href=`?${qs}page=1`) [<<] 16 | | 17 | a(href=`?${qs}page=${page-1}`) [<] 18 | | 19 | a.bold(href=`?${qs}page=${page}`) [#{page}] 20 | | 21 | a(href=`?${qs}page=${page+1}`) [>] 22 | -------------------------------------------------------------------------------- /migrations/0.0.20.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/../lib/file/uploaddirectory.js'); 5 | 6 | module.exports = async(db) => { 7 | console.log('put thumbs in a folder'); 8 | await fs.ensureDir(`${uploadDirectory}/file/thumb/`); 9 | await db.collection('files') 10 | .find() 11 | .forEach(async file => { 12 | const [hash] = file._id.split('.'); 13 | file.exts.forEach(ext => { 14 | fs.moveSync(`${uploadDirectory}/file/thumb-${hash}${ext}`, `${uploadDirectory}/file/thumb/${hash}${ext}`); 15 | }); 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /models/pages/globalmanage/editfilter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Filters } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let filter; 8 | try { 9 | filter = await Filters.findOne(null, req.params.filterid); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (!filter) { 15 | return next(); 16 | } 17 | 18 | res 19 | .set('Cache-Control', 'private, max-age=5') 20 | .render('globaleditfilter', { 21 | csrf: req.csrfToken(), 22 | permissions: res.locals.permissions, 23 | filter, 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /models/pages/manage/editfilter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Filters } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let filter; 8 | try { 9 | filter = await Filters.findOne(req.params.board, req.params.filterid); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (!filter) { 15 | return next(); 16 | } 17 | 18 | res 19 | .set('Cache-Control', 'private, max-age=5') 20 | .render('editfilter', { 21 | csrf: req.csrfToken(), 22 | permissions: res.locals.permissions, 23 | filter, 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /lib/middleware/permission/isloggedin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async (req, res, next) => { 4 | if (res.locals.user) { 5 | return next(); 6 | } 7 | let goto; 8 | if (req.method === 'GET' && req.path) { 9 | //coming from a GET page isLoggedIn middleware check 10 | goto = encodeURIComponent(req.path); 11 | } 12 | if (req.params.board && req.params.id) { 13 | if (req.path.includes('/manage/thread/')) { 14 | return res.redirect(`/${req.params.board}/thread/${req.params.id}.html`); 15 | } 16 | } 17 | return res.redirect(`/login.html${goto ? '?goto='+goto : ''}`); 18 | }; 19 | -------------------------------------------------------------------------------- /views/pages/editfilter.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/managenav.pug 3 | include ../mixins/boardheader.pug 4 | 5 | block head 6 | title /#{board._id}/ - #{__('Edit Filter')} 7 | 8 | block content 9 | +boardheader(__('Edit Filter')) 10 | br 11 | +managenav('filters', true) 12 | hr(size=1) 13 | h4.no-m-p 14 | | #{__('Edit Filter')} 15 | | ( 16 | a(href='/faq.html#filters') #{__('Filters FAQ')} 17 | | ): 18 | .form-wrapper.flex-center.mv-10 19 | form.form-post(action=`/forms/board/${board._id}/editfilter` method='POST') 20 | include ../includes/filtereditform.pug 21 | -------------------------------------------------------------------------------- /views/pages/globaleditfilter.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | 4 | block head 5 | title #{__('Edit Filter')} 6 | 7 | block content 8 | h1.board-title #{__('Global Management')} 9 | br 10 | +globalmanagenav('filters', true) 11 | hr(size=1) 12 | h4.no-m-p 13 | | #{__('Edit Filter')} 14 | | ( 15 | a(href='/faq.html#filters') #{__('Filters FAQ')} 16 | | ): 17 | include ../includes/stickynav.pug 18 | .form-wrapper.flex-center.mv-10 19 | form.form-post(action='/forms/global/editfilter' method='POST') 20 | include ../includes/filtereditform.pug 21 | -------------------------------------------------------------------------------- /migrations/0.0.23.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('add markdown permissions'); 5 | const settings = await redis.get('globalsettings'); 6 | const newSettings = { 7 | ...settings, 8 | permLevels: { 9 | markdown: { 10 | pink: 4, 11 | green: 4, 12 | bold: 4, 13 | underline: 4, 14 | strike: 4, 15 | italic: 4, 16 | title: 4, 17 | spoiler: 4, 18 | mono: 4, 19 | code: 4, 20 | link: 4, 21 | detected: 4, 22 | dice: 4, 23 | fortune: 0, 24 | }, 25 | }, 26 | }; 27 | redis.set('globalsettings', newSettings); 28 | }; 29 | -------------------------------------------------------------------------------- /models/pages/custompage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildCustomPage } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html, json; 8 | try { 9 | ({ html, json } = await buildCustomPage({ ...req.params, board: res.locals.board })); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (!html) { 15 | return next(); 16 | } 17 | 18 | if (req.path.endsWith('.json')) { 19 | return res.set('Cache-Control', 'max-age=0').json(json); 20 | } else { 21 | return res.set('Cache-Control', 'max-age=0').send(html); 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /models/pages/sessions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const redis = require(__dirname+'/../../lib/redis/redis.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let sessions; 8 | try { 9 | sessions = await redis.getPattern(`sess:*:${res.locals.user.username}`); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('sessions', { 17 | csrf: req.csrfToken(), 18 | user: res.locals.user, 19 | permissions: res.locals.permissions, 20 | currentSessionKey: `sess:${req.sessionID}`, 21 | sessions, 22 | }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /configs/nginx/snippets/error_pages.conf: -------------------------------------------------------------------------------- 1 | error_page 404 /404.html; 2 | error_page 500 /500.html; 3 | error_page 502 /502.html; 4 | error_page 503 /503.html; 5 | error_page 504 /504.html; 6 | location = /404.html { 7 | root /path/to/jschan/static/html; 8 | internal; 9 | } 10 | location = /500.html { 11 | root /path/to/jschan/static/html; 12 | internal; 13 | } 14 | location = /502.html { 15 | root /path/to/jschan/static/html; 16 | internal; 17 | } 18 | location = /503.html { 19 | root /path/to/jschan/static/html; 20 | internal; 21 | } 22 | location = /504.html { 23 | root /path/to/jschan/static/html; 24 | internal; 25 | } 26 | -------------------------------------------------------------------------------- /models/pages/manage/editcustompage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { CustomPages } = require(__dirname+'/../../../db/'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let customPage; 8 | try { 9 | customPage = await CustomPages.findOneId(req.params.custompageid, req.params.board); 10 | } catch (err) { 11 | return next(err); 12 | } 13 | 14 | if (!customPage) { 15 | return next(); 16 | } 17 | 18 | res 19 | .set('Cache-Control', 'private, max-age=5') 20 | .render('editcustompage', { 21 | csrf: req.csrfToken(), 22 | page: customPage, 23 | board: res.locals.board, 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /migrations/1.6.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | 5 | console.log('Updating globalsettings to add 2fa enforcement options'); 6 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 7 | '$set': { 8 | 'forceAccountTwofactor': false, 9 | 'forceActionTwofactor': false, //TODO: potentially break this down to be more granular on what needs 2fa 10 | }, 11 | }); 12 | 13 | console.log('Clearing globalsettings cache'); 14 | await redis.deletePattern('globalsettings'); 15 | console.log('Clearing boards cache'); 16 | await redis.deletePattern('board:*'); 17 | 18 | }; 19 | -------------------------------------------------------------------------------- /models/forms/lockposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js'); 4 | 5 | module.exports = (locals) => { 6 | 7 | const { posts, __, __n } = locals; 8 | 9 | const filteredposts = posts.filter(post => { 10 | return !post.thread; 11 | }); 12 | 13 | if (filteredposts.length === 0) { 14 | return { 15 | message: __('No threads selected to Lock'), 16 | }; 17 | } 18 | 19 | return { 20 | message: __n('Toggled Lock for %s threads', filteredposts.length), 21 | action: '$bit', 22 | query: { 23 | 'locked': { 24 | 'xor': NumberInt(1) 25 | }, 26 | } 27 | }; 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /views/pages/editrole.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | 4 | block head 5 | title #{__('Edit Role')} 6 | 7 | block content 8 | h1.board-title #{__('Global Management')} 9 | br 10 | +globalmanagenav('roles', true) 11 | hr(size=1) 12 | h4.mv-5 #{__('Edit role %s', __(roleNameMap[role.name]))} 13 | - const jsonPermissions = rolePermissions.toJSON(); 14 | .form-wrapper.flexleft 15 | form(action=`/forms/global/editrole` method='POST') 16 | input(type='hidden' name='_csrf' value=csrf) 17 | input(type='hidden' name='roleid' value=role._id) 18 | include ../includes/globalpermissionsform.pug 19 | -------------------------------------------------------------------------------- /views/pages/globalmanageroles.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | 4 | block head 5 | title #{__('Roles')} 6 | 7 | block content 8 | h1.board-title #{__('Global Management')} 9 | br 10 | +globalmanagenav('roles') 11 | hr(size=1) 12 | h4.mv-5 #{__('Roles')}: 13 | .table-container.flex-left.text-center 14 | table 15 | tr 16 | th #{__('Role')} 17 | th #{__('Permissions')} 18 | for role in allRoles 19 | tr 20 | td(title=`Raw permissions: ${role.permissions.toString('base64')}`) #{roleNameMap[role.name]} 21 | td: a(href=`/globalmanage/editrole/${role._id}.html`) [#{__('Edit')}] 22 | 23 | -------------------------------------------------------------------------------- /views/pages/message.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title #{title} 5 | if redirect 6 | meta(http-equiv="refresh" content=`3;url=${redirect}`) 7 | 8 | block content 9 | h1 #{title} 10 | ul 11 | if message 12 | li #{message} 13 | if error 14 | li #{error} 15 | if messages 16 | each msg in messages 17 | li #{msg} 18 | if errors 19 | each error in errors 20 | li #{error} 21 | if link 22 | div 23 | a.button(href=link.href target='_blank') #{link.text} 24 | if redirect 25 | p #{__('You will be redirected shortly.')} #[a(href=redirect) #{__('Click here if you are not redirected automatically.')}] 26 | -------------------------------------------------------------------------------- /models/forms/bumplockposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js'); 4 | 5 | module.exports = (locals) => { 6 | 7 | const { posts, __, __n } = locals; 8 | 9 | const filteredposts = posts.filter(post => { 10 | return !post.thread; 11 | }); 12 | 13 | if (filteredposts.length === 0) { 14 | return { 15 | message: __('No threads selected to Bumplock'), 16 | }; 17 | } 18 | 19 | return { 20 | message: __n('Toggled Bumplock for %s threads', filteredposts.length), 21 | action: '$bit', 22 | query: { 23 | 'bumplocked': { 24 | 'xor': NumberInt(1) 25 | }, 26 | } 27 | }; 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /models/forms/cycleposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js'); 4 | 5 | module.exports = (locals) => { 6 | 7 | const { posts, __, __n } = locals; 8 | 9 | const filteredposts = posts.filter(post => { 10 | return !post.thread; 11 | }); 12 | 13 | if (filteredposts.length === 0) { 14 | return { 15 | message: __('No threads selected to make Cyclical'), 16 | }; 17 | } 18 | 19 | return { 20 | message: __n('Toggled Cyclical mode for %s threads', filteredposts.length), 21 | action: '$bit', 22 | query: { 23 | 'cyclic': { 24 | 'xor': NumberInt(1) 25 | }, 26 | } 27 | }; 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /lib/post/markdown/escape.test.js: -------------------------------------------------------------------------------- 1 | const simpleEscape = require('./escape.js'); 2 | 3 | describe('simpleEscape() - convert some characters to html entities', () => { 4 | const cases = [ 5 | { in: '\'', out: ''' }, 6 | { in: '/', out: '/' }, 7 | { in: '`', out: '`' }, 8 | { in: '=', out: '=' }, 9 | { in: '&', out: '&' }, 10 | { in: '<', out: '<' }, 11 | { in: '>', out: '>' }, 12 | { in: '"', out: '"' }, 13 | ]; 14 | for (let i in cases) { 15 | test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => { 16 | expect(simpleEscape(cases[i].in)).toBe(cases[i].out); 17 | }); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /models/pages/globalmanage/bans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bans } = require(__dirname+'/../../../db/') 4 | , { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | let bans; 9 | try { 10 | bans = await Bans.getGlobalBans(); 11 | } catch (err) { 12 | return next(err); 13 | } 14 | 15 | res 16 | .set('Cache-Control', 'private, max-age=5') 17 | .render('globalmanagebans', { 18 | csrf: req.csrfToken(), 19 | permissions: res.locals.permissions, 20 | viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP), 21 | bans, 22 | }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /models/pages/manage/thread.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let thread; 8 | try { 9 | thread = await Posts.getThread(res.locals.board._id, res.locals.thread.postId, true); 10 | if (!thread) { 11 | return next(); //deleted between exists 12 | } 13 | } catch (err) { 14 | return next(err); 15 | } 16 | 17 | res 18 | .set('Cache-Control', 'private, max-age=5') 19 | .render('thread', { 20 | modview: true, 21 | upLevel: true, 22 | board: res.locals.board, 23 | thread, 24 | csrf: req.csrfToken(), 25 | }); 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /models/forms/deletenews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { News } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') 5 | , buildQueue = require(__dirname+'/../../lib/build/queue.js'); 6 | 7 | module.exports = async (req, res) => { 8 | 9 | const { __ } = res.locals; 10 | 11 | await News.deleteMany(req.body.checkednews); 12 | 13 | buildQueue.push({ 14 | 'task': 'buildNews', 15 | 'options': {} 16 | }); 17 | 18 | return dynamicResponse(req, res, 200, 'message', { 19 | 'title': __('Success'), 20 | 'message': __('Deleted news'), 21 | 'redirect': '/globalmanage/news.html' 22 | }); 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /migrations/0.1.2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('setting webring:false to boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$set': { 7 | 'webring': false, 8 | }, 9 | '$rename': { 10 | 'settings.tags': 'tags' 11 | } 12 | }); 13 | await db.collection('boards').dropIndexes(); 14 | await db.collection('boards').createIndex({ips: 1, pph:1, sequence_value:1}); 15 | await db.collection('boards').createIndex({tags: 1}); 16 | await db.collection('boards').createIndex({uri: 1}); 17 | await db.collection('boards').createIndex({lastPostTimestamp:1}); 18 | await db.dropCollection('webring'); 19 | }; 20 | -------------------------------------------------------------------------------- /models/forms/dismissreport.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (req, res) => { 4 | 5 | const { posts, __ } = res.locals; 6 | 7 | const filteredposts = posts.filter(post => { 8 | return (req.body.global_dismiss && post.globalreports.length > 0) 9 | || (req.body.dismiss && post.reports.length > 0); 10 | }); 11 | 12 | if (filteredposts.length === 0) { 13 | return { 14 | message: __('No reports to dismiss'), 15 | }; 16 | } 17 | 18 | const ret = { 19 | message: __('Dismissed reports'), 20 | action: '$set', 21 | query: {} 22 | }; 23 | ret.query[`${req.body.global_dismiss ? 'global' : ''}reports`] = []; 24 | 25 | return ret; 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /models/pages/randombanner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Boards = require(__dirname+'/../../db/boards.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | if (!req.query.board || typeof req.query.board !== 'string') { 8 | return next(); 9 | } 10 | 11 | let banner; 12 | try { 13 | banner = await Boards.randomBanner(req.query.board); 14 | } catch (err) { 15 | return next(err); 16 | } 17 | 18 | if (!banner) { 19 | //non existing boards will show default banner, but it doesnt really matter. 20 | return res.redirect('/file/defaultbanner.png'); 21 | } 22 | 23 | return res.redirect(`/banner/${req.query.board}/${banner}`); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /views/mixins/filters.pug: -------------------------------------------------------------------------------- 1 | mixin filters(filterArr) 2 | - const filterTypeMap = { single: __('Single'), fid: __('ID'), fname: __('Name'), ftrip: __('Tripcode'), fnamer: __('Name'), ftripr: __('Tripcode'), fsub: __('Subject'), fsubr: __('Subject'), fmsg: __('Message'), fmsgr: __('Message'), fflag: __('Flag'), fflagr: __('Flag') } 3 | if filterArr.length > 0 4 | each filter in filterArr 5 | tr 6 | td #{filterTypeMap[filter.type]} 7 | td #{filter.val.toString()} 8 | td: input(disabled type='checkbox' checked=filter.type.endsWith('r')) 9 | td: a.right.close(data-type=filter.type data-data=filter.val) × 10 | else 11 | td(colspan=4) #{__('No Filters')} 12 | -------------------------------------------------------------------------------- /lib/misc/dotwofactor.js: -------------------------------------------------------------------------------- 1 | const OTPAuth = require('otpauth') 2 | , redis = require(__dirname+'/../redis/redis.js'); 3 | 4 | module.exports = async (username, totpSecret, userInput) => { 5 | 6 | const totp = new OTPAuth.TOTP({ 7 | secret: totpSecret, 8 | algorithm: 'SHA256', 9 | }); 10 | 11 | let delta = totp.validate({ 12 | token: userInput, 13 | algorithm: 'SHA256', 14 | window: 1, 15 | }); 16 | 17 | if (delta !== null) { 18 | const key = `twofactor_success:${username}:${userInput}`; 19 | const uses = await redis.incr(key); 20 | redis.expire(key, 30); 21 | if (uses && uses > 1) { 22 | return null; 23 | } 24 | } 25 | 26 | return delta; 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /views/pages/banners.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/boardheader.pug 3 | include ../mixins/boardnav.pug 4 | 5 | block head 6 | title /#{board._id}/ - #{__('Banners')} 7 | 8 | block content 9 | +boardheader(__('Banners')) 10 | br 11 | include ../includes/stickynav.pug 12 | .pages 13 | +boardnav('banners', true, false) 14 | hr(size=1) 15 | if board.banners.length > 0 16 | .catalog 17 | each banner in board.banners 18 | img.board-banner(src=`/banner/${board._id}/${banner}` loading='lazy') 19 | else 20 | p #{__('Board has no custom banners.')} 21 | include ../includes/stickynav.pug 22 | hr(size=1) 23 | .pages 24 | +boardnav('banners', true, false) 25 | -------------------------------------------------------------------------------- /configs/nginx/README.md: -------------------------------------------------------------------------------- 1 | `sites-available.example` is a sample /etc/nginx/sites-available/example.com file 2 | `snippets/*` are sample snippets for /etc/nginx/snippets/ 3 | 4 | For standard installs, run nginx.sh for easy configuration with prompts. 5 | 6 | For non-standard installations, DIY. 7 | 8 | If you use cloudflare, please read [these](https://support.cloudflare.com/hc/en-us/articles/200170786-Restoring-original-visitor-IPs-Logging-visitor-IP-addresses-with-mod-cloudflare-) [articles](https://support.cloudflare.com/hc/en-us/articles/200168236-Configuring-Cloudflare-IP-Geolocation) to setup proper IP forwarding and geolocation headers. Similar steps would apply to other CDNs/reverse proxies. 9 | -------------------------------------------------------------------------------- /migrations/0.0.10.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('update moglog postids to postlinks'); 5 | await db.collection('modlog').updateMany({}, 6 | [{ 7 | $addFields: { 8 | postLinks: [ 9 | { 10 | $arrayToObject: { 11 | $map: { 12 | input: '$postIds', 13 | as: 'postId', 14 | in: { 15 | k: 'postId', 16 | v: '$$postId' 17 | } 18 | } 19 | } 20 | } 21 | ] 22 | } 23 | } 24 | ]); 25 | await db.collection('modlog').updateMany({}, { 26 | '$unset': { 27 | 'postIds': '' 28 | }, 29 | '$set': { 30 | 'showLinks': false 31 | } 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /models/pages/globalmanage/roles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Roles } = require(__dirname+'/../../../db/') 4 | , roleManager = require(__dirname+'/../../../lib/permission/rolemanager.js'); 5 | 6 | module.exports = async (req, res) => { 7 | 8 | const allRoles = await Roles.find(); 9 | 10 | res.set('Cache-Control', 'private, max-age=5'); 11 | 12 | if (req.path.endsWith('.json')) { 13 | res.json(allRoles); 14 | } else { 15 | res.render('globalmanageroles', { 16 | csrf: req.csrfToken(), 17 | permissions: res.locals.permissions, 18 | allRoles, 19 | roleNameMap: roleManager.roleNameMap, 20 | rolePermissionMap: roleManager.rolePermissionMap, 21 | }); 22 | } 23 | 24 | }; 25 | -------------------------------------------------------------------------------- /views/pages/managemypermissions.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/managenav.pug 3 | include ../mixins/boardheader.pug 4 | include ../mixins/mypermissions.pug 5 | 6 | block head 7 | title /#{board._id}/ - #{__('My Permissions')} 8 | 9 | block content 10 | +boardheader(__('My Permissions')) 11 | br 12 | +managenav('staff') 13 | hr(size=1) 14 | h4.mv-5 #{__('Board-specific permissions')} 15 | | 16 | if permissions.get(Permissions.MANAGE_BOARD_STAFF) && user.staffBoards.includes(board._id) 17 | | 18 | a(href=`/${board._id}/manage/editstaff/${user.username}.html`) [#{__('Edit')}] 19 | - const jsonPermissions = permissions.toJSON(); 20 | +mypermissions(jsonPermissions, true) 21 | -------------------------------------------------------------------------------- /views/includes/addbanform.pug: -------------------------------------------------------------------------------- 1 | .row 2 | .label #{__('IP/Hash')} 3 | input(type='text' name='ip' required) 4 | .row 5 | .label #{__('Ban Reason')} 6 | input(type='text' name='ban_reason') 7 | .row 8 | .label #{__('Modlog Message')} 9 | input(type='text' name='log_message') 10 | .row 11 | .label #{__('Ban Duration')} 12 | input(type='text' name='ban_duration' placeholder='e.g. 7d') 13 | .row 14 | .label #{__('Non-appealable Ban')} 15 | label.postform-style.ph-5 16 | input(type='checkbox', name='no_appeal' value='1') 17 | .row 18 | .label #{__('Hide Username In Modlog')} 19 | label.postform-style.ph-5 20 | input(type='checkbox', name='hide_name' value='1') 21 | input(type='submit', value=__('Submit')) 22 | 23 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process 4 | .on('uncaughtException', console.error) 5 | .on('unhandledRejection', console.error); 6 | 7 | const Mongo = require(__dirname+'/db/db.js') 8 | , config = require(__dirname+'/lib/misc/config.js'); 9 | 10 | (async () => { 11 | 12 | await Mongo.connect(); 13 | await Mongo.checkVersion(); 14 | await config.load(); 15 | 16 | const tasks = require(__dirname+'/lib/build/tasks.js') 17 | , { queue } = require(__dirname+'/lib/build/queue.js'); 18 | 19 | queue 20 | .on('error', console.error) 21 | .on('failed', console.warn); 22 | 23 | queue.process(async job => { 24 | await tasks[job.data.task](job.data.options); 25 | return null; 26 | }); 27 | 28 | })(); 29 | 30 | -------------------------------------------------------------------------------- /db/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | Posts: require(__dirname+'/posts.js'), 6 | Boards: require(__dirname+'/boards.js'), 7 | Stats: require(__dirname+'/stats.js'), 8 | Accounts: require(__dirname+'/accounts.js'), 9 | Roles: require(__dirname+'/roles.js'), 10 | Bans: require(__dirname+'/bans.js'), 11 | Captchas: require(__dirname+'/captchas.js'), 12 | Files: require(__dirname+'/files.js'), 13 | News: require(__dirname+'/news.js'), 14 | Filters: require(__dirname+'/filters.js'), 15 | CustomPages: require(__dirname+'/custompages.js'), 16 | Ratelimits: require(__dirname+'/ratelimits.js'), 17 | Modlogs: require(__dirname+'/modlogs.js'), 18 | Bypass: require(__dirname+'/bypass.js'), 19 | 20 | }; 21 | -------------------------------------------------------------------------------- /lib/converter/formatsize.test.js: -------------------------------------------------------------------------------- 1 | const formatSize = require('./formatsize.js'); 2 | 3 | describe('formatSize() - convert bytes to human readable file size', () => { 4 | const cases = [ 5 | {in: 1024, out: '1KB'}, 6 | {in: Math.pow(1024, 2), out: '1MB'}, 7 | {in: Math.pow(1024, 3), out: '1GB'}, 8 | {in: Math.pow(1024, 4), out: '1TB'}, 9 | {in: Math.pow(1024, 5), out: '1024TB'}, 10 | {in: Math.pow(1024, 3)+(Math.pow(1024, 2)*512), out: '1.5GB'}, 11 | {in: 100, out: '100B'}, 12 | {in: 0, out: '0B'}, 13 | ]; 14 | for (let i in cases) { 15 | test(`should output ${cases[i].out} for an input of ${cases[i].in} bytes`, () => { 16 | expect(formatSize(cases[i].in)).toBe(cases[i].out); 17 | }); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /lib/input/setting.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | 5 | trimSetting: (setting, oldSetting) => { 6 | return typeof setting === 'string' ? setting.trim() : oldSetting; 7 | }, 8 | 9 | numberSetting: (setting, oldSetting) => { 10 | return typeof setting === 'number' && setting !== oldSetting ? setting : oldSetting; 11 | }, 12 | 13 | booleanSetting: (setting) => { 14 | return setting != null; 15 | }, 16 | 17 | arraySetting: (setting, oldSetting, limit=false) => { 18 | if (typeof setting === 'string') { 19 | const split = setting 20 | .split(/\r?\n/) 21 | .filter(n => n); 22 | return split 23 | .slice(0, limit || split.length); 24 | } 25 | return oldSetting; 26 | }, 27 | 28 | }; 29 | -------------------------------------------------------------------------------- /models/forms/stickyposts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { NumberInt } = require(__dirname+'/../../db/db.js'); 4 | 5 | module.exports = (locals, sticky) => { 6 | 7 | const { posts, __, __n } = locals; 8 | 9 | const filteredposts = posts.filter(post => { 10 | return !post.thread; 11 | }); 12 | 13 | if (filteredposts.length === 0) { 14 | return { 15 | message: __('No threads selected to Sticky'), 16 | }; 17 | } 18 | 19 | const stickyValue = NumberInt(sticky); 20 | 21 | return { 22 | message: __n('Set Sticky level for %s threads to %s', 'Set Sticky level for %s threads to %s', filteredposts.length, sticky), 23 | action: '$set', 24 | query: { 25 | 'sticky': stickyValue, 26 | } 27 | }; 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /views/includes/registration.pug: -------------------------------------------------------------------------------- 1 | .form-wrapper.flex-center 2 | form.form-post(action='/forms/register' method='POST' data-captcha-preload='true') 3 | .row 4 | .label #{__('Username')} 5 | input(type='text', name='username', maxlength='50' pattern='[a-zA-Z0-9]+' required title=__('alphanumeric only')) 6 | .row 7 | .label #{__('Password')} 8 | input(type='password', name='password', maxlength='100' required) 9 | .row 10 | .label #{__('Confirm Password')} 11 | input(type='password', name='passwordconfirm', maxlength='100' required) 12 | if captchaOptions.type === 'text' 13 | include ./captchasidelabel.pug 14 | else 15 | include ./captchafieldrow.pug 16 | input(type='submit', value=__('Register')) 17 | -------------------------------------------------------------------------------- /views/mixins/announcements.pug: -------------------------------------------------------------------------------- 1 | mixin announcements(hrTop=true, hrMiddle=true, hrBottom=true) 2 | - const hasGlobalAnnouncement = globalAnnouncement.markdown; 3 | - const hasBoardAnnouncement = board && board.settings.announcement.markdown; 4 | if hrTop && (hasGlobalAnnouncement || hasBoardAnnouncement) 5 | hr(size=1) 6 | if hasGlobalAnnouncement 7 | pre.post-message.no-m-p.text-center !{globalAnnouncement.markdown} 8 | if hrMiddle && (hasGlobalAnnouncement && hasBoardAnnouncement) 9 | hr(size=1) 10 | if board && board.settings.announcement.markdown 11 | pre.post-message.no-m-p.text-center !{board.settings.announcement.markdown} 12 | if hrBottom && (hasGlobalAnnouncement || hasBoardAnnouncement) 13 | hr(size=1) 14 | -------------------------------------------------------------------------------- /lib/misc/themes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { readdirSync } = require('fs-extra') 4 | , config = require(__dirname+'/config.js') 5 | , { addCallback } = require(__dirname+'/../redis/redis.js') 6 | , updateThemes = () => { 7 | const { themes, codeThemes } = config.get; 8 | module.exports.themes = themes.length > 0 ? themes : readdirSync(__dirname+'/../../gulp/res/css/themes/').filter(x => x.endsWith('.css')).map(x => x.substring(0,x.length-4)); 9 | module.exports.codeThemes = codeThemes.length > 0 ? codeThemes : readdirSync(__dirname+'/../../node_modules/highlight.js/styles/').filter(x => x.endsWith('.css')).map(x => x.substring(0,x.length-4)); 10 | }; 11 | 12 | updateThemes(); 13 | addCallback('config', updateThemes); 14 | -------------------------------------------------------------------------------- /views/mixins/mypermissions.pug: -------------------------------------------------------------------------------- 1 | mixin mypermissions(jsonPermissions, boardOnly=false) 2 | - const permissionKeys = boardOnly ? Object.keys(jsonPermissions).filter(p => manageBoardBits.includes(parseInt(p))) : Object.keys(jsonPermissions) 3 | for bit, index in permissionKeys 4 | if index > 0 && jsonPermissions[bit].title 5 | hr(size=1) 6 | h4.mv-5 #{__(jsonPermissions[bit].title)} 7 | if jsonPermissions[bit].subtitle 8 | p #{__(jsonPermissions[bit].subtitle)} 9 | .row 10 | label.postform-style.ph-5.notallowed 11 | input(type='checkbox' name=`permission_bit_${bit}` value=bit checked=jsonPermissions[bit].state disabled=true) 12 | .rlabel #{__(jsonPermissions[bit].label)} 13 | p #{__(jsonPermissions[bit].desc)} 14 | -------------------------------------------------------------------------------- /migrations/0.0.16.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Allow tph/pph separate triggers and resets'); 5 | await db.collection('boards').updateMany({}, { 6 | '$rename': { 7 | 'settings.triggerAction' : 'settings.pphTriggerAction', 8 | } 9 | }); 10 | await db.collection('boards').updateMany({}, { 11 | '$unset': { 12 | 'settings.resetTrigger' : '', 13 | 'preTriggerMode': '', 14 | } 15 | }); 16 | await db.collection('boards').updateMany({}, { 17 | '$set': { 18 | 'settings.tphTriggerAction' : 0, 19 | 'settings.captchaReset' : 0, 20 | 'settings.lockReset' : 0, 21 | } 22 | }); 23 | console.log('Cleared boards cache'); 24 | await redis.deletePattern('board:*'); 25 | }; 26 | -------------------------------------------------------------------------------- /migrations/0.1.8.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding OP delete protection options to board settings'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'boardDefaults.deleteProtectionAge': 0, 8 | 'boardDefaults.deleteProtectionCount': 0, 9 | }, 10 | }); 11 | console.log('Clearing globalsettings cache'); 12 | await redis.deletePattern('globalsettings'); 13 | await db.collection('boards').updateMany({}, { 14 | '$set': { 15 | 'settings.deleteProtectionAge': 0, 16 | 'settings.deleteProtectionCount': 0, 17 | } 18 | }); 19 | console.log('Clearing boards cache'); 20 | await redis.deletePattern('board:*'); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /migrations/1.3.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | 5 | console.log('Updating globalsettings to add web3 settings'); 6 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 7 | '$set': { 8 | 'enableWeb3': false, 9 | 'ethereumLinksURL': 'https://etherscan.io/address/%s', 10 | }, 11 | }); 12 | 13 | console.log('Updating boards to add web3 settings'); 14 | await db.collection('boards').updateMany({}, { 15 | '$set': { 16 | 'enableWeb3': false, 17 | }, 18 | }); 19 | 20 | console.log('Clearing globalsettings cache'); 21 | await redis.deletePattern('globalsettings'); 22 | console.log('Clearing boards cache'); 23 | await redis.deletePattern('board:*'); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /gulp/res/js/i18n.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | /* globals TRANSLATIONS */ 3 | 4 | const pluralMap = { 5 | 1: 'one', 6 | // two, three, few, many, ... 7 | }; 8 | 9 | //simple translation 10 | const __ = (key, replacement=null) => { 11 | const translation = TRANSLATIONS[key] || key; 12 | return replacement !== null ? translation.replace('%s', replacement) : translation; 13 | }; 14 | 15 | //pluralisation 16 | const __n = (key, count) => { 17 | const pluralKey = pluralMap[count] || 'other'; 18 | const translationObj = TRANSLATIONS[key]; 19 | if (!translationObj) { 20 | return key; 21 | } 22 | const translationPlural = translationObj[pluralKey] || translationObj['other']; 23 | return translationPlural.replace('%s', count); 24 | }; 25 | -------------------------------------------------------------------------------- /configs/nginx/snippets/security_headers_nocache.conf: -------------------------------------------------------------------------------- 1 | add_header Content-Security-Policy "default-src 'self'; media-src 'self' blob:; img-src 'self' blob:; object-src 'self' blob:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-src 'self' https://www.youtube-nocookie.com/embed/ https://www.bitchute.com/embed/ https://odysee.com/%24/embed/; connect-src 'self' wss://example.com/ wss://www.example.com/ wss://www.example.onion/ wss://example.onion/ wss://www.example.loki/ wss://example.loki/; font-src 'self'" always; 2 | add_header Referrer-Policy "same-origin, strict-origin-when-cross-origin" always; 3 | add_header X-Frame-Options "sameorigin" always; 4 | add_header X-Content-Type-Options "nosniff" always; 5 | add_header X-XSS-Protection "1; mode=block" always; 6 | -------------------------------------------------------------------------------- /db/captchas.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , db = Mongo.db.collection('captcha'); 5 | 6 | module.exports = { 7 | 8 | db, 9 | 10 | findOne: (id) => { 11 | return db.findOne({ '_id': id }); 12 | }, 13 | 14 | insertOne: (answer) => { 15 | return db.insertOne({ 16 | 'answer': answer, 17 | 'expireAt': new Date() 18 | }); 19 | }, 20 | 21 | findOneAndDelete: (id, answer) => { 22 | return db.findOneAndDelete({ 23 | '_id': id, 24 | 'answer': answer 25 | }); 26 | }, 27 | 28 | randomSample: () => { 29 | return db.aggregate([ 30 | { 31 | $sample: { size: 1 } 32 | } 33 | ]).toArray(); 34 | }, 35 | 36 | deleteAll: () => { 37 | return db.deleteMany({}); 38 | }, 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /views/includes/banform.pug: -------------------------------------------------------------------------------- 1 | form.form-post(action=`/forms/appeal`, enctype='application/x-www-form-urlencoded', method='POST' data-captcha-preload='true') 2 | include ./bantable.pug 3 | for ban in bans 4 | +ban(ban, true) 5 | - const allowAppeal = bans.filter(ban => ban.allowAppeal === true && !ban.appeal).length > 0; 6 | if allowAppeal === true 7 | h4.no-m-p #{__('Appeal')}: 8 | .form-wrapper.flexleft.mt-10 9 | input(type='hidden' name='_csrf' value=csrf) 10 | .row 11 | .label #{__('Message')} 12 | textarea(rows='5' name='message' required) 13 | if captchaOptions.type === 'text' 14 | include ./captchasidelabel.pug 15 | else 16 | include ./captchafieldrow.pug 17 | input(type='submit', value=__('Submit')) 18 | -------------------------------------------------------------------------------- /gulp/res/css/nscaptcha.css: -------------------------------------------------------------------------------- 1 | img { 2 | width: var(--captcha-w); 3 | height: var(--captcha-h); 4 | image-rendering: crisp-edges; 5 | margin: 0 auto; 6 | } 7 | input { 8 | position: fixed; 9 | left: -3px; 10 | bottom: 0; 11 | opacity: 0.9; 12 | border: none; 13 | background: none; 14 | font-size: 18px; 15 | cursor: pointer; 16 | text-shadow: rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px, rgb(0, 0, 0) 0px 0px 1px; 17 | color: white; 18 | } 19 | body { 20 | font-family: arial, helvetica, sans-serif; 21 | font-size: 10pt; 22 | margin: 0; 23 | padding: 0; 24 | } 25 | .flexcenter { 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /lib/file/audio/audiothumbnail.js: -------------------------------------------------------------------------------- 1 | const ffmpeg = require('fluent-ffmpeg') 2 | , config = require(__dirname+'/../../misc/config.js') 3 | , uploadDirectory = require(__dirname+'/../uploaddirectory.js'); 4 | 5 | module.exports = (file) => { 6 | 7 | const { thumbSize } = config.get; 8 | return new Promise((resolve, reject) => { 9 | ffmpeg(`${uploadDirectory}/file/${file.filename}`) 10 | .on('end', () => { 11 | return resolve(); 12 | }) 13 | .on('error', function(err) { 14 | return reject(err); 15 | }) 16 | .complexFilter([{ 17 | filter: 'showwavespic', 18 | options: { split_channels: 1, s: `${thumbSize}x${thumbSize}` } 19 | }]) 20 | .save(`${uploadDirectory}/file/thumb/${file.hash}${file.thumbextension}`); 21 | }); 22 | 23 | }; 24 | -------------------------------------------------------------------------------- /models/forms/addstaff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Boards, Accounts } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') 5 | , roleManager = require(__dirname+'/../../lib/permission/rolemanager.js'); 6 | 7 | module.exports = async (req, res) => { 8 | 9 | const { __ } = res.locals; 10 | 11 | await Promise.all([ 12 | Accounts.addStaffBoard([req.body.username], res.locals.board._id), 13 | Boards.addStaff(res.locals.board._id, req.body.username, roleManager.roles.BOARD_STAFF_DEFAULTS) 14 | ]); 15 | 16 | return dynamicResponse(req, res, 200, 'message', { 17 | 'title': __('Success'), 18 | 'message': __('Added staff'), 19 | 'redirect': `/${req.params.board}/manage/staff.html`, 20 | }); 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /migrations/0.0.21.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('migrate old config to db'); 5 | const oldSettings = require(__dirname+'/../configs/main.js'); 6 | const secrets = require(__dirname+'/../configs/secrets.js'); 7 | //delete anythign thats in the secrets 8 | Object.keys(secrets).forEach(key => { 9 | delete oldSettings[key]; 10 | }); 11 | //and a few more that arent in the root 12 | delete oldSettings.captchaOptions.google; 13 | delete oldSettings.captchaOptions.hcaptcha; 14 | const templateSettings = require(__dirname+'/../configs/template.js.example'); 15 | const newSettings = { ...templateSettings, ...oldSettings }; 16 | //set default settings into redis instead 17 | redis.set('globalsettings', newSettings); 18 | }; 19 | -------------------------------------------------------------------------------- /views/pages/managereports.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/ban.pug 4 | include ../mixins/managenav.pug 5 | include ../mixins/boardheader.pug 6 | 7 | block head 8 | title /#{board._id}/ - #{__('Reports')} 9 | 10 | block content 11 | +boardheader(__('Reports')) 12 | br 13 | +managenav('reports') 14 | hr(size=1) 15 | h4.no-m-p #{__('Reports')}: 16 | form(action=`/forms/board/${board._id}/modactions` method='POST' enctype='application/x-www-form-urlencoded') 17 | if reports.length === 0 18 | p #{__('No reports.')} 19 | else 20 | input(type='hidden' name='_csrf' value=csrf) 21 | for report in reports 22 | .thread 23 | +post(report, false, true) 24 | hr(size=1) 25 | include ../includes/actionfooter_manage.pug 26 | -------------------------------------------------------------------------------- /lib/file/deleteold.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { stat, remove, readdir } = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/uploaddirectory.js'); 5 | 6 | //takes directory name and timestamp to delete files older than 7 | module.exports = async (directory, olderThan) => { 8 | const dir = `${uploadDirectory}/${directory}`; 9 | const files = await readdir(dir); 10 | if (files.length > 0) { 11 | return Promise.all(files.map(async file => { 12 | const filePath = `${dir}/${file}`; 13 | try { 14 | const stats = await stat(filePath); 15 | const expiry = new Date(olderThan).getTime(); 16 | if (stats.ctime.getTime() < expiry) { 17 | await remove(filePath); 18 | } 19 | } catch (e) { 20 | console.error(e); 21 | } 22 | })); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /migrations/0.1.3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db) => { 4 | console.log('add unix time to posts for (u) links'); 5 | await db.collection('posts').aggregate([ 6 | { 7 | $project: { 8 | _id: '$_id', 9 | u: { 10 | $toDouble: '$date' 11 | } 12 | } 13 | }, { 14 | $merge: { 15 | into: 'posts' 16 | } 17 | } 18 | ]).toArray(); 19 | console.log('=\nNOTICE: 0.1.3 has updated nginx config, now using snippets for a more modular config that is easier to maintain. It is recommended to update these, refer to step 6 of INSTALLATION.\n='); 20 | console.log('=\nNOTICE: 0.1.3 now makes custom favicon generate easily and properly. Place your master image file in gulp/res/icons/master.png, then run "gulp generate-favicon && gulp icons"\n='); 21 | }; 22 | -------------------------------------------------------------------------------- /migrations/0.5.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding tegaki sizes to globalsettings, and toggle option to board settings'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'frontendScriptDefault.tegakiWidth': 500, 8 | 'frontendScriptDefault.tegakiHeight': 500, 9 | 'boardDefaults.enableTegaki': true, 10 | }, 11 | }); 12 | await db.collection('boards').updateMany({ 13 | 'webring': false, 14 | }, { 15 | '$set':{ 16 | 'settings.enableTegaki': true, 17 | }, 18 | }); 19 | console.log('Clearing globalsettings cache'); 20 | await redis.deletePattern('globalsettings'); 21 | console.log('Clearing user and board cache'); 22 | await redis.deletePattern('board:*'); 23 | }; 24 | -------------------------------------------------------------------------------- /lib/middleware/file/imagehash.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const imageHash = require('imghash').hash 4 | , config = require(__dirname+'/../../misc/config.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | const { hashImages } = config.get; 8 | if (hashImages && res.locals.numFiles > 0) { 9 | const hashPromises = []; 10 | for (let i = 0; i < res.locals.numFiles; i++) { 11 | const mainType = req.files.file[i].mimetype.split('/')[0]; 12 | if (mainType === 'image') { 13 | hashPromises.push(imageHash(req.files.file[i].tempFilePath, 8, 'hex').then(res => { 14 | req.files.file[i].phash = res; 15 | })); 16 | } 17 | } 18 | await Promise.all(hashPromises) 19 | .catch(() => { /* noop on fail, wont hurt. phashes just wont be set */ }); 20 | } 21 | next(); 22 | }; 23 | -------------------------------------------------------------------------------- /migrations/1.7.3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding hoverExpandsMedia and follow cursor to frontendScriptDefaults globalsetting'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'frontendScriptDefault.hoverExpandsMedia': false, 8 | 'frontendScriptDefault.hoverExpandFollowCursor': false, 9 | 'boardDefaults.autoBumplockTime': 0, 10 | }, 11 | }); 12 | await db.collection('boards').updateMany({}, { 13 | '$set': { 14 | 'settings.autoBumplockTime': 0, 15 | }, 16 | }); 17 | 18 | console.log('Clearing globalsettings cache'); 19 | await redis.deletePattern('globalsettings'); 20 | console.log('Clearing boards cache'); 21 | await redis.deletePattern('board:*'); 22 | }; 23 | 24 | -------------------------------------------------------------------------------- /tools/clean_locales.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | , locales = ['en-GB', 'pt-PT', 'pt-BR', 'ru-RU', 'it-IT'] 3 | , enGBData = require(__dirname+'/../locales/en-GB.json'); 4 | 5 | locales.forEach(locale => { 6 | const localeData = require(__dirname+`/../locales/${locale}.json`); 7 | // Take any missing values from en-GB 8 | for (const key in enGBData) { 9 | if (!localeData[key]) { 10 | localeData[key] = enGBData[key]; 11 | } 12 | } 13 | // Sort and write updated data 14 | const sortedEntries = Object.entries(localeData) 15 | .sort((a, b) => a[0].localeCompare(b[0])) 16 | .reduce((acc, [key, value]) => { 17 | acc[key] = value; 18 | return acc; 19 | }, {}); 20 | fs.writeFileSync(__dirname+`/../locales/${locale}.json`, JSON.stringify(sortedEntries, null, '\t')); 21 | }); 22 | -------------------------------------------------------------------------------- /views/mixins/overboardform.pug: -------------------------------------------------------------------------------- 1 | mixin overboardform(overboardFormAction) 2 | .flexcenter.mv-10 3 | details 4 | summary.collapse #{__('Customise')} 5 | form.mt-5.form-post#overboardform(action=overboardFormAction method='GET') 6 | .col 7 | .row 8 | .label #{__('Include Default Boards')} 9 | label.postform-style.ph-5 10 | input(type='checkbox', name='include_default', value='true' checked=includeDefault) 11 | .row 12 | .label #{__('Add Boards')} 13 | input(type='text' name='add' value=addBoards.join(',') placeholder=__('comma separated')) 14 | .row 15 | .label #{__('Remove Boards')} 16 | input(type='text' name='rem' value=removeBoards.join(',') placeholder=__('comma separated')) 17 | input(type='submit', value=__('Filter')) 18 | -------------------------------------------------------------------------------- /lib/post/checkfilters.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = (filters, combinedString, strictCombinedString) => { 4 | 5 | let filterHits = []; 6 | for (const filter of filters) { 7 | if (filter.filterMode === 0) { continue; } //skip "Do nothing" mode filters 8 | const string = filter.strictFiltering ? strictCombinedString : combinedString; 9 | const hitFilter = filter.filters.find(match => string.includes(match.toLowerCase()) ); 10 | if (hitFilter) { 11 | //if either of these are hit, we can stop checking 12 | if (filter.filterMode === 1 || filter.filterMode === 2) { 13 | return [{ h: hitFilter, f: filter }]; 14 | } else { 15 | filterHits.push({ h: hitFilter, f: filter }); 16 | } 17 | } 18 | } 19 | 20 | return filterHits.length ? filterHits : false; 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /db/ratelimits.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , db = Mongo.db.collection('ratelimit'); 5 | 6 | module.exports = { 7 | 8 | db, 9 | 10 | resetQuota: (identifier, action) => { 11 | return db.deleteOne({ '_id': `${identifier}-${action}` }); 12 | }, 13 | 14 | incrmentQuota: (identifier, action, amount) => { 15 | return db.findOneAndUpdate( 16 | { 17 | '_id': `${identifier}-${action}` 18 | }, 19 | { 20 | '$inc': { 21 | 'sequence_value': amount 22 | }, 23 | '$setOnInsert': { 24 | 'expireAt': new Date() 25 | } 26 | }, 27 | { 28 | 'upsert': true 29 | } 30 | ).then(r => { return r.value ? r.value.sequence_value : 0; }); 31 | }, 32 | 33 | deleteAll: () => { 34 | return db.deleteMany({}); 35 | }, 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | backup.sh 3 | 4 | #secrets 5 | configs/*.json 6 | configs/*.js 7 | 8 | # ignore jschan tmp and static 9 | tmp/ 10 | static/* 11 | docker/static/* 12 | 13 | # ignore some gulp resources 14 | /gulp/res/css/codethemes 15 | gulp/res/css/locals.css 16 | gulp/res/css/custom.css 17 | gulp/res/icons/* 18 | 19 | # no compiledclients js files generated from .pug in gulpfile 20 | gulp/res/js/locals.js 21 | gulp/res/js/post.js 22 | gulp/res/js/modal.js 23 | gulp/res/js/captchaformsection.js 24 | gulp/res/js/pugfilters.js 25 | gulp/res/js/threadwatcher.js 26 | gulp/res/js/watchedthread.js 27 | gulp/res/js/pugruntime.js 28 | gulp/res/js/uploaditem.js 29 | gulp/res/js/socket.io.js 30 | gulp/res/js/web3.js 31 | 32 | # some ide and testing files, artefacts, etc 33 | .idea/ 34 | coverage/ 35 | junit.xml 36 | -------------------------------------------------------------------------------- /models/pages/manage/reports.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js') 4 | , { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | let reports; 9 | try { 10 | reports = await Posts.getReports(req.params.board, res.locals.permissions); 11 | } catch (err) { 12 | return next(err); 13 | } 14 | 15 | res.set('Cache-Control', 'private, max-age=5'); 16 | 17 | if (req.path.endsWith('/reports.json')) { 18 | res.json({ 19 | reports, 20 | }); 21 | } else { 22 | res.render('managereports', { 23 | csrf: req.csrfToken(), 24 | reports, 25 | permissions: res.locals.permissions, 26 | viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP), 27 | }); 28 | } 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /models/pages/manage/bans.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Bans = require(__dirname+'/../../../db/bans.js') 4 | , { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | let bans; 9 | try { 10 | const showGlobal = res.locals.permissions.get(Permissions.VIEW_BOARD_GLOBAL_BANS); 11 | const bansBoard = showGlobal ? req.params.board : { '$eq': req.params.board }; 12 | bans = await Bans.getBoardBans(bansBoard); 13 | } catch (err) { 14 | return next(err); 15 | } 16 | 17 | res 18 | .set('Cache-Control', 'private, max-age=5') 19 | .render('managebans', { 20 | csrf: req.csrfToken(), 21 | permissions: res.locals.permissions, 22 | viewRawIp: res.locals.permissions.get(Permissions.VIEW_RAW_IP), 23 | bans, 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /models/pages/manage/editstaff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Permission = require(__dirname+'/../../../lib/permission/permission.js') 4 | , { Permissions } = require(__dirname+'/../../../lib/permission/permissions.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | let staffData = res.locals.board.staff[req.params.staffusername]; 9 | 10 | if (staffData == null) { 11 | //staff does not exist 12 | return next(); 13 | } 14 | 15 | res 16 | // .set('Cache-Control', 'private, max-age=5') 17 | .render('editstaff', { 18 | csrf: req.csrfToken(), 19 | board: res.locals.board, 20 | permissions: res.locals.permissions, 21 | staffUsername: req.params.staffusername, 22 | staffPermissions: new Permission(staffData.permissions), 23 | manageBoardBits: Permissions._MANAGE_BOARD_BITS, 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /models/pages/globalmanage/editrole.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Roles } = require(__dirname+'/../../../db/') 4 | , roleManager = require(__dirname+'/../../../lib/permission/rolemanager.js') 5 | , Permission = require(__dirname+'/../../../lib/permission/permission.js'); 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | const role = await Roles.findOne(req.params.roleid); 10 | 11 | if (role == null) { 12 | //role does not exist 13 | return next(); 14 | } 15 | 16 | res 17 | .set('Cache-Control', 'private, max-age=5') 18 | .render('editrole', { 19 | csrf: req.csrfToken(), 20 | role, 21 | rolePermissions: new Permission(role.permissions), 22 | roleNameMap: roleManager.roleNameMap, 23 | rolePermissionMap: roleManager.rolePermissionMap, 24 | permissions: res.locals.permissions, 25 | }); 26 | 27 | }; 28 | -------------------------------------------------------------------------------- /lib/input/modlogactions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = Object.seal(Object.freeze(Object.preventExtensions({ 4 | 5 | BAN: 'Ban', 6 | GLOBAL_BAN: 'Global ban', 7 | 8 | BAN_REPORTER: 'Ban reporter', 9 | GLOBAL_BAN_REPORTER: 'Global ban reporter', 10 | 11 | DISMISS: 'Dismiss reports', 12 | GLOBAL_DISMISS: 'Dismiss global reports', 13 | 14 | DELETE: 'Delete', 15 | DELETE_BY_IP: 'Delete by IP', 16 | GLOBAL_DELETE_BY_IP: 'Global delete by IP', 17 | 18 | UNLINK_FILES: 'Unlink files', 19 | DELETE_FILES: 'Delete files', 20 | SPOILER_FILES: 'Spoiler files', 21 | 22 | EDIT: 'Edit', 23 | MOVE: 'Move', 24 | 25 | BUMPLOCK: 'Bumplock', 26 | LOCK: 'Lock', 27 | STICKY: 'Sticky', 28 | CYCLE: 'Cycle', 29 | 30 | EDIT_BAN: 'Edit Ban', 31 | SETTINGS: 'Settings', 32 | CREATE_BOARD: 'Create Board', 33 | DELETE_BOARD: 'Delete Board', 34 | 35 | }))); 36 | -------------------------------------------------------------------------------- /models/pages/manage/board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../../db/posts.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | const page = req.params.page === 'index' ? 1 : Number(req.params.page); 8 | let maxPage; 9 | let threads; 10 | try { 11 | maxPage = Math.min(Math.ceil((await Posts.getPages(req.params.board)) / 10), Math.ceil(res.locals.board.settings.threadLimit/10)) || 1; 12 | if (page > maxPage) { 13 | return next(); 14 | } 15 | threads = await Posts.getRecent(req.params.board, page, 10, true); 16 | } catch (err) { 17 | return next(err); 18 | } 19 | 20 | res 21 | .set('Cache-Control', 'private, max-age=5') 22 | .render('board', { 23 | modview: true, 24 | page, 25 | maxPage, 26 | threads, 27 | board: res.locals.board, 28 | csrf: req.csrfToken(), 29 | }); 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /views/pages/globalmanagereports.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/globalmanagenav.pug 4 | 5 | block head 6 | title #{__('Reports')} 7 | 8 | block content 9 | h1.board-title #{__('Global Management')} 10 | br 11 | +globalmanagenav('reports') 12 | form(action=`/forms/global/actions` method='POST' enctype='application/x-www-form-urlencoded') 13 | if reports.length === 0 14 | hr(size=1) 15 | p #{__('No reports.')} 16 | else 17 | input(type='hidden' name='_csrf' value=csrf) 18 | hr(size=1) 19 | if ip 20 | h4.no-m-p #{__('Reports against or by ip %s', ip)} 21 | hr(size=1) 22 | for report in reports 23 | .thread 24 | +post(report, false, false, true) 25 | hr(size=1) 26 | .pages.mv-5 27 | include ../includes/pages.pug 28 | include ../includes/actionfooter_globalmanage.pug 29 | -------------------------------------------------------------------------------- /models/pages/manage/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { themes, codeThemes } = require(__dirname+'/../../../lib/misc/themes.js') 4 | , i18n = require(__dirname+'/../../../lib/locale/locale.js') 5 | , config = require(__dirname+'/../../../lib/misc/config.js') 6 | , { getCountryNames, countryCodes } = require(__dirname+'/../../../lib/misc/countries.js'); 7 | 8 | module.exports = async (req, res) => { 9 | 10 | const { forceActionTwofactor } = config.get; 11 | 12 | res 13 | .set('Cache-Control', 'private, max-age=5') 14 | .render('managesettings', { 15 | csrf: req.csrfToken(), 16 | permissions: res.locals.permissions, 17 | countryNamesMap: getCountryNames(res.locals.locale, { select: 'official' }), 18 | countryCodes, 19 | themes, 20 | codeThemes, 21 | forceActionTwofactor, 22 | languages: i18n.getLocales(), 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /models/pages/globalmanage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | globalManageReports: require(__dirname+'/reports.js'), 5 | globalManageBans: require(__dirname+'/bans.js'), 6 | globalManageLogs: require(__dirname+'/logs.js'), 7 | globalManageBoards: require(__dirname+'/boards.js'), 8 | globalManageRecent: require(__dirname+'/recent.js'), 9 | globalManageNews: require(__dirname+'/news.js'), 10 | globalManageFilters: require(__dirname+'/filters.js'), 11 | globalManageAccounts: require(__dirname+'/accounts.js'), 12 | globalManageSettings: require(__dirname+'/settings.js'), 13 | globalManageRoles: require(__dirname+'/roles.js'), 14 | globalEditFilter: require(__dirname+'/editfilter.js'), 15 | editNews: require(__dirname+'/editnews.js'), 16 | editAccount: require(__dirname+'/editaccount.js'), 17 | editRole: require(__dirname+'/editrole.js'), 18 | }; 19 | -------------------------------------------------------------------------------- /migrations/0.9.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('add globalsettings board default option, and tegaki replay mime to allowed mime types'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'boardDefaults.hideBanners': false, 8 | 'overboardReverseLinks': true, 9 | }, 10 | '$push': { 11 | 'otherMimeTypes': 'tegaki/replay', 12 | }, 13 | }); 14 | console.log('add board option to hide banners and [banners] link'); 15 | await db.collection('boards').updateMany({ 16 | 'webring': false, 17 | }, { 18 | '$set':{ 19 | 'settings.hideBanners': false, 20 | }, 21 | }); 22 | console.log('Clearing globalsettings cache'); 23 | await redis.deletePattern('globalsettings'); 24 | console.log('Clearing boards cache'); 25 | await redis.deletePattern('board:*'); 26 | }; 27 | -------------------------------------------------------------------------------- /views/mixins/catalogfile.pug: -------------------------------------------------------------------------------- 1 | mixin catalogfile(postURL, post, file, small=false) 2 | - const type = file.mimetype.split('/')[0] 3 | .post-file-src 4 | a(href=`${postURL}#${post.postId}`) 5 | if post.spoiler || file.spoiler 6 | div.spoilerimg.catalog-thumb(class=(small?'small':'')) 7 | else if file.hasThumb 8 | img.catalog-thumb(class=(small?'small':'') src=`/file/thumb/${file.hash}${file.thumbextension}` width=file.geometry.thumbwidth height=file.geometry.thumbheight loading='lazy') 9 | else if file.attachment 10 | div.attachmentimg.catalog-thumb(class=(small?'small':'') data-mimetype=file.mimetype) 11 | else if type === 'audio' 12 | div.audioimg.catalog-thumb(class=(small?'small':'')) 13 | else 14 | img.catalog-thumb(class=(small?'small':'') src=`/file/${file.filename}` width=file.geometry.width height=file.geometry.height loading='lazy') 15 | -------------------------------------------------------------------------------- /models/forms/reportpost.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { ObjectId } = require(__dirname+'/../../db/db.js'); 4 | 5 | module.exports = (req, res) => { 6 | 7 | const { __n } = res.locals; 8 | 9 | const report = { 10 | 'id': ObjectId(), 11 | 'reason': req.body.report_reason, 12 | 'date': new Date(), 13 | 'ip': { 14 | 'cloak': res.locals.ip.cloak, 15 | 'raw': res.locals.ip.raw, 16 | 'type': res.locals.ip.type, 17 | } 18 | }; 19 | 20 | const ret = { 21 | message: __n('Reported %s posts', res.locals.posts.length), 22 | action: '$push', 23 | query: {} 24 | }; 25 | const query = { 26 | '$each': [report], 27 | '$slice': -5 //limit number of reports 28 | }; 29 | if (req.body.global_report) { 30 | ret.query['globalreports'] = query; 31 | } 32 | if (req.body.report) { 33 | ret.query['reports'] = query; 34 | } 35 | 36 | return ret; 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /lib/misc/fonts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { debugLogs } = require(__dirname+'/../../configs/secrets.js') 4 | , fontList = require('child_process') 5 | .execSync('fc-list -f "%{file}:%{family[0]} %{style[0]}\n"') 6 | .toString() 7 | .split('\n') //split by newlines, like here ^ 8 | .filter(line => line) //filter empty lines 9 | .map(line => { 10 | //map to an object with path and name 11 | const [path, name] = line.split(':'); 12 | return { path, name }; 13 | }) 14 | .sort((a, b) => { 15 | //alphabetical name sort 16 | return a.name.localeCompare(b.name); 17 | }); 18 | 19 | debugLogs && console.log(`${fontList.length} system fonts available`); 20 | 21 | module.exports = { 22 | fontList, 23 | fontPaths: new Set(['default', ...fontList.map(f => f.path)]), //memoize paths 24 | DejaVuSans: fontList.find(f => f.name === 'DejaVu Sans Book'), //default for grid captchas 25 | }; 26 | -------------------------------------------------------------------------------- /views/pages/editstaff.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/boardheader.pug 3 | include ../mixins/managenav.pug 4 | 5 | block head 6 | title /#{board._id}/ - #{__('Edit Staff Permissions')} 7 | 8 | block content 9 | +boardheader(__('Edit Staff Permissions')) 10 | br 11 | +managenav('staff', true) 12 | hr(size=1) 13 | h4.mv-5 #{__('Edit board permissions for "%s"', staffUsername)} 14 | | 15 | if permissions.get(Permissions.MANAGE_GLOBAL_ACCOUNTS) 16 | | 17 | a(href=`/globalmanage/editaccount/${staffUsername}.html`) [#{__('Edit Account Permissions')}] 18 | - const jsonPermissions = staffPermissions.toJSON(); 19 | .form-wrapper.flexleft 20 | form(action=`/forms/board/${board._id}/editstaff` method='POST') 21 | input(type='hidden' name='_csrf' value=csrf) 22 | input(type='hidden' name='username' value=staffUsername) 23 | include ../includes/staffpermissionsform.pug 24 | 25 | -------------------------------------------------------------------------------- /models/forms/globalclear.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Bypass } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') 5 | , redis = require(__dirname+'/../../lib/redis/redis.js'); 6 | 7 | module.exports = async (req, res) => { 8 | 9 | const { __, __n } = res.locals; 10 | 11 | let deletedCount = 0; 12 | switch (req.body.table) { 13 | case 'blockbypass': 14 | deletedCount = await Bypass.deleteAll().then(res => res.deletedCount || 0); 15 | break; 16 | case 'sessions': 17 | deletedCount = await redis.deletePattern('sess:*'); 18 | break; 19 | default: 20 | throw 'invalid table'; //Should never get here 21 | } 22 | 23 | return dynamicResponse(req, res, 200, 'message', { 24 | 'title': __('Success'), 25 | 'message': __n('Deleted %s records.', deletedCount), 26 | 'redirect': '/globalmanage/settings.html' 27 | }); 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /models/pages/board.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Posts = require(__dirname+'/../../db/posts.js') 4 | , { buildBoard } = require(__dirname+'/../../lib/build/tasks.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | const page = req.params.page === 'index' ? 1 : Number(req.params.page); 9 | let html, json; 10 | try { 11 | const maxPage = Math.min(Math.ceil((await Posts.getPages(req.params.board)) / 10), Math.ceil(res.locals.board.settings.threadLimit/10)) || 1; 12 | if (page > maxPage) { 13 | return next(); 14 | } 15 | ({ html, json } = await buildBoard({ 16 | board: res.locals.board, 17 | page, 18 | maxPage 19 | })); 20 | } catch (err) { 21 | return next(err); 22 | } 23 | 24 | if (req.path.endsWith('.json')) { 25 | return res.set('Cache-Control', 'max-age=0').json(json); 26 | } else { 27 | return res.set('Cache-Control', 'max-age=0').send(html); 28 | } 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /migrations/0.1.5.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/../lib/file/uploaddirectory.js'); 5 | 6 | module.exports = async(db, redis) => { 7 | console.log('Adding assets'); 8 | await fs.ensureDir(`${uploadDirectory}/asset/`); 9 | const template = require(__dirname+'/../configs/template.js.example'); 10 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 11 | '$set': { 12 | 'globalLimits.assetFiles': template.globalLimits.assetFiles, 13 | 'globalLimits.assetFilesSize': template.globalLimits.assetFilesSize, 14 | }, 15 | }); 16 | await db.collection('boards').updateMany({}, { 17 | '$set': { 18 | 'assets': [], 19 | }, 20 | }); 21 | console.log('Cleared boards cache'); 22 | await redis.deletePattern('board:*'); 23 | console.log('Cleared globalsettings cache'); 24 | await redis.deletePattern('globalsettings'); 25 | }; 26 | -------------------------------------------------------------------------------- /models/pages/thread.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { buildThread } = require(__dirname+'/../../lib/build/tasks.js'); 4 | 5 | module.exports = async (req, res, next) => { 6 | 7 | let html, json; 8 | try { 9 | const buildThreadData = await buildThread({ 10 | threadId: res.locals.thread.postId, 11 | board: res.locals.board 12 | }); 13 | /* unlikely, but postsExists middleware can be true, but this can be null if deleted. so just next() to 404 14 | wont matter in the build-workers that call this because they dont destructure and never cause the bug */ 15 | if (!buildThreadData) { 16 | return next(); 17 | } 18 | ({ html, json } = buildThreadData); 19 | } catch (err) { 20 | return next(err); 21 | } 22 | 23 | if (req.path.endsWith('.json')) { 24 | return res.set('Cache-Control', 'max-age=0').json(json); 25 | } else { 26 | return res.set('Cache-Control', 'max-age=0').send(html); 27 | } 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /views/mixins/boardnav.pug: -------------------------------------------------------------------------------- 1 | mixin boardnav(selected, showIndex, upLevel) 2 | if showIndex 3 | | 4 | a(href=`${upLevel ? '../' : ''}index.html` class=(selected === 'index' ? 'bold' : '')) [#{__('Index')}] 5 | | 6 | a(href=`${upLevel ? '../' : ''}catalog.html` class=(selected === 'catalog' ? 'bold' : '')) [#{__('Catalog')}] 7 | if board.settings != null && board.settings.hideBanners === false 8 | | 9 | a(href=`${upLevel ? '../' : ''}banners.html` class=(selected === 'banners' ? 'bold' : '')) [#{__('Banners')}] 10 | | 11 | a(href=`${upLevel ? '../' : ''}logs.html` class=(selected === 'logs' ? 'bold' : '')) [#{__('Logs')}] 12 | if thread != null && board.settings != null && board.settings.archiveLinks === true 13 | | 14 | a(href=`${archiveLinksURL.replace('%s', encodeURIComponent(meta.url+'/'+thread.board+'/thread/'+thread.postId+'.html'))}` rel='nofollow' referrerpolicy='same-origin' target='_blank') [#{__('Archive')}] 15 | -------------------------------------------------------------------------------- /schedules/Schedule.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require(__dirname+'/../lib/misc/config.js'); 4 | 5 | module.exports = class Schedule { 6 | 7 | constructor (func, interval, immediate, condition) { 8 | this.func = func; 9 | this.interval = interval; 10 | this.immediate = immediate; 11 | this.condition = condition; 12 | this.intervalId = null; 13 | this.update(); 14 | } 15 | 16 | //start the schedule 17 | start () { 18 | if (!this.intervalId) { 19 | if (this.immediate) { 20 | this.func(); 21 | } 22 | this.intervalId = setInterval(this.func, this.interval); 23 | } 24 | } 25 | 26 | //stop the schedule 27 | stop () { 28 | clearInterval(this.interval); 29 | this.intervalId = null; 30 | } 31 | 32 | //check config and either start or stop 33 | update () { 34 | if (!this.condition || config.get[this.condition]) { 35 | this.start(); 36 | } else { 37 | this.stop(); 38 | } 39 | } 40 | 41 | }; 42 | -------------------------------------------------------------------------------- /gulp/res/js/iscanvasblocked.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | // Canvas Blocker & Firefox privacy.resistFingerprinting Detector. (c) 2018 // JOHN OZBAY // CRYPT.EE // MIT License: https://github.com/johnozbay/canvas-block-detector/blob/master/isCanvasBlocked.js 3 | function isCanvasBlocked() { 4 | var canvas = document.createElement('canvas'); 5 | var ctx = canvas.getContext('2d'); 6 | if (!ctx) { return true; } 7 | var imageData = ctx.createImageData(1, 1); 8 | var originalImageData = imageData.data; 9 | originalImageData[0] = 128; 10 | originalImageData[1] = 128; 11 | originalImageData[2] = 128; 12 | originalImageData[3] = 255; 13 | ctx.putImageData(imageData, 1, 1); 14 | try { 15 | var checkData = ctx.getImageData(1, 1, 1, 1).data; 16 | if ( 17 | originalImageData[0] !== checkData[0] && 18 | originalImageData[1] !== checkData[1] 19 | ) { return true; } 20 | } catch (error) { 21 | return true; 22 | } 23 | return false; 24 | } 25 | -------------------------------------------------------------------------------- /db/news.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const Mongo = require(__dirname+'/db.js') 5 | , db = Mongo.db.collection('news'); 6 | 7 | module.exports = { 8 | 9 | db, 10 | 11 | find: (limit=0) => { 12 | return db.find({}).sort({ 13 | '_id': -1 14 | }) 15 | .limit(limit) 16 | .toArray(); 17 | }, 18 | 19 | findOne: (id) => { 20 | return db.findOne({ 21 | '_id': id, 22 | }); 23 | }, 24 | 25 | updateOne: (id, title, raw, markdown) => { 26 | return db.updateOne({ 27 | '_id': id, 28 | }, { 29 | '$set': { 30 | 'title': title, 31 | 'message.raw': raw, 32 | 'message.markdown': markdown, 33 | 'edited': new Date(), 34 | } 35 | }); 36 | }, 37 | 38 | insertOne: (news) => { 39 | return db.insertOne(news); 40 | }, 41 | 42 | deleteMany: (ids) => { 43 | return db.deleteMany({ 44 | '_id': { 45 | '$in': ids 46 | } 47 | }); 48 | }, 49 | 50 | deleteAll: () => { 51 | return db.deleteMany({}); 52 | }, 53 | 54 | }; 55 | -------------------------------------------------------------------------------- /views/pages/mypermissions.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/mypermissions.pug 3 | 4 | block head 5 | title #{__('My Permisions')} 6 | 7 | block content 8 | .board-header 9 | h1.board-title #{__('My Permissions')} 10 | br 11 | hr(size=1) 12 | h4.no-m-p #{__('Board-specific permissions')} 13 | if user.ownedBoards && user.ownedBoards.length > 0 || user.staffBoards && user.staffBoards.length > 0 14 | ul 15 | for b in user.ownedBoards 16 | li 17 | a(href=`/${b}/manage/mypermissions.html`) /#{b}/ 18 | for b in user.staffBoards 19 | li 20 | a(href=`/${b}/manage/mypermissions.html`) /#{b}/ 21 | else 22 | p #{__('None')} 23 | hr(size=1) 24 | h4.mv-5 #{__('Account permissions')} 25 | | 26 | if permissions.get(Permissions.MANAGE_GLOBAL_ACCOUNTS) 27 | | 28 | a(href=`/globalmanage/editaccount/${user.username}.html`) [#{__('Edit')}] 29 | - const jsonPermissions = permissions.toJSON(); 30 | +mypermissions(jsonPermissions) 31 | -------------------------------------------------------------------------------- /.gitlab/issue_templates/Default.md: -------------------------------------------------------------------------------- 1 | - [ ] I have searched the issues to make sure I am not opening a duplicate 2 | - [ ] If this is a jschan installation/setup issue, I have followed the INSTALLATION.md 3 | - [ ] I am asking a [SMART QUESTION](http://www.catb.org/~esr/faqs/smart-questions.html) 4 | - [ ] Leave this box unchecked to prove you actually bothered to read 5 | 6 | ## Issue report 7 | 8 | ### Description 9 | A clear and concise description of what the problem is. 10 | 11 | ### Steps to reproduce (if applicable) 12 | 13 | 1. Click on '...' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | ### Expected result 18 | What did you expect to happen? 19 | 20 | ### Actual result 21 | What actually happened? 22 | 23 | ### Possible solution 24 | Optional, if you have a suggestion. 25 | 26 | ### Additional information 27 | For example, screenshots and log excerpts. 28 | 29 | ### Environment 30 | Device: 31 | Operating System and version: 32 | Browser and version: 33 | -------------------------------------------------------------------------------- /lib/middleware/locale/locale.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const i18n = require(__dirname+'/../../locale/locale.js') 4 | , config = require(__dirname+'/../../misc/config.js'); 5 | 6 | module.exports = { 7 | 8 | setGlobalLanguage: (req, res, next) => { 9 | // global settings locale 10 | const { language } = config.get; 11 | res.locals.setLocale(res.locals, language); 12 | next(); 13 | }, 14 | 15 | setBoardLanguage: (req, res, next) => { 16 | // board settings locale 17 | const language = res.locals.board.settings.language; 18 | res.locals.setLocale(res.locals, language); 19 | next(); 20 | }, 21 | 22 | setQueryLanguage: (req, res, next) => { 23 | if (req.query.language 24 | && typeof req.query.language === 'string' 25 | && i18n.getLocales().includes(req.query.language)) { 26 | res.locals.setLocale(res.locals, req.query.language); 27 | } 28 | next(); 29 | }, 30 | 31 | //TODO set param language? set body language? surely not... 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /views/pages/modloglist.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/boardheader.pug 3 | include ../mixins/boardnav.pug 4 | 5 | block head 6 | title /#{board._id}/ - #{__('Logs')} 7 | 8 | block content 9 | +boardheader(__('Logs')) 10 | br 11 | include ../includes/stickynav.pug 12 | .pages 13 | +boardnav('logs', true, false) 14 | hr(size=1) 15 | if dates.length === 0 16 | p #{__('No Logs.')} 17 | else 18 | .table-container.flex-center.mv-10.text-center 19 | table 20 | tr 21 | th #{__('Date')} 22 | th #{__('Events')} 23 | for row in dates 24 | tr 25 | - 26 | const { date, count } = row; 27 | const day = ('0'+date.day).slice(-2); 28 | const month = ('0'+date.month).slice(-2); 29 | const year = date.year; 30 | const logName = `${month}-${day}-${year}`; 31 | td: a(href=`logs/${logName}.html`) #{logName} 32 | td #{count} 33 | hr(size=1) 34 | .pages 35 | +boardnav('logs', true, false) 36 | -------------------------------------------------------------------------------- /migrations/0.1.1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra') 4 | , uploadDirectory = require(__dirname+'/../lib/file/uploaddirectory.js'); 5 | 6 | module.exports = async(db) => { 7 | console.log('adding flags customisation'); 8 | await fs.ensureDir(`${uploadDirectory}/flag/`); 9 | const template = require(__dirname+'/../configs/template.js.example'); 10 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 11 | '$set': { 12 | 'globalLimits.flagFiles': template.globalLimits.flagFiles, 13 | 'globalLimits.flagFilesSize': template.globalLimits.flagFilesSize, 14 | 'boardDefaults.customFlags': false, 15 | }, 16 | '$rename': { 17 | 'boardDefaults.flags': 'boardDefaults.geoFlags', 18 | } 19 | }); 20 | await db.collection('boards').updateMany({}, { 21 | '$set': { 22 | 'flags': {}, 23 | 'settings.customFlags': false, 24 | }, 25 | '$rename': { 26 | 'settings.flags': 'settings.geoFlags', 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/post/tripcode.test.js: -------------------------------------------------------------------------------- 1 | const { getSecureTrip, getInsecureTrip } = require('./tripcode.js'); 2 | 3 | describe('getSecureTrip() - "secure" tripcodes', () => { 4 | const cases = [ 5 | { in: '' }, 6 | { in: null }, 7 | { in: '13245' }, 8 | { in: '1324512345123451234512345123451234512345' }, 9 | ]; 10 | for (let i in cases) { 11 | test(`should not error for an input of ${cases[i].in}`, async () => { 12 | expect((await getSecureTrip(cases[i].in))); 13 | }); 14 | } 15 | }); 16 | 17 | describe('getInsecureTrip() - "insecure" tripcodes', () => { 18 | const cases = [ 19 | { in: '', out: '8NBuQ4l6uQ' }, 20 | { in: null, out: '8NBuQ4l6uQ' }, 21 | { in: '13245', out: 'VPkdFNhOGY' }, 22 | { in: '1324512345123451234512345123451234512345', out: '9ovLU2O1wk' }, 23 | ]; 24 | for (let i in cases) { 25 | test(`should contain ${cases[i].out} for an input of ${cases[i].in}`, () => { 26 | expect(getInsecureTrip(cases[i].in)).toBe(cases[i].out); 27 | }); 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /models/pages/globalmanage/editaccount.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Accounts } = require(__dirname+'/../../../db/') 4 | , roleManager = require(__dirname+'/../../../lib/permission/rolemanager.js') 5 | , Permission = require(__dirname+'/../../../lib/permission/permission.js'); 6 | 7 | module.exports = async (req, res, next) => { 8 | 9 | const editingAccount = await Accounts.findOne(req.params.accountusername); 10 | 11 | if (editingAccount == null) { 12 | //account does not exist 13 | return next(); 14 | } 15 | 16 | const accountPermissions = new Permission(editingAccount.permissions); 17 | //accountPermissions.applyInheritance(); 18 | 19 | res 20 | .set('Cache-Control', 'private, max-age=5') 21 | .render('editaccount', { 22 | csrf: req.csrfToken(), 23 | board: res.locals.board, 24 | accountUsername: req.params.accountusername, 25 | accountPermissions, 26 | roles: roleManager.roles, 27 | permissions: res.locals.permissions, 28 | }); 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /models/pages/manage/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | manageReports: require(__dirname+'/reports.js'), 5 | manageRecent: require(__dirname+'/recent.js'), 6 | manageSettings: require(__dirname+'/settings.js'), 7 | manageFilters: require(__dirname+'/filters.js'), 8 | manageBans: require(__dirname+'/bans.js'), 9 | manageLogs: require(__dirname+'/logs.js'), 10 | manageAssets: require(__dirname+'/assets.js'), 11 | manageBoard: require(__dirname+'/board.js'), 12 | manageCatalog: require(__dirname+'/catalog.js'), 13 | manageThread: require(__dirname+'/thread.js'), 14 | manageCustomPages: require(__dirname+'/custompages.js'), 15 | manageMyPermissions: require(__dirname+'/mypermissions.js'), 16 | editCustomPage: require(__dirname+'/editcustompage.js'), 17 | editFilter: require(__dirname+'/editfilter.js'), 18 | editPost: require(__dirname+'/editpost.js'), 19 | manageStaff: require(__dirname+'/staff.js'), 20 | editStaff: require(__dirname+'/editstaff.js'), 21 | }; 22 | -------------------------------------------------------------------------------- /models/pages/globalmanage/settings.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const config = require(__dirname+'/../../../lib/misc/config.js') 5 | , { fontList } = require(__dirname+'/../../../lib/misc/fonts.js') 6 | , { themes, codeThemes } = require(__dirname+'/../../../lib/misc/themes.js') 7 | , i18n = require(__dirname+'/../../../lib/locale/locale.js') 8 | , { getCountryNames, countryCodes } = require(__dirname+'/../../../lib/misc/countries.js'); 9 | 10 | module.exports = async (req, res) => { 11 | 12 | const { forceActionTwofactor } = config.get; 13 | 14 | res 15 | .set('Cache-Control', 'private, max-age=5') 16 | .render('globalmanagesettings', { 17 | csrf: req.csrfToken(), 18 | settings: config.get, 19 | permissions: res.locals.permissions, 20 | countryNamesMap: getCountryNames(res.locals.locale, { select: 'official' }), 21 | countryCodes, 22 | themes, 23 | codeThemes, 24 | fontList, 25 | languages: i18n.getLocales(), 26 | forceActionTwofactor, 27 | }); 28 | 29 | }; 30 | -------------------------------------------------------------------------------- /lib/post/markdown/handler/linkmatch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Permissions } = require(__dirname+'/../../../permission/permissions.js'); 4 | 5 | module.exports = (permissions, match, p1, p2, p3, offset, string, groups) => { 6 | 7 | let { url, label, urlOnly } = groups; 8 | 9 | url = url || urlOnly; 10 | if (!permissions.get(Permissions.MANAGE_BOARD_GENERAL)) { 11 | label = url 12 | .replace(/\(/g, '(') 13 | .replace(/\)/g, ')'); 14 | } 15 | url = url.replace(/\(/g, '%28') 16 | .replace(/\)/g, '%29'); 17 | 18 | /* 19 | //todo: Something to revisit later 20 | const href = url; 21 | if (urlOnly == null) { 22 | try { 23 | const urlObject = new URL(url); 24 | if (config.get.allowedHosts.includes(urlObject.hostname)) { 25 | href = `${urlObject.pathname}${urlObject.search}${urlObject.hash}`; 26 | } 27 | } catch (e) { } 28 | } 29 | */ 30 | 31 | return `${label || url}`; 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /views/pages/overboard.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/post.pug 3 | include ../mixins/overboardform.pug 4 | include ../mixins/announcements.pug 5 | 6 | block head 7 | title #{__('Overboard Index')} 8 | 9 | block content 10 | .board-header 11 | h1.board-title #{__('Overboard Index')} 12 | h4.board-description #{__('Recently bumped threads from multiple boards')} 13 | | 14 | | ( 15 | a(href=`/catalog.html?${cacheQueryString}`) #{__('Catalog View')} 16 | | ) 17 | include ../includes/stickynav.pug 18 | if allowCustomOverboard === true 19 | +overboardform('/overboard.html') 20 | +announcements(true, true, false) 21 | hr(size=1) 22 | if threads.length === 0 23 | p #{__('No posts.')} 24 | for thread in threads 25 | h4.no-m-p #{__('Thread from')} #[a(href=`/${thread.board}/index.html`) /#{thread.board}/] 26 | .thread 27 | +post(thread, true, false, false, false, true) 28 | for post in thread.replies 29 | +post(post, true, false, false, false, true) 30 | hr(size=1) 31 | -------------------------------------------------------------------------------- /migrations/1.0.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Updating edited posts username property for "hidden users" to support localisation'); 5 | await db.collection('posts').updateMany({ 6 | 'edited.username': 'Hidden User', 7 | }, { 8 | '$set': { 9 | 'edited.username': null, 10 | }, 11 | }); 12 | console.log('Updating db for language settings'); 13 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 14 | '$set': { 15 | 'language': 'en-GB', 16 | }, 17 | }); 18 | await db.collection('boards').updateMany({}, { 19 | '$set': { 20 | 'settings.language': 'en-GB', 21 | }, 22 | }); 23 | await db.collection('modlog').updateMany({ 24 | 'actions': 'Edit', 25 | }, { 26 | '$set': { 27 | 'actions': ['Edit'], 28 | }, 29 | }); 30 | console.log('Clearing globalsettings cache'); 31 | await redis.deletePattern('globalsettings'); 32 | console.log('Clearing boards cache'); 33 | await redis.deletePattern('board:*'); 34 | }; 35 | -------------------------------------------------------------------------------- /migrations/0.0.4.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Renaming some settings fields on boards'); 5 | await db.collection('boards').updateMany({}, { 6 | '$rename': { 7 | 'settings.locked': 'settings.lockMode', 8 | 'settings.unlisted': 'settings.unlistedLocal', 9 | 'settings.webring': 'settings.unlistedWebring' 10 | } 11 | }); 12 | console.log('upadting renamed fields to proper values'); 13 | await db.collection('boards').updateMany({ 14 | 'settings.lockMode': true, 15 | }, { 16 | '$set': { 17 | 'settings.lockMode': 2, 18 | } 19 | }); 20 | await db.collection('boards').updateMany({ 21 | 'settings.lockMode': false, 22 | }, { 23 | '$set': { 24 | 'settings.lockMode': 0, 25 | } 26 | }); 27 | await db.collection('boards').updateMany({ 28 | 'settings.triggerAction': 3, 29 | }, { 30 | '$set': { 31 | 'settings.triggerAction': 4, 32 | } 33 | }); 34 | console.log('clearing boards cache'); 35 | await redis.deletePattern('board:*'); 36 | }; 37 | -------------------------------------------------------------------------------- /migrations/0.1.6.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = async(db, redis) => { 4 | console.log('Adding custom overboard toggle, links for archive and reverse image urls and add to board defaults'); 5 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 6 | '$set': { 7 | 'allowCustomOverboard': false, 8 | 'archiveLinksURL': 'https://archive.today/?run=1&url=%s', 9 | 'reverseImageLinksURL': 'https://tineye.com/search?url=%s', 10 | 'boardDefaults.archiveLinks': false, 11 | 'boardDefaults.reverseImageSearchLinks': false, 12 | }, 13 | }); 14 | console.log('Cleared globalsettings cache'); 15 | await redis.deletePattern('globalsettings'); 16 | console.log('Adding archive and imgops link options to boards'); 17 | await db.collection('boards').updateMany({}, { 18 | '$set': { 19 | 'settings.archiveLinks': false, 20 | 'settings.reverseImageSearchLinks': false, 21 | } 22 | }); 23 | console.log('Cleared boards cache'); 24 | await redis.deletePattern('board:*'); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /models/forms/deletestaff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Boards, Accounts } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js'); 5 | 6 | module.exports = async (req, res) => { 7 | 8 | const { __ } = res.locals; 9 | //only a ROOT could do this, per the permission bypass in the controller 10 | const deletingBoardOwner = req.body.checkedstaff.some(s => s === res.locals.board.owner); 11 | 12 | await Promise.all([ 13 | Accounts.removeStaffBoard(req.body.checkedstaff, res.locals.board._id), 14 | Boards.removeStaff(res.locals.board._id, req.body.checkedstaff), 15 | deletingBoardOwner ? Accounts.removeOwnedBoard(res.locals.board.owner, res.locals.board._id) : void 0, 16 | deletingBoardOwner ? Boards.setOwner(res.locals.board._id, null) : void 0, 17 | ]); 18 | 19 | return dynamicResponse(req, res, 200, 'message', { 20 | 'title': __('Success'), 21 | 'message': __('Deleted staff'), 22 | 'redirect': `/${req.params.board}/manage/staff.html`, 23 | }); 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /schedules/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process 4 | .on('uncaughtException', console.error) 5 | .on('unhandledRejection', console.error); 6 | 7 | const Mongo = require(__dirname+'/../db/db.js') 8 | , config = require(__dirname+'/../lib/misc/config.js') 9 | , { addCallback } = require(__dirname+'/../lib/redis/redis.js'); 10 | 11 | (async () => { 12 | 13 | await Mongo.connect(); 14 | await Mongo.checkVersion(); 15 | await config.load(); 16 | 17 | //start all the scheduled tasks 18 | const schedules = require(__dirname+'/tasks/index.js'); 19 | 20 | //update the schedules to start/stop timer after config change 21 | addCallback('config', () => { 22 | Object.values(schedules).forEach(sc => { 23 | sc.update(); 24 | }); 25 | }); 26 | 27 | //update board stats and homepage task, use cron and bull for proper timing 28 | require(__dirname+'/../lib/build/queue.js').push({ 29 | 'task': 'updateStats', 30 | 'options': {} 31 | }, { 32 | 'repeat': { 33 | 'cron': '0 * * * *' 34 | } 35 | }); 36 | 37 | })(); 38 | -------------------------------------------------------------------------------- /db/roles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , db = Mongo.db.collection('roles') 5 | , cache = require(__dirname+'/../lib/redis/redis.js'); 6 | 7 | module.exports = { 8 | 9 | db, 10 | 11 | findOne: async (id) => { 12 | //is there any point even caching 13 | let role = await cache.get(`role:${id}`); 14 | if (role) { 15 | return role; 16 | } else { 17 | role = await db.findOne({ '_id': id }); 18 | if (role) { 19 | role.permissions = role.permissions.toString('base64'); 20 | cache.set(`role:${id}`, role); 21 | } 22 | } 23 | return role; 24 | }, 25 | 26 | find: () => { 27 | return db.find({}).toArray(); 28 | }, 29 | 30 | updateOne: async (id, permissions) => { 31 | const res = await db.updateOne({ 32 | '_id': id 33 | }, { 34 | '$set': { 35 | 'permissions': Mongo.Binary(permissions.array), 36 | }, 37 | }); 38 | cache.del(`role:${id}`); 39 | return res; 40 | }, 41 | 42 | deleteAll: () => { 43 | return db.deleteMany({}); 44 | }, 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /views/includes/permissionsform.pug: -------------------------------------------------------------------------------- 1 | if jsonPermissions[bit].title && index > 0 2 | hr(size=1) 3 | h4.mv-5 #{__(jsonPermissions[bit].title)} 4 | if jsonPermissions[bit].subtitle 5 | p #{__(jsonPermissions[bit].subtitle)} 6 | .row 7 | - const parentAllowed = jsonPermissions[bit].parents == null || permissions.hasAny(...jsonPermissions[bit].parents); 8 | - const parentLabel = !parentAllowed ? (jsonPermissions[bit].parents ? jsonPermissions[bit].parents.map(p => jsonPermissions[p].label).join('\n') : '') : ''; 9 | label.postform-style.ph-5(id=`perm_${bit}` class=(!parentAllowed ? 'notallowed' : null) title=(!parentAllowed ? __(`Requires permission "${parentLabel}"`) : null)) 10 | input(type='checkbox' name=`permission_bit_${bit}` value=bit checked=jsonPermissions[bit].state disabled=(!parentAllowed || jsonPermissions[bit].block)) 11 | .rlabel #{__(jsonPermissions[bit].label)} 12 | p 13 | if !parentAllowed && parentLabel 14 | span.br #{__(`Requires permission "${parentLabel}"`)} 15 | | - 16 | | #{__(jsonPermissions[bit].desc)} 17 | -------------------------------------------------------------------------------- /views/pages/twofactor.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title #{__('Two Factor Authentication Setup')} 5 | 6 | block content 7 | .board-header 8 | h1.board-title #{__('Two Factor Authentication Setup')} 9 | .form-wrapper.flex-center.mv-10 10 | form.form-post.nogrow(action=`/forms/twofactor` method='POST' enctype='application/x-www-form-urlencoded') 11 | input(type='hidden' name='_csrf' value=csrf) 12 | 13 | h4.mv-5 #{__('Scan the QR Code in an authenticator app, and submit the code')}: 14 | .row 15 | span.code.hljs.twofactor.noselect #{qrCodeText} 16 | .row 17 | h4.no-m-p #{__('No camera? Use this secret in your authenticator app instead')}: 18 | .row 19 | span.code #{secretBase32} 20 | .row 21 | h4.mv-5.ban #{__('Enabling 2FA will invalidate all your existing sessions and you will have to login again.')} 22 | .row 23 | .label #{__('2FA Code')} 24 | input(type='number' name='twofactor' placeholder=__('6 digits')) 25 | .row 26 | input(type='submit', value=__('Submit')) 27 | -------------------------------------------------------------------------------- /gulp/res/css/themes/army-green.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --icon-color:invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%); 3 | --alt-label-color:#6e7e46; 4 | --alt-font-color:black; 5 | --background-top:#2a401b; 6 | --background-rest:#5b7744; 7 | --navbar-color:#2a401b; 8 | --post-color:#4e5e36; 9 | --post-outline-color:#1a401b; 10 | --label-color:#6e7e46; 11 | --box-border-color:#1a401b; 12 | --darken:#00000030; 13 | --highlighted-post-color:#4e4e26; 14 | --highlighted-post-outline-color:#411; 15 | --board-title:#af0a0f; 16 | --hr:#c2c888; 17 | --font-color:#fff; 18 | --name-color:#dd0; 19 | --capcode-color:#f00; 20 | --subject-color:#ff0; 21 | --post-link-color:#d2d888; 22 | --link-color:#d2d888; 23 | --link-hover:#fff; 24 | --accent-color:#000; 25 | --input-borders:#a9a9a9; 26 | --input-color:#000; 27 | --input-background:white; 28 | --dice-color:darkorange; 29 | --title-color:#fd0; 30 | --greentext-color:#de6; 31 | --pinktext-color:#E0727F; 32 | } 33 | .postform-style { 34 | color: #000; 35 | } 36 | -------------------------------------------------------------------------------- /gulp/res/css/themes/gurochan.css: -------------------------------------------------------------------------------- 1 | /*gurochan*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #e6cbc0; 5 | --alt-font-color: #000; 6 | --background-top: #EDDAD2; 7 | --background-rest: #EDDAD2; 8 | --navbar-color: #D9AF9E; 9 | --post-color: #D9AF9E; 10 | --post-outline-color: #CA927B; 11 | --label-color: #E6CBC0; 12 | --box-border-color: #CA927B; 13 | --darken: #0000000f; 14 | --highlighted-post-color: #EEDACB; 15 | --highlighted-post-outline-color: #CA927B; 16 | --board-title: #FF6600; 17 | --hr: #CA927B; 18 | --font-color: #000; 19 | --name-color: #000; 20 | --capcode-color: #f00; 21 | --subject-color: #004A99; 22 | --link-color: #34345C; 23 | --post-link-color: #34345C; 24 | --link-hover: #34345C; 25 | --accent-color: #E6CBC0; 26 | --input-borders: #CA927B; 27 | --input-color: #000; 28 | --input-background: #E6CBC0; 29 | --dice-color: darkorange; 30 | --title-color: #d70000; 31 | --greentext-color: #789922; 32 | --pinktext-color:#E0727F; 33 | } 34 | -------------------------------------------------------------------------------- /lib/input/settingsdiff.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isDeepStrictEqual } = require('util'); 4 | 5 | function getDotProp(obj, prop) { 6 | return prop 7 | .split('.') 8 | .reduce((a, b) => { 9 | if (a && a[b]) { 10 | return a[b]; 11 | } 12 | return null; 13 | }, obj); 14 | } 15 | 16 | function includeChildren(template, prop, tasks) { 17 | return Object.keys(getDotProp(template, prop) || {}) 18 | .reduce((a, x) => { 19 | a[`${prop}.${x}`] = tasks; 20 | return a; 21 | }, {}); 22 | } 23 | 24 | function compareSettings(entries, oldObject, newObject, maxSetSize) { 25 | const resultSet = new Set(); 26 | entries.every(entry => { 27 | const oldValue = getDotProp(oldObject, entry[0]); 28 | const newValue = getDotProp(newObject, entry[0]); 29 | if (!isDeepStrictEqual(oldValue, newValue)) { 30 | entry[1].forEach(t => resultSet.add(t)); 31 | } 32 | return resultSet.size <= maxSetSize; 33 | }); 34 | return resultSet; 35 | } 36 | 37 | module.exports = { 38 | getDotProp, 39 | includeChildren, 40 | compareSettings, 41 | }; 42 | -------------------------------------------------------------------------------- /models/pages/modlog.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Modlogs } = require(__dirname+'/../../db/') 4 | , { buildModLog } = require(__dirname+'/../../lib/build/tasks.js'); 5 | 6 | module.exports = async (req, res, next) => { 7 | 8 | if (!res.locals.date) { 9 | return next(); 10 | } 11 | 12 | const startDate = res.locals.date.date; 13 | const { year, month, day } = res.locals.date; 14 | const endDate = new Date(Date.UTC(year, month, day, 23, 59, 59, 999)); 15 | 16 | let html, json; 17 | try { 18 | const logs = await Modlogs.findBetweenDate(res.locals.board, startDate, endDate); 19 | if (!logs || logs.length === 0) { 20 | return next(); 21 | } 22 | ({ html, json } = await buildModLog({ 23 | board: res.locals.board, 24 | startDate, 25 | endDate, 26 | logs 27 | })); 28 | } catch (err) { 29 | return next(err); 30 | } 31 | 32 | if (req.path.endsWith('.json')) { 33 | return res.set('Cache-Control', 'max-age=0').json(json); 34 | } else { 35 | return res.set('Cache-Control', 'max-age=0').send(html); 36 | } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /views/mixins/custompage.pug: -------------------------------------------------------------------------------- 1 | mixin custompage(page, manage=false) 2 | .table-container.flex-center.mv-5 3 | table 4 | tr 5 | th 6 | if manage === true 7 | input.left.post-check(type='checkbox', name='checkedcustompages' value=page.page) 8 | a.left(href=`/${board._id}/page/${page.page}.html`) #{page.title} 9 | a.right.ml-5(href=`/${board._id}/manage/editcustompage/${page._id}.html`) [#{__('Edit')}] 10 | - const pageDate = new Date(page.date); 11 | time.right.reltime(datetime=pageDate.toISOString()) #{pageDate.toLocaleString(pageLanguage, {hourCycle:'h23'})} 12 | tr 13 | td 14 | if manage === true 15 | p.no-m-p #{`${page.message.raw.substring(0,50)}...`} 16 | else 17 | pre.post-message.no-m-p !{page.message.markdown} 18 | if page.edited 19 | small.right.cb.edited 20 | | #{__('Last edited')} 21 | - const pageEditDate = new Date(page.edited); 22 | time.reltime(datetime=pageEditDate.toISOString()) #{pageEditDate.toLocaleString(pageLanguage, {hourCycle:'h23'})} 23 | -------------------------------------------------------------------------------- /gulp/res/css/themes/choc.css: -------------------------------------------------------------------------------- 1 | /*tomorrow*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #42200f; 5 | --alt-font-color: #ff7e83; 6 | --background-top: #2f1903; 7 | --background-rest: #261602; 8 | --navbar-color: #302016; 9 | --post-color: #302016; 10 | --post-outline-color: #523830; 11 | --label-color: #42200f; 12 | --box-border-color: #4c332c; 13 | --darken: #ffffff10; 14 | --highlighted-post-color: #572a1b; 15 | --highlighted-post-outline-color: #784134; 16 | --board-title: #ff7e83; 17 | --hr: #4c332c; 18 | --font-color: #ff7e83; 19 | --name-color: #ff7e83; 20 | --capcode-color: #ff5252; 21 | --subject-color: #ff5252; 22 | --link-color: #a2a2ff; 23 | --post-link-color: #a2a2ff; 24 | --link-hover: #ff4d4b; 25 | --accent-color: #784134; 26 | --input-borders: #535353; 27 | --input-color: #fff; 28 | --input-background: #000; 29 | --dice-color: darkorange; 30 | --title-color: #d70000; 31 | --greentext-color: green; 32 | --pinktext-color:#E0727F; 33 | } 34 | -------------------------------------------------------------------------------- /lib/post/deletequotes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //use simple string replacement and filter to remove dead quotes, instead of running the whole messagehandler again 4 | module.exports = (deletedPosts, updateQuotePosts) => { 5 | const bulkWrites = []; 6 | updateQuotePosts.forEach(post => { 7 | deletedPosts.forEach(ap => { 8 | const quotesBefore = post.quotes.length; 9 | post.quotes = post.quotes.filter(q => q.postId !== ap.postId); 10 | if (quotesBefore !== post.quotes.length) { //optimization, probably 11 | post.message = post.message.replace( 12 | `>>${ap.postId}`, 13 | `>>${ap.postId}` 14 | ); 15 | } 16 | }); 17 | bulkWrites.push({ 18 | 'updateOne': { 19 | 'filter': { 20 | '_id': post._id 21 | }, 22 | 'update': { 23 | '$set': { 24 | 'quotes': post.quotes, 25 | 'message': post.message, 26 | } 27 | } 28 | } 29 | }); 30 | }); 31 | return bulkWrites; 32 | }; 33 | -------------------------------------------------------------------------------- /gulp/res/css/themes/navy.css: -------------------------------------------------------------------------------- 1 | /*tomorrow*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #1a1e34; 5 | --alt-font-color: #bfbfbf; 6 | --background-top: #080c19; 7 | --background-rest: #080c19; 8 | --navbar-color: #1a1e34; 9 | --post-color: #1a1e34; 10 | --post-outline-color: #2c3a4e; 11 | --label-color: #1a1e34; 12 | --box-border-color: #2c3a4e; 13 | --darken: #ffffff08; 14 | --highlighted-post-color: #44283e; 15 | --highlighted-post-outline-color: #664e5c; 16 | --board-title: #c10000; 17 | --hr: #303030; 18 | --font-color: #bfbfbf; 19 | --name-color: #67b38d; 20 | --capcode-color: #ff4141; 21 | --subject-color: #8f8dc9; 22 | --link-color: #8585a3; 23 | --post-link-color: #f65252; 24 | --link-hover: #f65252; 25 | --accent-color: #434343; 26 | --input-borders: #434343; 27 | --input-color: #bfbfbf; 28 | --input-background: #080c19; 29 | --dice-color: darkorange; 30 | --title-color: #d70000; 31 | --greentext-color: #0a0; 32 | --pinktext-color:#E0727F; 33 | } 34 | -------------------------------------------------------------------------------- /gulp/res/css/themes/rei-zero.css: -------------------------------------------------------------------------------- 1 | /*rei-zero*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #29373e; 5 | --alt-font-color: #d6d6d6; 6 | --background-top: #000E1C; 7 | --background-rest: #000E1C; 8 | --navbar-color: #29373E; 9 | --post-color: #29373E; 10 | --post-outline-color: #666666; 11 | --label-color: #29373E; 12 | --box-border-color: #000; 13 | --darken: #00000010; 14 | --highlighted-post-color: #193A3A; 15 | --highlighted-post-outline-color: #999999; 16 | --board-title: #193A3A; 17 | --hr: #474747; 18 | --font-color: #D6D6D6; 19 | --name-color: #117743; 20 | --capcode-color: #f00; 21 | --subject-color: #CCCCCC; 22 | --link-color: #5C99AD; 23 | --post-link-color: #00BfFF; 24 | --link-hover: #eeeeee; 25 | --accent-color: #29373E; 26 | --input-borders: #a9a9a9; 27 | --input-color: lightgray; 28 | --input-background: #001229; 29 | --dice-color: darkorange; 30 | --title-color: #d70000; 31 | --greentext-color: #789922; 32 | --pinktext-color:#E0727F; 33 | } 34 | -------------------------------------------------------------------------------- /views/pages/create.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title #{__('Create Board')} 5 | 6 | block content 7 | h1.board-title #{__('Create Board')} 8 | .form-wrapper.flex-center.mv-10 9 | form.form-post(action='/forms/create' method='POST' data-captcha-preload='true') 10 | .row 11 | .label #{__('URI e.g. /uri/')} 12 | input(type='text', name='uri', maxlength=globalLimits.fieldLength.uri pattern='[a-zA-Z0-9]+' required title=__('alphanumeric only')) 13 | .row 14 | .label #{__('Name')} 15 | input(type='text', name='name', maxlength=globalLimits.fieldLength.boardname required) 16 | .row 17 | .label #{__('Description')} 18 | input(type='text', name='description', maxlength=globalLimits.fieldLength.description) 19 | .row 20 | .label #{__('Tags')} 21 | textarea(name='tags' placeholder=__('Newline separated, max 10')) 22 | if captchaOptions.type === 'text' 23 | include ../includes/captchasidelabel.pug 24 | else 25 | include ../includes/captchafieldrow.pug 26 | input(type='submit', value=__('Create')) 27 | 28 | -------------------------------------------------------------------------------- /models/forms/deletefilter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Filters } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js'); 5 | 6 | module.exports = async (req, res) => { 7 | 8 | const { __, __n } = res.locals; 9 | 10 | const deletedFilters = await Filters.deleteMany(req.params.board, req.body.checkedfilters).then(result => result.deletedCount); 11 | 12 | if (deletedFilters === 0 || deletedFilters < req.body.checkedfilters.length) { 13 | return dynamicResponse(req, res, 400, 'message', { 14 | 'title': __('Bad request'), 15 | 'error': __n('Deleted %s filters', deletedFilters), 16 | 'redirect': req.headers.referer || (req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html') 17 | }); 18 | } 19 | 20 | return dynamicResponse(req, res, 200, 'message', { 21 | 'title': __('Success'), 22 | 'message': __n('Deleted %s filters', deletedFilters), 23 | 'redirect': req.params.board ? `/${req.params.board}/manage/filters.html` : '/globalmanage/filters.html' 24 | }); 25 | 26 | }; 27 | -------------------------------------------------------------------------------- /views/pages/editnews.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | 4 | block head 5 | title #{__('Edit News')} 6 | 7 | block content 8 | h1.board-title #{__('Global Management')} 9 | br 10 | +globalmanagenav('news', true) 11 | hr(size=1) 12 | h4.no-m-p #{__('Edit Newspost')}: 13 | include ../includes/stickynav.pug 14 | .form-wrapper.flex-center.mv-10 15 | form.form-post(action='/forms/global/editnews' method='POST') 16 | input(type='hidden' name='_csrf' value=csrf) 17 | input(type='hidden' name='news_id' value=news._id) 18 | .table-container.flex-center.mv-5 19 | table 20 | tr 21 | th 22 | input.edit.left(type='text' name='title' value=news.title required) 23 | - const newsDate = new Date(news.date); 24 | time.right.reltime(datetime=newsDate.toISOString()) #{newsDate.toLocaleString(pageLanguage, {hourCycle:'h23'})} 25 | tr 26 | td 27 | textarea.edit.fw(name='message' rows='10' placeholder=__('Supports post styling') required) #{news.message.raw} 28 | input(type='submit', value=__('Save')) 29 | -------------------------------------------------------------------------------- /db/bypass.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Mongo = require(__dirname+'/db.js') 4 | , config = require(__dirname+'/../lib/misc/config.js') 5 | , db = Mongo.db.collection('bypass'); 6 | 7 | module.exports = { 8 | 9 | db, 10 | 11 | checkBypass: (id, anonymizer=false) => { 12 | return db.findOneAndUpdate({ 13 | '_id': id, 14 | 'anonymizer': anonymizer, 15 | 'uses': { 16 | '$gt': 0 17 | } 18 | }, { 19 | '$inc': { 20 | 'uses': -1, 21 | } 22 | }).then(r => r.value); 23 | }, 24 | 25 | getBypass: (anonymizer=false, id=null, uses=0) => { 26 | const { blockBypass } = config.get; 27 | const newBypass = { 28 | 'uses': uses, 29 | 'anonymizer': anonymizer, 30 | 'expireAt': new Date(Date.now() + blockBypass.expireAfterTime) 31 | }; 32 | if (id !== null) { 33 | newBypass._id = Mongo.ObjectId(id); 34 | return db.replaceOne({ 35 | _id: newBypass._id 36 | }, newBypass, { 37 | upsert: true, 38 | }); 39 | } 40 | return db.insertOne(newBypass); 41 | }, 42 | 43 | deleteAll: () => { 44 | return db.deleteMany({}); 45 | }, 46 | 47 | }; 48 | -------------------------------------------------------------------------------- /views/pages/sessions.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | 3 | block head 4 | title #{__('Sessions')} 5 | 6 | block content 7 | .board-header 8 | h1.board-title #{__('Active Sessions')} 9 | br 10 | hr(size=1) 11 | h4.mv-5 #{__('Active Sessions')}: 12 | form.form-post.nogrow(action=`/forms/deletesessions` method='POST' enctype='application/x-www-form-urlencoded') 13 | input(type='hidden' name='_csrf' value=csrf) 14 | .table-container.flex-left.text-center 15 | table 16 | tr 17 | th 18 | th #{__('ID')} 19 | th #{__('Expires')} 20 | each session, sessionId in sessions 21 | tr(class=(sessionId === currentSessionKey ? 'bold' : '')) 22 | td: input(type='checkbox', name='checkedsessionids' value=sessionId) 23 | td #{sessionId} #{sessionId === currentSessionKey ? '(current)' : ''} 24 | - const expiryDate = new Date(session.cookie.expires) 25 | td: time.reltime(datetime=expiryDate.toISOString()) #{expiryDate.toLocaleString(pageLanguage, {hourCycle:'h23'})} 26 | h4.mv-5 #{__('Delete Selected')}: 27 | input(type='submit', value=__('Delete')) 28 | -------------------------------------------------------------------------------- /configs/secrets.js.example: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | //mongodb connection string 4 | dbURL: 'mongodb://jschan:CHANGE-ME-YOUR-SECURE-MONGODB-PASSWORD@127.0.0.1:27017/jschan', 5 | 6 | //database name 7 | dbName: 'jschan', 8 | 9 | //redis connection info 10 | redis: { 11 | host: '127.0.0.1', 12 | port: '6379', 13 | password: 'CHANGE-ME-YOUR-SECURE-REDIS-PASSWORD' 14 | }, 15 | 16 | //backend webserver port 17 | port: 7000, 18 | 19 | //secrets/salts for various things 20 | cookieSecret: 'changeme', 21 | tripcodeSecret: 'changeme', 22 | ipHashSecret: 'changeme', 23 | postPasswordSecret: 'changeme', 24 | 25 | //keys for google recaptcha 26 | google: { 27 | siteKey: 'changeme', 28 | secretKey: 'changeme' 29 | }, 30 | 31 | //keys for hcaptcha 32 | hcaptcha: { 33 | siteKey: '10000000-ffff-ffff-ffff-000000000001', 34 | secretKey: '0x0000000000000000000000000000000000000000' 35 | }, 36 | 37 | //keys for yandex smartcaptcha 38 | yandex: { 39 | siteKey: 'changeme', 40 | secretKey: 'changeme' 41 | }, 42 | 43 | //enable debug logging 44 | debugLogs: true, 45 | 46 | }; 47 | -------------------------------------------------------------------------------- /views/mixins/newspost.pug: -------------------------------------------------------------------------------- 1 | mixin newspost(post, globalmanage=false) 2 | .table-container.flex-center.mv-5 3 | .anchor(id=post._id) 4 | table 5 | tr 6 | th 7 | if globalmanage === true 8 | input.left.post-check(type='checkbox', name='checkednews' value=post._id) 9 | a.left(href=`/news.html#${post._id}`) #{post.title} 10 | if globalmanage === true 11 | a.right.ml-5(href=`/globalmanage/editnews/${post._id}.html`) [#{__('Edit')}] 12 | - const newsDate = new Date(post.date); 13 | time.right.reltime(datetime=newsDate.toISOString()) #{newsDate.toLocaleString(pageLanguage, {hourCycle:'h23'})} 14 | tr 15 | td 16 | if globalmanage === true 17 | p.no-m-p #{`${post.message.raw.substring(0,50)}...`} 18 | else 19 | pre.post-message.no-m-p !{post.message.markdown} 20 | 21 | if post.edited 22 | small.right.cb.edited 23 | | #{__('Last edited')} 24 | - const newsEditDate = new Date(post.edited); 25 | time.reltime(datetime=newsEditDate.toISOString()) #{newsEditDate.toLocaleString(pageLanguage, {hourCycle:'h23'})} 26 | -------------------------------------------------------------------------------- /lib/input/decodequeryip.test.js: -------------------------------------------------------------------------------- 1 | const decodeQueryIp = require('./decodequeryip.js'); 2 | const Permission = require('../permission/permission.js'); 3 | const ROOT = new Permission(); 4 | ROOT.setAll(Permission.allPermissions); 5 | const NO_PERMISSION = new Permission(); 6 | 7 | describe('decode query ip', () => { 8 | 9 | const cases = [ 10 | { in: { query: null, permission: ROOT }, out: null }, 11 | { in: { query: {}, permission: ROOT }, out: null }, 12 | { in: { query: { ip: '10.0.0.1' }, permission: ROOT }, out: '10.0.0.1' }, 13 | { in: { query: { ip: '10.0.0.1' }, permission: NO_PERMISSION }, out: null }, 14 | { in: { query: { ip: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, permission: ROOT }, out: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, 15 | { in: { query: { ip: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, permission: NO_PERMISSION }, out: '8s7AGX4n.qHsw9mp.uw54Nfl.IP' }, 16 | ]; 17 | 18 | for (let i in cases) { 19 | test(`should output ${cases[i].out} for an input of ${cases[i].in}`, () => { 20 | expect(decodeQueryIp(cases[i].in.query, cases[i].in.permission)).toStrictEqual(cases[i].out); 21 | }); 22 | } 23 | 24 | }); 25 | -------------------------------------------------------------------------------- /tools/backup.sh.example: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Dumps jschan database to gzip archive, and archives the static folder 4 | # 5 | 6 | #variables, change me 7 | APP_NAME="whatever" 8 | MONGO_DATABASE="jschan" 9 | MONGO_HOST="" 10 | MONGO_PORT="" 11 | MONGO_USER="" 12 | MONGO_PASSWORD="" 13 | TIMESTAMP=`date +%F-%H%M` 14 | BACKUPS_DIR="/path/to/backups/folder/$APP_NAME" 15 | 16 | #probably dont change these 17 | DB_BACKUP_NAME="$APP_NAME-$TIMESTAMP.gz" 18 | FILE_BACKUP_NAME="$APP_NAME-$TIMESTAMP-files.zip" 19 | DB_ARCHIVE_PATH="$BACKUPS_DIR/$DB_BACKUP_NAME" 20 | FILE_ARCHIVE_PATH="$BACKUPS_DIR/$FILE_BACKUP_NAME" 21 | 22 | #create backup folder 23 | mkdir -p $BACKUPS_DIR 24 | 25 | #archive (no compression) files 26 | zip -r -0 $FILE_ARCHIVE_PATH ./static 27 | 28 | #dump database to .gz archive 29 | mongodump --username $MONGO_USER --password $MONGO_PASSWORD --authenticationDatabase admin --db $MONGO_DATABASE --archive=$DB_ARCHIVE_PATH --gzip 30 | rm -rf dump 31 | 32 | #delete backups older than 7 days 33 | sudo find $ARCHIVE_PATH -type f -name "*.gz" -mtime +7 -exec rm -f {} \; 34 | sudo find $ARCHIVE_PATH -type f -name "*.zip" -mtime +7 -exec rm -f {} \; 35 | -------------------------------------------------------------------------------- /migrations/0.8.0.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const timeUtils = require(__dirname+'/../lib/converter/timeutils.js'); 4 | 5 | module.exports = async(db, redis) => { 6 | console.log('add more captcha options and add inactive account and board auto handling'); 7 | await db.collection('globalsettings').updateOne({ _id: 'globalsettings' }, { 8 | '$set': { 9 | 'captchaOptions.text': { 10 | 'font': 'default', 11 | 'line': true, 12 | 'wave': 0, 13 | 'paint': 2, 14 | 'noise': 0, 15 | }, 16 | 'captchaOptions.grid.falses': ['○','□','♘','♢','▽','△','♖','✧','♔','♘','♕','♗','♙','♧'], 17 | 'captchaOptions.grid.trues': ['●','■','♞','♦','▼','▲','♜','✦','♚','♞','♛','♝','♟','♣'], 18 | 'captchaOptions.grid.question': 'Select the solid/filled icons', 19 | 'captchaOptions.grid.noise': 0, 20 | 'captchaOptions.grid.edge': 25, 21 | 'inactiveAccountTime': timeUtils.MONTH * 3, 22 | 'inactiveAccountAction': 0, //no actions by default 23 | 'abandonedBoardAction': 0, 24 | 'hotThreadsMaxAge': timeUtils.MONTH, 25 | }, 26 | }); 27 | console.log('Clearing globalsettings cache'); 28 | await redis.deletePattern('globalsettings'); 29 | }; 30 | -------------------------------------------------------------------------------- /lib/middleware/misc/referrercheck.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const config = require(__dirname+'/../../misc/config.js') 4 | , dynamicResponse = require(__dirname+'/../../misc/dynamic.js') 5 | , { addCallback } = require(__dirname+'/../../redis/redis.js'); 6 | 7 | let refererCheck, allowedHosts, allowedHostSet; 8 | const updateReferers = () => { 9 | ({ refererCheck, allowedHosts } = config.get); 10 | allowedHostSet = new Set(allowedHosts); 11 | }; 12 | updateReferers(); 13 | addCallback('config', updateReferers); 14 | 15 | module.exports = (req, res, next) => { 16 | if (req.method !== 'POST') { 17 | return next(); 18 | } 19 | let validReferer = false; 20 | try { 21 | const url = new URL(req.headers.referer); 22 | validReferer = allowedHostSet.has(url.hostname); 23 | } catch (e) { 24 | //referrer is invalid url 25 | } 26 | if (refererCheck === true && (!req.headers.referer || !validReferer)) { 27 | const { __ } = res.locals; 28 | return dynamicResponse(req, res, 403, 'message', { 29 | 'title': __('Forbidden'), 30 | 'message': __('Invalid or missing "Referer" header. Are you posting from the correct URL?'), 31 | }); 32 | } 33 | next(); 34 | }; 35 | -------------------------------------------------------------------------------- /lib/post/tripcode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { tripcodeSecret } = require(__dirname+'/../../configs/secrets.js') 4 | , { createHash } = require('crypto') 5 | , { encode } = require('iconv-lite') 6 | , crypt = require('unix-crypt-td-js') 7 | , replace = { 8 | ':': 'A', 9 | ';': 'B', 10 | '<': 'C', 11 | '=': 'D', 12 | '>': 'E', 13 | '?': 'F', 14 | '@': 'G', 15 | '[': 'a', 16 | '\\': 'b', 17 | ']': 'c', 18 | '^': 'd', 19 | '_': 'e', 20 | '`': 'f', 21 | }; 22 | 23 | module.exports = { 24 | 25 | getSecureTrip: async (password) => { 26 | const tripcodeHash = createHash('sha256').update(password + tripcodeSecret).digest('base64'); 27 | const tripcode = tripcodeHash.substring(tripcodeHash.length-10); 28 | return tripcode; 29 | }, 30 | 31 | getInsecureTrip: (password) => { 32 | const encoded = encode(password, 'SHIFT_JIS') 33 | .toString('latin1'); 34 | let salt = `${encoded}H..` 35 | .substring(1, 3) 36 | .replace(/[^.-z]/g, '.'); 37 | for (let find in replace) { 38 | salt = salt.split(find).join(replace[find]); 39 | } 40 | const hashed = crypt(encoded, salt); 41 | return hashed.slice(-10); 42 | }, 43 | 44 | }; 45 | -------------------------------------------------------------------------------- /models/forms/addnews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { News } = require(__dirname+'/../../db/') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') 5 | , buildQueue = require(__dirname+'/../../lib/build/queue.js') 6 | , { prepareMarkdown } = require(__dirname+'/../../lib/post/markdown/markdown.js') 7 | , messageHandler = require(__dirname+'/../../lib/post/message.js'); 8 | 9 | module.exports = async (req, res) => { 10 | 11 | const { __ } = res.locals; 12 | const message = prepareMarkdown(req.body.message, false); 13 | const { message: markdownNews } = await messageHandler(message, null, null, res.locals.permissions); 14 | 15 | const post = { 16 | 'title': req.body.title, 17 | 'message': { 18 | 'raw': message, 19 | 'markdown': markdownNews 20 | }, 21 | 'date': new Date(), 22 | 'edited': null, 23 | }; 24 | 25 | await News.insertOne(post); 26 | 27 | buildQueue.push({ 28 | 'task': 'buildNews', 29 | 'options': {} 30 | }); 31 | 32 | return dynamicResponse(req, res, 200, 'message', { 33 | 'title': __('Success'), 34 | 'message': __('Added newspost'), 35 | 'redirect': '/globalmanage/news.html' 36 | }); 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /controllers/forms/deletenews.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deleteNews = require(__dirname+'/../../models/forms/deletenews.js') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') 5 | , paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js') 6 | , { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js'); 7 | 8 | module.exports = { 9 | 10 | paramConverter: paramConverter({ 11 | allowedArrays: ['checkednews'], 12 | objectIdArrays: ['checkednews'] 13 | }), 14 | 15 | controller: async (req, res, next) => { 16 | 17 | const { __ } = res.locals; 18 | 19 | const errors = await checkSchema([ 20 | { result: lengthBody(req.body.checkednews, 1), expected: false, error: __('Must select at least one newspost to delete') }, 21 | ]); 22 | 23 | if (errors.length > 0) { 24 | return dynamicResponse(req, res, 400, 'message', { 25 | 'title': __('Bad request'), 26 | 'errors': errors, 27 | 'redirect': '/globalmanage/news.html' 28 | }); 29 | } 30 | 31 | try { 32 | await deleteNews(req, res, next); 33 | } catch (err) { 34 | return next(err); 35 | } 36 | 37 | } 38 | 39 | }; 40 | -------------------------------------------------------------------------------- /models/forms/deleteassets.js: -------------------------------------------------------------------------------- 1 | 2 | 'use strict'; 3 | 4 | const { remove } = require('fs-extra') 5 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') 6 | , uploadDirectory = require(__dirname+'/../../lib/file/uploaddirectory.js') 7 | , { Boards } = require(__dirname+'/../../db/'); 8 | 9 | module.exports = async (req, res) => { 10 | 11 | const { __ } = res.locals; 12 | const redirect = `/${req.params.board}/manage/assets.html`; 13 | 14 | //delete file of all selected assets 15 | await Promise.all(req.body.checkedassets.map(async filename => { 16 | remove(`${uploadDirectory}/asset/${req.params.board}/${filename}`); 17 | })); 18 | 19 | //remove from db 20 | const amount = await Boards.removeAssets(req.params.board, req.body.checkedassets).then(result => result.modifiedCount); 21 | 22 | //update res locals assets in memory 23 | res.locals.board.assets = res.locals.board.assets.filter(asset => { 24 | return !req.body.checkedassets.includes(asset); 25 | }); 26 | 27 | return dynamicResponse(req, res, 200, 'message', { 28 | 'title': __('Success'), 29 | 'message': __('Deleted %s assets', amount), 30 | 'redirect': redirect 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /views/pages/globalmanagerecent.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/globalmanagenav.pug 3 | include ../mixins/post.pug 4 | 5 | block head 6 | title #{__('Recent Posts')} 7 | 8 | block content 9 | h1.board-title #{__('Global Management')} 10 | br 11 | .wrapbar 12 | +globalmanagenav('recent') 13 | if page === 1 && !ip 14 | .jsonly#livetext(data-view-raw-ip=(viewRawIp?'true':'false') data-room=`globalmanage-recent-${viewRawIp === true ? 'raw' : 'hashed'}`) 15 | .dot#livecolor 16 | | #{__('Connecting...')} 17 | input.postform-style.ml-5.di#updatepostsbutton(type='button' value=__('Update')) 18 | form(action=`/forms/global/actions` method='POST' enctype='application/x-www-form-urlencoded') 19 | input(type='hidden' name='_csrf' value=csrf) 20 | if posts.length === 0 21 | hr(size=1) 22 | p #{__('No posts.')} 23 | else 24 | hr(size=1) 25 | if ip 26 | h4.no-m-p #{__(`Global post history for %s`, ip)} 27 | hr(size=1) 28 | for p in posts 29 | .thread 30 | +post(p, false, false, true) 31 | hr(size=1) 32 | .pages.mv-5 33 | include ../includes/pages.pug 34 | include ../includes/actionfooter_globalmanage.pug 35 | -------------------------------------------------------------------------------- /gulp/res/css/themes/clear.css: -------------------------------------------------------------------------------- 1 | /*clear*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #f5f5f5; 5 | --alt-font-color: #333; 6 | --background-top: #EEE; 7 | --background-rest: #EEE; 8 | --navbar-color: #DDD; 9 | --post-color: #DDD; 10 | --post-outline-color: #d2d2d2; 11 | --label-color: #f5f5f5; 12 | --box-border-color: #e0e0e0; 13 | --darken: #0000000f; 14 | --highlighted-post-color: #EEDACB; 15 | --highlighted-post-outline-color: #d2d2d2; 16 | --board-title: maroon; 17 | --hr: #DDD; 18 | --font-color: #333; 19 | --name-color: #333; 20 | --capcode-color: #f00; 21 | --subject-color: #004A99; 22 | --link-color: black; 23 | --post-link-color: darkblue; 24 | --link-hover: #a74300; 25 | --accent-color: #cacaca; 26 | --input-borders: #d2d2d2; 27 | --input-color: #333; 28 | --input-background: #f5f5f5; 29 | --dice-color: maroon; 30 | --title-color: #d70000; 31 | --greentext-color: #789922; 32 | --pinktext-color:#E0727F; 33 | } 34 | .post-info, .post-container:target .post-info, .post-container.highlighted .post-info { 35 | background:none!important;border:none!important 36 | } 37 | -------------------------------------------------------------------------------- /views/pages/overboardcatalog.pug: -------------------------------------------------------------------------------- 1 | extends ../layout.pug 2 | include ../mixins/catalogtile.pug 3 | include ../mixins/overboardform.pug 4 | include ../mixins/announcements.pug 5 | 6 | block head 7 | title #{__('Overboard Catalog')} 8 | 9 | block content 10 | .board-header.mb-5 11 | h1.board-title #{__('Overboard Catalog')} 12 | h4.board-description #{__('Recently bumped threads from multiple boards')} 13 | | 14 | | ( 15 | a(href=`/overboard.html?${cacheQueryString}`) #{__('Index View')} 16 | | ) 17 | if allowCustomOverboard === true 18 | +overboardform('/catalog.html') 19 | +announcements() 20 | include ../includes/stickynav.pug 21 | .wrapbar 22 | .pages.jsonly 23 | input#catalogfilter(type='text' placeholder=__('Filter')) 24 | select.ml-5.right#catalogsort 25 | option(value="" disabled selected hidden) #{__('Sort By')} 26 | option(value="bump") #{__('Bump Order')} 27 | option(value="date") #{__('Creation Date')} 28 | option(value="replies") #{__('Reply Count')} 29 | hr(size=1) 30 | if threads.length === 0 31 | p #{__('No posts.')} 32 | else 33 | .catalog 34 | for thread, i in threads 35 | +catalogtile(thread, i, true) 36 | hr(size=1) 37 | -------------------------------------------------------------------------------- /controllers/forms/deletecustompage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const deleteCustomPage = require(__dirname+'/../../models/forms/deletecustompage.js') 4 | , dynamicResponse = require(__dirname+'/../../lib/misc/dynamic.js') 5 | , paramConverter = require(__dirname+'/../../lib/middleware/input/paramconverter.js') 6 | , { checkSchema, lengthBody } = require(__dirname+'/../../lib/input/schema.js'); 7 | 8 | module.exports = { 9 | 10 | paramConverter: paramConverter({ 11 | allowedArrays: ['checkedcustompages'], 12 | }), 13 | 14 | controller: async (req, res, next) => { 15 | 16 | const { __ } = res.locals; 17 | 18 | const errors = await checkSchema([ 19 | { result: lengthBody(req.body.checkedcustompages, 1), expected: false, error: __('Must select at least one custom page to delete') }, 20 | ]); 21 | 22 | if (errors.length > 0) { 23 | return dynamicResponse(req, res, 400, 'message', { 24 | 'title': __('Bad request'), 25 | 'errors': errors, 26 | 'redirect': `/${req.params.board}/manage/custompages.html` 27 | }); 28 | } 29 | 30 | try { 31 | await deleteCustomPage(req, res, next); 32 | } catch (err) { 33 | return next(err); 34 | } 35 | 36 | } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /gulp/res/js/settings.js: -------------------------------------------------------------------------------- 1 | /* globals modal themes codeThemes */ 2 | window.addEventListener('DOMContentLoaded', () => { 3 | 4 | let settingsModal; 5 | let settingsBg; 6 | 7 | const hideSettings = () => { 8 | settingsModal.style.display = 'none'; 9 | settingsBg.style.display = 'none'; 10 | }; 11 | 12 | const openSettings = () => { 13 | settingsModal.style.display = 'unset'; 14 | settingsBg.style.display = 'unset'; 15 | }; 16 | 17 | const modalHtml = modal({ 18 | modal: { 19 | title: 'Settings', 20 | settings: { 21 | themes, 22 | codeThemes, 23 | }, 24 | hidden: true, 25 | } 26 | }); 27 | 28 | document.body.insertAdjacentHTML('afterbegin', modalHtml); 29 | settingsBg = document.getElementsByClassName('modal-bg')[0]; 30 | settingsModal = document.getElementsByClassName('modal')[0]; 31 | 32 | settingsBg.onclick = hideSettings; 33 | settingsModal.getElementsByClassName('close')[0].onclick = hideSettings; 34 | 35 | const settings = document.getElementById('settings'); 36 | if (settings) { //can be false if we are in minimal view 37 | settings.onclick = openSettings; 38 | } 39 | 40 | window.dispatchEvent(new CustomEvent('settingsReady')); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /gulp/res/css/themes/solarized-dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%); 3 | --alt-label-color: #800; 4 | --alt-font-color: #fff; 5 | --background-top: #002b36; 6 | --background-rest: #002b36; 7 | --navbar-color: #002b36; 8 | --post-color: #073642; 9 | --post-outline-color: #586e75; 10 | --label-color: #073642; 11 | --box-border-color: #586e75; 12 | --darken: #00000010; 13 | --highlighted-post-color: #002b36; 14 | --highlighted-post-outline-color: #cb4b16; 15 | --board-title: #dc322f; 16 | --hr: #93a1a1; 17 | --font-color: #839496; 18 | --name-color: #b58900; 19 | --capcode-color: #f00; 20 | --subject-color: #b58900; 21 | --link-color: #839496; 22 | --post-link-color: #268bd2; 23 | --link-hover: #2aa198; 24 | --accent-color: #073642; 25 | --input-borders: #586e75; 26 | --input-color: #839496; 27 | --input-background: #002b36; 28 | --dice-color: #D33682; 29 | --title-color: #dc322f; 30 | --greentext-color: #789922; 31 | --pinktext-color: #d33682; 32 | } 33 | .bold { 34 | color: #93a1a1; 35 | } 36 | .em { 37 | color: #586e75; 38 | } 39 | img[src='/file/dice.png'] { 40 | filter: hue-rotate(290deg) 41 | } 42 | -------------------------------------------------------------------------------- /gulp/res/css/themes/yotsuba-b.css: -------------------------------------------------------------------------------- 1 | /*yotsuba b*/ 2 | :root { 3 | --icon-color:invert(17%)sepia(89%)saturate(7057%)hue-rotate(2deg)brightness(93%)contrast(120%); 4 | --alt-label-color: #98E; 5 | --alt-font-color: black; 6 | --background-top: #d6daf0; 7 | --background-rest: #eef2ff; 8 | --navbar-color: #D6DAF0; 9 | --post-color: #D6DAF0; 10 | --post-outline-color: #B7C5D9; 11 | --label-color: #98E; 12 | --box-border-color: #000; 13 | --darken: #00000010; 14 | --highlighted-post-color: #d6bad0; 15 | --highlighted-post-outline-color: #ba9dbf; 16 | --board-title: #af0a0f; 17 | --hr: lightgray; 18 | --font-color: black; 19 | --name-color: #117743; 20 | --capcode-color: #f00; 21 | --subject-color: #0F0C5D; 22 | --link-color: #34345C; 23 | --post-link-color: #d00; 24 | --link-hover: #d00; 25 | --input-borders: #a9a9a9; 26 | --input-color: #000; 27 | --input-background: white; 28 | --dice-color: darkorange; 29 | --title-color: #d70000; 30 | --greentext-color: green; 31 | --pinktext-color:#E0727F; 32 | } 33 | 34 | .post-container, #float .post-container, .stickynav, .pages, #action-menu, .catalog-tile, #livetext, #threadstats, .collapse, .bottom-reply { 35 | border-width: 0 1px 1px 0; 36 | } 37 | -------------------------------------------------------------------------------- /gulp/res/css/themes/solarized-light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --icon-color: invert(17%) sepia(89%) saturate(7057%) hue-rotate(2deg) brightness(93%) contrast(120%); 3 | --alt-label-color: #EEE8D5; 4 | --alt-font-color: #657B83; 5 | --background-top: #FDF6E3; 6 | --background-rest: #FDF6E3; 7 | --navbar-color: #EEE8D5; 8 | --post-color: #EEE8D5; 9 | --post-outline-color: #93A1A1; 10 | --label-color: #EEE8D5; 11 | --box-border-color: #93A1A1; 12 | --darken: #00000010; 13 | --highlighted-post-color: #FDF6E3; 14 | --highlighted-post-outline-color: #cb4b16; 15 | --board-title: #dc322f; 16 | --hr: #93a1a1; 17 | --font-color: #657B83; 18 | --name-color: #b58900; 19 | --capcode-color: #f00; 20 | --subject-color: #b58900; 21 | --link-color: #657B83; 22 | --post-link-color: #268bd2; 23 | --link-hover: #2aa198; 24 | --accent-color: #EEE8D5; 25 | --input-borders: #93A1A1; 26 | --input-color: #657B83; 27 | --input-background: #FDF6E3; 28 | --dice-color: #D33682; 29 | --title-color: #dc322f; 30 | --greentext-color: #789922; 31 | --pinktext-color: #d33682; 32 | } 33 | .bold { 34 | color: #93a1a1; 35 | } 36 | .em { 37 | color: #586e75; 38 | } 39 | img[src='/file/dice.png'] { 40 | filter: hue-rotate(290deg) 41 | } 42 | -------------------------------------------------------------------------------- /tools/example_request.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Send an authenticated request to jschan 4 | # 5 | 6 | # Variables, change me 7 | DOMAIN="your.ib" 8 | USERNAME="username" 9 | PASSWORD="password" 10 | FORM_PATH="/forms/whatever" 11 | FORM_DATA="something=ABC&something_else=123" 12 | 13 | # Login 14 | LOGIN_REQUEST=$(curl -si "https://$DOMAIN/forms/login" \ 15 | -H "Referer: https://$DOMAIN" \ 16 | -H "x-using-xhr: 1" \ 17 | --data "username=$USERNAME" \ 18 | --data "password=$PASSWORD") 19 | RESPONSE_CODE=$(echo "$LOGIN_REQUEST" | grep HTTP | awk '{print $2}') 20 | [[ "$RESPONSE_CODE" != "302" ]] && echo "Login failed" && exit; 21 | SESSION_COOKIE=$(echo "$LOGIN_REQUEST" | grep 'set-cookie' | awk '{print $2}') 22 | 23 | # Get CSRF token 24 | CSRF_REQUEST=$(curl -s "https://$DOMAIN/csrf.json" \ 25 | -H "Cookie: $SESSION_COOKIE" \ 26 | -H "Referer: https://$DOMAIN" \ 27 | -H "x-using-xhr: 1") 28 | CSRF_TOKEN=$(echo "$CSRF_REQUEST" | jq -j '.token') 29 | 30 | # Send authed request 31 | ANNOUNCEMENT_REQUEST=$(curl -si "https://$DOMAIN$FORM_PATH" \ 32 | -H "Cookie: $SESSION_COOKIE" \ 33 | -H "Referer: https://$DOMAIN" \ 34 | -H "x-using-xhr: 1" \ 35 | --data "_csrf=$CSRF_TOKEN" \ 36 | --data "$FORM_DATA") 37 | --------------------------------------------------------------------------------