├── 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 |
--------------------------------------------------------------------------------