diff --git a/Dockerfile b/Dockerfile index 80dce7cfb5..8a5b7ae9bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,4 +22,4 @@ ENV NODE_ENV=production \ EXPOSE 4567 -CMD node ./nodebb build ; node ./nodebb start +CMD test -n "${SETUP}" && ./nodebb setup || node ./nodebb build; node ./nodebb start diff --git a/install/package.json b/install/package.json index 2344c516f5..95d2089d19 100644 --- a/install/package.json +++ b/install/package.json @@ -89,10 +89,10 @@ "nodebb-plugin-2factor": "3.0.4", "nodebb-plugin-composer-default": "7.0.20", "nodebb-plugin-dbsearch": "5.1.1", - "nodebb-plugin-emoji": "3.5.12", + "nodebb-plugin-emoji": "3.5.16", "nodebb-plugin-emoji-android": "2.0.5", - "nodebb-plugin-markdown": "9.0.6", - "nodebb-plugin-mentions": "3.0.4", + "nodebb-plugin-markdown": "9.0.7", + "nodebb-plugin-mentions": "3.0.5", "nodebb-plugin-spam-be-gone": "0.7.13", "nodebb-rewards-essentials": "0.2.1", "nodebb-theme-lavender": "5.3.2", diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index b3d89ee254..170a30782f 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -436,7 +436,7 @@ ajaxify.widgets = { render: render }; ajaxify.loadTemplate = function (template, callback) { $.ajax({ - url: `${config.assetBaseUrl}/templates/${template}.js`, + url: `${config.asset_base_url}/templates/${template}.js`, dataType: 'text', success: function (script) { var context = { diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index d146df51f8..b2e51ab4cf 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -5,7 +5,7 @@ const factory = require('./translator.common'); define('translator', ['jquery', 'utils'], function (jQuery, utils) { function loadClient(language, namespace) { return new Promise(function (resolve, reject) { - jQuery.getJSON([config.assetBaseUrl, 'language', language, namespace].join('/') + '.json?' + config['cache-buster'], function (data) { + jQuery.getJSON([config.asset_base_url, 'language', language, namespace].join('/') + '.json?' + config['cache-buster'], function (data) { const payload = { language: language, namespace: namespace, diff --git a/src/controllers/api.js b/src/controllers/api.js index 1194a75399..7474f6e7a0 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -14,6 +14,7 @@ const apiController = module.exports; const relative_path = nconf.get('relative_path'); const upload_url = nconf.get('upload_url'); +const asset_base_url = nconf.get('asset_base_url'); const socketioTransports = nconf.get('socket.io:transports') || ['polling', 'websocket']; const socketioOrigins = nconf.get('socket.io:origins'); const websocketAddress = nconf.get('socket.io:address') || ''; @@ -22,7 +23,8 @@ apiController.loadConfig = async function (req) { const config = { relative_path, upload_url, - assetBaseUrl: `${relative_path}/assets`, + asset_base_url, + assetBaseUrl: asset_base_url, // deprecate in 1.20.x siteTitle: validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')), browserTitle: validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')), titleLayout: (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'), diff --git a/src/install.js b/src/install.js index 3067a05ab5..f9f7912d9f 100644 --- a/src/install.js +++ b/src/install.js @@ -46,22 +46,54 @@ questions.optional = [ }, ]; -function checkSetupFlag() { - let setupVal = install.values; +function checkSetupFlagEnv() { + let setupVal = install.values || {}; + const envConfMap = { + NODEBB_URL: 'url', + NODEBB_PORT: 'port', + NODEBB_ADMIN_USERNAME: 'admin:username', + NODEBB_ADMIN_PASSWORD: 'admin:password', + NODEBB_ADMIN_EMAIL: 'admin:email', + NODEBB_DB: 'database', + NODEBB_DB_HOST: 'host', + NODEBB_DB_PORT: 'port', + NODEBB_DB_USER: 'username', + NODEBB_DB_PASSWORD: 'password', + NODEBB_DB_NAME: 'database', + NODEBB_DB_SSL: 'ssl', + }; + + // Set setup values from env vars (if set) + winston.info('[install/checkSetupFlagEnv] checking env vars for setup info...'); + + Object.entries(process.env).forEach(([evName, evValue]) => { // get setup values from env + if (evName.startsWith('NODEBB_DB_')) { + setupVal[`${process.env.NODEBB_DB}:${envConfMap[evName]}`] = evValue; + } else if (evName.startsWith('NODEBB_')) { + setupVal[envConfMap[evName]] = evValue; + } + }); + + setupVal['admin:password:confirm'] = setupVal['admin:password']; + + // try to get setup values from json, if successful this overwrites all values set by env + // TODO: better behaviour would be to support overrides per value, i.e. in order of priority (generic pattern): + // flag, env, config file, default try { if (nconf.get('setup')) { - setupVal = JSON.parse(nconf.get('setup')); + const setupJSON = JSON.parse(nconf.get('setup')); + setupVal = { ...setupVal, ...setupJSON }; } } catch (err) { - winston.error('Invalid json in nconf.get(\'setup\'), ignoring setup values'); + winston.error('[install/checkSetupFlagEnv] invalid json in nconf.get(\'setup\'), ignoring setup values from json'); } if (setupVal && typeof setupVal === 'object') { if (setupVal['admin:username'] && setupVal['admin:password'] && setupVal['admin:password:confirm'] && setupVal['admin:email']) { install.values = setupVal; } else { - winston.error('Required values are missing for automated setup:'); + winston.error('[install/checkSetupFlagEnv] required values are missing for automated setup:'); if (!setupVal['admin:username']) { winston.error(' admin:username'); } @@ -95,7 +127,7 @@ function checkCIFlag() { if (ciVals.hasOwnProperty('host') && ciVals.hasOwnProperty('port') && ciVals.hasOwnProperty('database')) { install.ciVals = ciVals; } else { - winston.error('Required values are missing for automated CI integration:'); + winston.error('[install/checkCIFlag] required values are missing for automated CI integration:'); if (!ciVals.hasOwnProperty('host')) { winston.error(' host'); } @@ -521,7 +553,7 @@ async function checkUpgrade() { install.setup = async function () { try { - checkSetupFlag(); + checkSetupFlagEnv(); checkCIFlag(); await setupConfig(); await setupDefaultConfigs(); diff --git a/src/prestart.js b/src/prestart.js index 55dcb72235..b93bf05838 100644 --- a/src/prestart.js +++ b/src/prestart.js @@ -95,6 +95,7 @@ function loadConfig(configFile) { nconf.set('secure', urlObject.protocol === 'https:'); nconf.set('use_port', !!urlObject.port); nconf.set('relative_path', relativePath); + nconf.set('asset_base_url', `${relativePath}/assets`); nconf.set('port', nconf.get('PORT') || nconf.get('port') || urlObject.port || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); // cookies don't provide isolation by port: http://stackoverflow.com/a/16328399/122353 diff --git a/src/upgrades/1.19.3/fix_user_uploads_zset.js b/src/upgrades/1.19.3/fix_user_uploads_zset.js index 155697bc6d..e5989ba75d 100644 --- a/src/upgrades/1.19.3/fix_user_uploads_zset.js +++ b/src/upgrades/1.19.3/fix_user_uploads_zset.js @@ -1,3 +1,5 @@ +/* eslint-disable no-await-in-loop */ + 'use strict'; const crypto = require('crypto'); @@ -14,13 +16,11 @@ module.exports = { const { progress } = this; await batch.processSortedSet('users:joindate', async (uids) => { - let keys = uids.map(uid => `uid:${uid}:uploads`); - const exists = await db.exists(keys); - keys = keys.filter((key, idx) => exists[idx]); + const keys = uids.map(uid => `uid:${uid}:uploads`); + progress.incr(uids.length); - progress.incr(uids.length - keys.length); - - await Promise.all(keys.map(async (key, idx) => { + for (let idx = 0; idx < uids.length; idx++) { + const key = keys[idx]; // Rename the paths within let uploads = await db.getSortedSetRangeWithScores(key, 0, -1); @@ -37,9 +37,7 @@ module.exports = { // Add uid to the upload's hash object uploads = await db.getSortedSetMembers(key); await db.setObjectBulk(uploads.map(relativePath => [`upload:${md5(relativePath)}`, { uid: uids[idx] }])); - - progress.incr(); - })); + } }, { batch: 100, progress: progress, diff --git a/src/upgrades/1.19.3/rename_post_upload_hashes.js b/src/upgrades/1.19.3/rename_post_upload_hashes.js index 5e702c39ee..5664243c3f 100644 --- a/src/upgrades/1.19.3/rename_post_upload_hashes.js +++ b/src/upgrades/1.19.3/rename_post_upload_hashes.js @@ -1,3 +1,5 @@ +/* eslint-disable no-await-in-loop */ + 'use strict'; const crypto = require('crypto'); @@ -18,14 +20,14 @@ module.exports = { const exists = await db.exists(keys); keys = keys.filter((key, idx) => exists[idx]); - progress.incr(pids.length - keys.length); + progress.incr(pids.length); - await Promise.all(keys.map(async (key) => { + for (const key of keys) { // Rename the paths within let uploads = await db.getSortedSetRangeWithScores(key, 0, -1); // Don't process those that have already the right format - uploads = uploads.filter(upload => !upload.value.startsWith('files/')); + uploads = uploads.filter(upload => upload && upload.value && !upload.value.startsWith('files/')); // Rename the zset members await db.sortedSetRemove(key, uploads.map(upload => upload.value)); @@ -38,12 +40,21 @@ module.exports = { // Rename the object and pids zsets const hashes = uploads.map(upload => md5(upload.value)); const newHashes = uploads.map(upload => md5(`files/${upload.value}`)); - const promises = hashes.map((hash, idx) => db.rename(`upload:${hash}`, `upload:${newHashes[idx]}`)); - promises.concat(hashes.map((hash, idx) => db.rename(`upload:${hash}:pids`, `upload:${newHashes[idx]}:pids`))); - await Promise.all(promises); - progress.incr(); - })); + // cant use db.rename since `fix_user_uploads_zset.js` upgrade script already creates + // `upload:md5(upload.value) hash, trying to rename to existing key results in dupe error + const oldData = await db.getObjects(hashes.map(hash => `upload:${hash}`)); + const bulkSet = []; + oldData.forEach((data, idx) => { + if (data) { + bulkSet.push([`upload:${newHashes[idx]}`, data]); + } + }); + await db.setObjectBulk(bulkSet); + await db.deleteAll(hashes.map(hash => `upload:${hash}`)); + + await Promise.all(hashes.map((hash, idx) => db.rename(`upload:${hash}:pids`, `upload:${newHashes[idx]}:pids`))); + } }, { batch: 100, progress: progress, diff --git a/src/user/delete.js b/src/user/delete.js index 47cc3b2742..917d8a97c9 100644 --- a/src/user/delete.js +++ b/src/user/delete.js @@ -108,8 +108,11 @@ module.exports = function (User) { `uid:${uid}:notifications:read`, `uid:${uid}:notifications:unread`, `uid:${uid}:bookmarks`, + `uid:${uid}:tids_read`, + `uid:${uid}:tids_unread`, `uid:${uid}:followed_tids`, `uid:${uid}:ignored_tids`, + `uid:${uid}:blocked_uids`, `user:${uid}:settings`, `user:${uid}:usernames`, `user:${uid}:emails`, diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 911398b708..414cb7cfdd 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -38,6 +38,7 @@ nconf.defaults({ const urlObject = url.parse(nconf.get('url')); const relativePath = urlObject.pathname !== '/' ? urlObject.pathname : ''; nconf.set('relative_path', relativePath); +nconf.set('asset_base_url', `${relativePath}/assets`); nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path'))); nconf.set('upload_url', '/assets/uploads'); nconf.set('url_parsed', urlObject);