diff --git a/configs/main.js.example b/configs/main.js.example index d438ccd5..1b4ba425 100644 --- a/configs/main.js.example +++ b/configs/main.js.example @@ -130,6 +130,9 @@ module.exports = { //how many replies a thread needs to not get removed by early404 early404Replies: 5, + //how many of the most recent newsposts to show on the homepage + maxRecentNews: 3, + /* filter filenames on posts and banners false=no filtering true=allow only A-Za-z0-9_- diff --git a/db/news.js b/db/news.js index 288f786e..8b2d3a3a 100644 --- a/db/news.js +++ b/db/news.js @@ -8,10 +8,12 @@ module.exports = { db, - find: () => { + find: (limit=0) => { return db.find({}).sort({ '_id': -1 - }).toArray(); + }) + .limit(limit) + .toArray(); }, insertOne: (news) => { diff --git a/gulp/res/css/style.css b/gulp/res/css/style.css index 49023e6d..0cefd12a 100644 --- a/gulp/res/css/style.css +++ b/gulp/res/css/style.css @@ -795,6 +795,18 @@ input:invalid, textarea:invalid { line-height: 3em; } +a.button { + -webkit-appearance: button; + -moz-appearance: button; + appearance: button; + text-decoration: none; + color: initial; +} + +a.button:hover { + color: initial!important; +} + input[type="button"][disabled] { opacity: 0.5; } @@ -1040,6 +1052,10 @@ table, .boardtable { display: none; } + table.newstable td:nth-child(2), table.newstable th:nth-child(2) { + display: none; + } + .modal { top: 5px; max-height: calc(100% - 10px); diff --git a/gulp/res/js/forms.js b/gulp/res/js/forms.js index 8337f069..026ff25b 100644 --- a/gulp/res/js/forms.js +++ b/gulp/res/js/forms.js @@ -6,7 +6,6 @@ function removeModal() { function doModal(data) { const modalHtml = modal({ modal: data }); document.body.insertAdjacentHTML('afterbegin', modalHtml); - new formHandler(document.getElementsByClassName('modal')[0].querySelector('form')); document.getElementById('modalclose').onclick = removeModal; document.getElementsByClassName('modal-bg')[0].onclick = removeModal; } @@ -19,7 +18,7 @@ function isCheckBox(element) { function formToJSON(form) { const data = {}; for (element of form.elements) { - if (element.name && element.value && (!isCheckBox(element) || element.checked)) { + if (element.name /*&& element.value*/ && (!isCheckBox(element) || element.checked)) { if (isCheckBox(element) && data[element.name]) { if (Array.isArray(data[element.name])) { data[element.name] = data[element.name].push(element.value); @@ -38,6 +37,7 @@ class formHandler { constructor(form) { this.form = form; + this.enctype = this.form.getAttribute('enctype'); this.messageBox = form.querySelector('#message') this.submit = form.querySelector('input[type="submit"]') this.originalSubmitText = this.submit.value; @@ -58,8 +58,9 @@ class formHandler { } formSubmit(e) { + const xhr = new XMLHttpRequest(); let postData; - if (this.form.getAttribute('enctype') === 'multipart/form-data') { + if (this.enctype === 'multipart/form-data') { this.fileInput.disabled = true; //palemoon is dumb, so append them instead postData = new FormData(this.form); this.fileInput.disabled = false; @@ -70,8 +71,7 @@ class formHandler { } } } else { - xhr.setRequestHeader('Content-Type', 'application/json'); - postData = formToJSON(this.form); + postData = new URLSearchParams([...(new FormData(this.form))]); } if (this.banned) { return true; @@ -79,7 +79,6 @@ class formHandler { e.preventDefault(); } this.submit.disabled = true; - const xhr = new XMLHttpRequest(); if (this.files && this.files.length > 0) { //show progress on file uploads xhr.onloadstart = () => { @@ -100,13 +99,14 @@ class formHandler { if (xhr.responseText) { try { json = JSON.parse(xhr.responseText); +console.log(json) } catch (e) { //wasnt json response } } if (xhr.status == 200) { if (!json) { - if (xhr.responseURL + if (xhr.responseURL && xhr.responseURL !== `${location.origin}${this.form.getAttribute('action')}`) { window.location = xhr.responseURL; return; @@ -114,7 +114,9 @@ class formHandler { //todo: show success messages nicely for forms like actions (this doesnt apply to non file forms yet) } } else { - if (socket && socket.connected) { + if (json.message || json.messages || json.error || json.errors) { + doModal(json); + } else if (socket && socket.connected) { window.myPostId = json.postId; window.location.hash = json.postId } else { @@ -123,7 +125,6 @@ class formHandler { } setLocalStorage('myPostId', json.postId); forceUpdate(); -// window.location.reload(); } } this.form.reset(); @@ -139,11 +140,10 @@ class formHandler { if (xhr.status === 413) { this.clearFiles(); } - //not 200 status, so some error/failed post, wrong captcha, etc if (json) { doModal(json); } else { -//for bans, post form to show TODO: make modal support bans json and send dynamicresponse from it +//for bans, post form to show TODO: make modal support bans json and send dynamicresponse from it (but what about appeals, w/ captcha, etc?) this.clearFiles(); //dont resubmit files this.banned = true; this.form.dispatchEvent(new Event('submit')); @@ -165,6 +165,9 @@ class formHandler { if (isLive) { xhr.setRequestHeader('x-using-live', true); } + if (this.enctype !== 'multipart/form-data') { + xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + } xhr.send(postData); } @@ -200,6 +203,9 @@ class formHandler { //remove all files from this form clearFiles() { + if (!this.fileInput) { + return; + } this.files = []; //empty file list this.fileInput.value = null; //remove the files for real if (this.fileRequired) { //reset to required if clearing files @@ -284,9 +290,7 @@ window.addEventListener('settingsReady', () => { const forms = document.getElementsByTagName('form'); for(let i = 0; i < forms.length; i++) { - if (forms[i].method === 'post' - && forms[i].encoding === 'multipart/form-data') { - //used only for file posting forms currently. + if (forms[i].method === 'post' /*&& forms[i].encoding === 'multipart/form-data'*/) { new formHandler(forms[i]); } } diff --git a/gulp/res/js/modal.js b/gulp/res/js/modal.js index 06face9f..bb68ab7c 100644 --- a/gulp/res/js/modal.js +++ b/gulp/res/js/modal.js @@ -55,7 +55,7 @@ pug_html = pug_html + "\u003Cli\u003E" + (pug_escape(null == (pug_interp = error } pug_html = pug_html + "\u003C\u002Ful\u003E\u003C\u002Fdiv\u003E"; if (data.link) { -pug_html = pug_html + "\u003Cdiv class=\"row\"\u003E\u003Ca" + (pug_attr("href", data.link.href, true, false)+" target=\"_blank\"") + "\u003E" + (pug_escape(null == (pug_interp = data.link.text) ? "" : pug_interp)) + "\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E"; +pug_html = pug_html + "\u003Cdiv class=\"row\"\u003E\u003Ca" + (" class=\"button mv-0\""+pug_attr("href", data.link.href, true, false)+" target=\"_blank\"") + "\u003E" + (pug_escape(null == (pug_interp = data.link.text) ? "" : pug_interp)) + "\u003C\u002Fa\u003E\u003C\u002Fdiv\u003E"; } } else diff --git a/gulp/res/js/themelist.js b/gulp/res/js/themelist.js index d44973e4..bc68d799 100644 --- a/gulp/res/js/themelist.js +++ b/gulp/res/js/themelist.js @@ -1 +1 @@ -const themes = ['choc', 'clear', 'darkblue', 'lain', 'pink', 'tomorrow', 'yotsuba b', 'yotsuba'];const codeThemes = ['a11y-dark', 'a11y-light', 'agate', 'an-old-hope', 'androidstudio', 'arduino-light', 'arta', 'ascetic', 'atelier-cave-dark', 'atelier-cave-light', 'atelier-dune-dark', 'atelier-dune-light', 'atelier-estuary-dark', 'atelier-estuary-light', 'atelier-forest-dark', 'atelier-forest-light', 'atelier-heath-dark', 'atelier-heath-light', 'atelier-lakeside-dark', 'atelier-lakeside-light', 'atelier-plateau-dark', 'atelier-plateau-light', 'atelier-savanna-dark', 'atelier-savanna-light', 'atelier-seaside-dark', 'atelier-seaside-light', 'atelier-sulphurpool-dark', 'atelier-sulphurpool-light', 'atom-one-dark-reasonable', 'atom-one-dark', 'atom-one-light', 'brown-paper', 'brown-papersq', 'codepen-embed', 'color-brewer', 'darcula', 'dark', 'darkula', 'default', 'docco', 'dracula', 'far', 'foundation', 'github-gist', 'github', 'gml', 'googlecode', 'gradient-dark', 'grayscale', 'gruvbox-dark', 'gruvbox-light', 'hopscotch', 'hybrid', 'idea', 'ir-black', 'isbl-editor-dark', 'isbl-editor-light', 'kimbie.dark', 'kimbie.light', 'lightfair', 'magula', 'mono-blue', 'monokai-sublime', 'monokai', 'night-owl', 'nord', 'obsidian', 'ocean', 'paraiso-dark', 'paraiso-light', 'pojoaque', 'pojoaque', 'purebasic', 'qtcreator_dark', 'qtcreator_light', 'railscasts', 'rainbow', 'routeros', 'school-book', 'school-book', 'shades-of-purple', 'solarized-dark', 'solarized-light', 'sunburst', 'tomorrow-night-blue', 'tomorrow-night-bright', 'tomorrow-night-eighties', 'tomorrow-night', 'tomorrow', 'vs', 'vs2015', 'xcode', 'xt256', 'zenburn']; \ No newline at end of file +const themes = ['clear', 'tomorrow', 'lain'];const codeThemes = ['a11y-dark', 'a11y-light', 'agate', 'an-old-hope', 'androidstudio', 'arduino-light', 'arta', 'ascetic', 'atelier-cave-dark', 'atelier-cave-light', 'atelier-dune-dark', 'atelier-dune-light', 'atelier-estuary-dark', 'atelier-estuary-light', 'atelier-forest-dark', 'atelier-forest-light', 'atelier-heath-dark', 'atelier-heath-light', 'atelier-lakeside-dark', 'atelier-lakeside-light', 'atelier-plateau-dark', 'atelier-plateau-light', 'atelier-savanna-dark', 'atelier-savanna-light', 'atelier-seaside-dark', 'atelier-seaside-light', 'atelier-sulphurpool-dark', 'atelier-sulphurpool-light', 'atom-one-dark-reasonable', 'atom-one-dark', 'atom-one-light', 'brown-paper', 'brown-papersq', 'codepen-embed', 'color-brewer', 'darcula', 'dark', 'darkula', 'default', 'docco', 'dracula', 'far', 'foundation', 'github-gist', 'github', 'gml', 'googlecode', 'gradient-dark', 'grayscale', 'gruvbox-dark', 'gruvbox-light', 'hopscotch', 'hybrid', 'idea', 'ir-black', 'isbl-editor-dark', 'isbl-editor-light', 'kimbie.dark', 'kimbie.light', 'lightfair', 'magula', 'mono-blue', 'monokai-sublime', 'monokai', 'night-owl', 'nord', 'obsidian', 'ocean', 'paraiso-dark', 'paraiso-light', 'pojoaque', 'pojoaque', 'purebasic', 'qtcreator_dark', 'qtcreator_light', 'railscasts', 'rainbow', 'routeros', 'school-book', 'school-book', 'shades-of-purple', 'solarized-dark', 'solarized-light', 'sunburst', 'tomorrow-night-blue', 'tomorrow-night-bright', 'tomorrow-night-eighties', 'tomorrow-night', 'tomorrow', 'vs', 'vs2015', 'xcode', 'xt256', 'zenburn']; \ No newline at end of file diff --git a/gulp/res/js/time.js b/gulp/res/js/time.js index 96c6c813..4dbfadca 100644 --- a/gulp/res/js/time.js +++ b/gulp/res/js/time.js @@ -52,7 +52,10 @@ const relativeTimeString = (date) => { } const changeDateFormat = (date) => { - const options = { hourCycle: hour24 ? 'h23' : 'h12' }; + const options = { + hourCycle: hour24 ? 'h23' : 'h12', + hour12: !hour24 + }; if (!localTime) { options.timeZone = SERVER_TIMEZONE; } diff --git a/helpers/checks/bancheck.js b/helpers/checks/bancheck.js index 420c5aa8..966e3f6b 100644 --- a/helpers/checks/bancheck.js +++ b/helpers/checks/bancheck.js @@ -1,7 +1,8 @@ 'use strict'; const { Bans } = require(__dirname+'/../../db/') - , hasPerms = require(__dirname+'/hasperms.js'); + , hasPerms = require(__dirname+'/hasperms.js') + , dynamicResponse = require(__dirname+'/../dynamic.js'); module.exports = async (req, res, next) => { @@ -14,7 +15,7 @@ module.exports = async (req, res, next) => { const unseenBans = bans.filter(b => !b.seen).map(b => b._id); await Bans.markSeen(unseenBans); //mark bans as seen bans.forEach(ban => ban.seen = true); //mark seen as true in memory for user viewed ban page - return dynamicResponse(req, res, 403, 'message', { + return res.status(403).render('ban', { bans: bans, }); } diff --git a/helpers/processip.js b/helpers/processip.js index 6086fc95..016fb477 100644 --- a/helpers/processip.js +++ b/helpers/processip.js @@ -5,7 +5,6 @@ const { ipHashMode } = require(__dirname+'/../configs/main.js') , hashIp = require(__dirname+'/haship.js'); module.exports = (req, res, next) => { - const ip = req.headers['x-real-ip'] || req.connection.remoteAddress; //need to consider forwarded-for, etc here and in nginx const ipVersion = isIP(ip); if (ipVersion) { diff --git a/helpers/tasks.js b/helpers/tasks.js index 01f560bf..e1bc867e 100644 --- a/helpers/tasks.js +++ b/helpers/tasks.js @@ -5,7 +5,7 @@ const Mongo = require(__dirname+'/../db/db.js') , timeUtils = require(__dirname+'/timeutils.js') , uploadDirectory = require(__dirname+'/files/uploadDirectory.js') , { remove } = require('fs-extra') - , { debugLogs, pruneModlogs, pruneAfterDays, enableWebring } = require(__dirname+'/../configs/main.js') + , { debugLogs, pruneModlogs, pruneAfterDays, enableWebring, maxRecentNews } = require(__dirname+'/../configs/main.js') , { Stats, Posts, Files, Boards, News, Modlogs } = require(__dirname+'/../db/') , render = require(__dirname+'/render.js') , timeDiffString = require(__dirname+'/timediffstring.js'); @@ -195,15 +195,17 @@ module.exports = { buildHomepage: async () => { const label = '/index.html'; const start = process.hrtime(); - let [ totalStats, boards, fileStats ] = await Promise.all([ + let [ totalStats, boards, fileStats, recentNews ] = await Promise.all([ Boards.totalStats(), //overall total posts ever made Boards.boardSort(0, 20), //top 20 boards sorted by users, pph, total posts - Files.activeContent() //size ans number of files + Files.activeContent(), //size ans number of files + News.find(maxRecentNews), //some recent newsposts ]); const html = await render('index.html', 'home.pug', { totalStats, boards, fileStats, + recentNews, }); const end = process.hrtime(start); debugLogs && console.log(timeDiffString(label, end)); diff --git a/server.js b/server.js index b288bce2..8a781518 100644 --- a/server.js +++ b/server.js @@ -44,7 +44,6 @@ const express = require('express') app.disable('x-powered-by'); // parse forms app.use(express.urlencoded({extended: false})); - //app.use(express.json()); //unused atm, will be used with forms.js eventually // parse cookies app.use(cookieParser()); @@ -105,7 +104,7 @@ const express = require('express') // catch any unhandled errors app.use((err, req, res, next) => { if (err.code === 'EBADCSRFTOKEN') { - return res.status(403).render('message', { + return dynamicResponse(req, res, 403, 'message', { 'title': 'Forbidden', 'message': 'Invalid CSRF token' }); diff --git a/views/mixins/modal.pug b/views/mixins/modal.pug index 12849929..12f61956 100644 --- a/views/mixins/modal.pug +++ b/views/mixins/modal.pug @@ -19,9 +19,7 @@ mixin modal(data) li #{error} if data.link .row - a(href=data.link.href target='_blank') #{data.link.text} - else if data.bans - include ../includes/banform.pug + a.button.mv-0(href=data.link.href target='_blank') #{data.link.text} else if data.settings .row .form-wrapper.flexleft.mt-10 diff --git a/views/pages/home.pug b/views/pages/home.pug index 0b74b9b9..ac220a4b 100644 --- a/views/pages/home.pug +++ b/views/pages/home.pug @@ -16,6 +16,22 @@ block content | This is an anonymous imageboard, a type of BBS where anyone can post messages and share images. | You don't need to register or provide any personal information to make a post. | Choose a board below to join the discussion, or #[a(href='/create.html') create your own]. + if recentNews && recentNews.length > 0 + .table-container.flex-center.mv-5 + table.newstable + tr + th(colspan=3) + a(href='/news.html') Latest News + each post in recentNews + tr + td + a.left(href=`/news.html#${post._id}`) #{post.title} + td + p.no-m-p #{`${post.message.raw.substring(0,50)}${post.message.raw.length > 50 ? '...' : ''}`} + td + - const newsDate = new Date(post.date); + time.right.reltime(datetime=newsDate.toISOString()) #{newsDate.toLocaleString(undefined, {hour12:false})} + if boards && boards.length > 0 include ../includes/boardtable.pug each board in boards diff --git a/views/pages/message.pug b/views/pages/message.pug index 60ea3791..8755f9e9 100644 --- a/views/pages/message.pug +++ b/views/pages/message.pug @@ -21,6 +21,6 @@ block content li #{error} if link div - a(href=link.href target='_blank') #{link.text} + a.button(href=link.href target='_blank') #{link.text} if redirect p You will be redirected shortly. If you are not redirected automatically, you can #[a(href=redirect) click here]. diff --git a/views/pages/thread.pug b/views/pages/thread.pug index fe11bafd..acbf785d 100644 --- a/views/pages/thread.pug +++ b/views/pages/thread.pug @@ -6,7 +6,7 @@ include ../mixins/boardheader.pug block head script(src='/js/all.js') - - const subjectString = thread.subject || thread.nomarkup ? `${thread.nomarkup.substring(0, globalLimits.fieldLength.subject)}${thread.nomarkup.length > globalLimits.fieldLength.subject ? '...' : ''}` : thread.postId; + - const subjectString = thread.subject || (thread.nomarkup ? `${thread.nomarkup.substring(0, globalLimits.fieldLength.subject)}${thread.nomarkup.length > globalLimits.fieldLength.subject ? '...' : ''}` : thread.postId); title /#{board._id}/ - #{subjectString} if !modview meta(property='og:site_name', value=meta.siteName)