From a1a87c7ca123091301f8126420b0e0bbd33e8411 Mon Sep 17 00:00:00 2001 From: Andrew Rodrigues Date: Sun, 8 Oct 2017 00:04:13 -0400 Subject: [PATCH 01/16] closes #5969 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 19984ac8f6..43045ca080 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "nodebb-plugin-spam-be-gone": "0.5.1", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "4.1.0", - "nodebb-theme-persona": "6.1.0", + "nodebb-theme-persona": "6.1.1", "nodebb-theme-slick": "1.1.1", "nodebb-theme-vanilla": "7.1.0", "nodebb-widget-essentials": "3.0.6", From cd6dcff38b79a4d63fc9ef7eab3fa7bd4d9e0a63 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Oct 2017 13:38:27 -0400 Subject: [PATCH 02/16] Split up customJS into customHTML and customJS for better organisation (#5981) * WIP * fixed customJS not actually working in footer * Moving scripts to footer, #5980 * Added upgrade scripts for #5980 --- .../en-GB/admin/appearance/customise.json | 6 ++- public/less/admin/appearance/customise.less | 2 +- public/src/admin/appearance/customise.js | 11 ++++++ src/middleware/header.js | 28 ++++++++------ src/upgrades/1.7.0/generate-custom-html.js | 37 +++++++++++++++++++ src/views/admin/appearance/customise.tpl | 25 +++++++++++-- 6 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 src/upgrades/1.7.0/generate-custom-html.js diff --git a/public/language/en-GB/admin/appearance/customise.json b/public/language/en-GB/admin/appearance/customise.json index 5095f7a937..a1220ec96d 100644 --- a/public/language/en-GB/admin/appearance/customise.json +++ b/public/language/en-GB/admin/appearance/customise.json @@ -3,8 +3,12 @@ "custom-css.description": "Enter your own CSS declarations here, which will be applied after all other styles.", "custom-css.enable": "Enable Custom CSS", + "custom-js": "Custom Javascript", + "custom-js.description": "Enter your own javascript here. It will be executed after the page is loaded completely.", + "custom-js.enable": "Enable Custom Javascript", + "custom-header": "Custom Header", - "custom-header.description": "Enter custom HTML here (ex. JavaScript, Meta Tags, etc.), which will be appended to the <head> section of your forum's markup.", + "custom-header.description": "Enter custom HTML here (ex. Meta Tags, etc.), which will be appended to the <head> section of your forum's markup. Script tags are allowed, but are discouraged, as the Custom Javascript tab is available.", "custom-header.enable": "Enable Custom Header", "custom-css.livereload": "Enable Live Reload", diff --git a/public/less/admin/appearance/customise.less b/public/less/admin/appearance/customise.less index 093230c7c6..3bef7fa560 100644 --- a/public/less/admin/appearance/customise.less +++ b/public/less/admin/appearance/customise.less @@ -1,4 +1,4 @@ -#customCSS, #customHTML, #email-editor { +#customCSS, #customJS, #customHTML, #email-editor { width: 100%; height: 450px; display: block; diff --git a/public/src/admin/appearance/customise.js b/public/src/admin/appearance/customise.js index e95ea5ba30..b9abf899c2 100644 --- a/public/src/admin/appearance/customise.js +++ b/public/src/admin/appearance/customise.js @@ -6,9 +6,11 @@ define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Se Customise.init = function () { Settings.prepare(function () { $('#customCSS').text($('#customCSS-holder').val()); + $('#customJS').text($('#customJS-holder').val()); $('#customHTML').text($('#customHTML-holder').val()); var customCSS = ace.edit('customCSS'); + var customJS = ace.edit('customJS'); var customHTML = ace.edit('customHTML'); customCSS.setTheme('ace/theme/twilight'); @@ -20,6 +22,15 @@ define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Se $('#customCSS-holder').val(customCSS.getValue()); }); + customJS.setTheme('ace/theme/twilight'); + customJS.getSession().setMode('ace/mode/js'); + + customJS.on('change', function () { + app.flags = app.flags || {}; + app.flags._unsaved = true; + $('#customJS-holder').val(customJS.getValue()); + }); + customHTML.setTheme('ace/theme/twilight'); customHTML.getSession().setMode('ace/mode/html'); diff --git a/src/middleware/header.js b/src/middleware/header.js index e35cc0e333..ec454936e0 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -65,9 +65,6 @@ module.exports = function (middleware) { async.waterfall([ function (next) { async.parallel({ - scripts: function (next) { - plugins.fireHook('filter:scripts.get', [], next); - }, isAdmin: function (next) { user.isAdministrator(req.uid, next); }, @@ -143,8 +140,8 @@ module.exports = function (middleware) { templateValues.userJSON = JSON.stringify(results.user); templateValues.useCustomCSS = parseInt(meta.config.useCustomCSS, 10) === 1 && meta.config.customCSS; templateValues.customCSS = templateValues.useCustomCSS ? (meta.config.renderedCustomCSS || '') : ''; - templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1; - templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : ''; + templateValues.useCustomHTML = parseInt(meta.config.useCustomHTML, 10) === 1; + templateValues.customHTML = templateValues.useCustomHTML ? meta.config.customHTML : ''; templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin; templateValues.defaultLang = meta.config.defaultLang || 'en-GB'; templateValues.userLang = res.locals.config.userLang; @@ -155,12 +152,6 @@ module.exports = function (middleware) { templateValues.template = { name: res.locals.template }; templateValues.template[res.locals.template] = true; - templateValues.scripts = results.scripts.map(function (script) { - return { src: script }; - }); - - addTimeagoLocaleScript(templateValues.scripts, res.locals.config.userLang); - if (req.route && req.route.path === '/') { modifyTitle(templateValues); } @@ -192,6 +183,21 @@ module.exports = function (middleware) { }, next); }, function (data, next) { + async.parallel({ + scripts: async.apply(plugins.fireHook, 'filter:scripts.get', []), + }, function (err, results) { + next(err, data, results); + }); + }, + function (data, results, next) { + data.templateValues.scripts = results.scripts.map(function (script) { + return { src: script }; + }); + addTimeagoLocaleScript(data.templateValues.scripts, res.locals.config.userLang); + + data.templateValues.useCustomJS = parseInt(meta.config.useCustomJS, 10) === 1; + data.templateValues.customJS = data.templateValues.useCustomJS ? meta.config.customJS : ''; + req.app.render('footer', data.templateValues, next); }, ], callback); diff --git a/src/upgrades/1.7.0/generate-custom-html.js b/src/upgrades/1.7.0/generate-custom-html.js new file mode 100644 index 0000000000..5de0238920 --- /dev/null +++ b/src/upgrades/1.7.0/generate-custom-html.js @@ -0,0 +1,37 @@ +'use strict'; + +var meta = require('../../meta'); + +module.exports = { + name: 'Generate customHTML block from old customJS setting', + timestamp: Date.UTC(2017, 9, 12), + method: function (callback) { + var newHTML = meta.config.customJS; + var newJS = []; + + // Forgive me for parsing HTML with regex... + var scriptMatch = /^([\s\S]+?)<\/script>/m; + var match = scriptMatch.exec(newHTML); + + while (match) { + if (match[1]) { + // Append to newJS array + newJS.push(match[1].trim()); + + // Remove the match from the existing value + newHTML = ((match.index > 0 ? newHTML.slice(0, match.index) : '') + newHTML.slice(match.index + match[0].length)).trim(); + } + + match = scriptMatch.exec(newHTML); + } + + // Combine newJS array + newJS = newJS.join('\n\n'); + + // Write both values to config + meta.configs.setMultiple({ + customHTML: newHTML, + customJS: newJS, + }, callback); + }, +}; diff --git a/src/views/admin/appearance/customise.tpl b/src/views/admin/appearance/customise.tpl index b2d1307b80..1c8a76b2f4 100644 --- a/src/views/admin/appearance/customise.tpl +++ b/src/views/admin/appearance/customise.tpl @@ -1,6 +1,7 @@ +
+

+ [[admin/appearance/customise:custom-js.description]] +

+
+ + +
+
+
+ +
+
+
+

[[admin/appearance/customise:custom-header.description]]

- +
-
From cfacc4f133aa00ffcf62eba81b328e32118e8cf1 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 12 Oct 2017 17:01:01 -0400 Subject: [PATCH 03/16] potential fix for #5980 --- public/src/ajaxify.js | 2 -- public/src/client/footer.js | 1 + 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index c18922485b..14b2ab0ad1 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -424,6 +424,4 @@ $(document).ready(function () { // Progressive Enhancement, ajaxify available only to modern browsers ajaxifyAnchors(); } - - app.load(); }); diff --git a/public/src/client/footer.js b/public/src/client/footer.js index 7dcdade78b..872104976b 100644 --- a/public/src/client/footer.js +++ b/public/src/client/footer.js @@ -98,4 +98,5 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu socket.on('event:unread.updateChatCount', updateUnreadChatCount); initUnreadTopics(); + app.load(); }); From ae261e073bd7ee885520db4a9b5e5d492263d897 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Oct 2017 09:58:26 -0400 Subject: [PATCH 04/16] updating markdown and moving app.load back into ajaxify.end --- package.json | 2 +- public/src/ajaxify.js | 2 ++ public/src/client/footer.js | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 89456f495d..671b7626c9 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "nodebb-plugin-dbsearch": "2.0.6", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.2.1", - "nodebb-plugin-markdown": "8.1.0", + "nodebb-plugin-markdown": "8.2.0", "nodebb-plugin-mentions": "2.1.6", "nodebb-plugin-soundpack-default": "1.0.0", "nodebb-plugin-spam-be-gone": "0.5.1", diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index 14b2ab0ad1..c18922485b 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -424,4 +424,6 @@ $(document).ready(function () { // Progressive Enhancement, ajaxify available only to modern browsers ajaxifyAnchors(); } + + app.load(); }); diff --git a/public/src/client/footer.js b/public/src/client/footer.js index 872104976b..7dcdade78b 100644 --- a/public/src/client/footer.js +++ b/public/src/client/footer.js @@ -98,5 +98,4 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu socket.on('event:unread.updateChatCount', updateUnreadChatCount); initUnreadTopics(); - app.load(); }); From 8741f7c63c6395df8b5b9c8538577276ef633714 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Oct 2017 12:20:48 -0400 Subject: [PATCH 05/16] updated ACP menu label --- public/language/en-GB/admin/menu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/language/en-GB/admin/menu.json b/public/language/en-GB/admin/menu.json index 3ec4ad41c4..3ff123c2ee 100644 --- a/public/language/en-GB/admin/menu.json +++ b/public/language/en-GB/admin/menu.json @@ -39,7 +39,7 @@ "section-appearance": "Appearance", "appearance/themes": "Themes", "appearance/skins": "Skins", - "appearance/customise": "Custom HTML & CSS", + "appearance/customise": "Custom Content (HTML/JS/CSS)", "section-extend": "Extend", "extend/plugins": "Plugins", From f9a1951ec5039f03b94eb5ba749bdc312d73ff5b Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 13 Oct 2017 10:53:02 -0600 Subject: [PATCH 06/16] Enable running multiple upgrades at once --- app.js | 9 ++++----- src/upgrade.js | 18 +++++++----------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/app.js b/app.js index 686b355602..4ce28da224 100644 --- a/app.js +++ b/app.js @@ -219,14 +219,13 @@ function upgrade() { var meta = require('./src/meta'); var upgrade = require('./src/upgrade'); var build = require('./src/meta/build'); - var tasks = [db.init, meta.configs.init, upgrade.run, build.buildAll]; + var tasks = [db.init, meta.configs.init]; if (nconf.get('upgrade') !== true) { // Likely an upgrade script name passed in - tasks[2] = async.apply(upgrade.runSingle, nconf.get('upgrade')); - - // Skip build - tasks.pop(); + tasks.push(async.apply(upgrade.runParticular, nconf.get('upgrade').split(','))); + } else { + tasks.push(upgrade.run, build.buildAll); } // disable mongo timeouts during upgrade nconf.set('mongo:options:socketTimeoutMS', 0); diff --git a/src/upgrade.js b/src/upgrade.js index f1f5a016ad..97f5287bc6 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -87,23 +87,19 @@ Upgrade.run = function (callback) { }); }; -Upgrade.runSingle = function (query, callback) { +Upgrade.runParticular = function (names, callback) { process.stdout.write('\nParsing upgrade scripts... '); async.waterfall([ async.apply(file.walk, path.join(__dirname, './upgrades')), function (files, next) { - next(null, files.filter(function (file) { - return path.basename(file, '.js') === query; - })); - }, - ], function (err, files) { - if (err) { - return callback(err); - } + var upgrades = files.filter(function (file) { + return names.indexOf(path.basename(file, '.js')) !== -1; + }); - Upgrade.process(files, 0, callback); - }); + Upgrade.process(upgrades, 0, next); + }, + ], callback); }; Upgrade.process = function (files, skipCount, callback) { From 0efa403a688c3abc2326f1e6ee40b30c8f2f6737 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Fri, 13 Oct 2017 13:12:48 -0400 Subject: [PATCH 07/16] invoking the proper ACE Editor mode --- public/src/admin/appearance/customise.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/admin/appearance/customise.js b/public/src/admin/appearance/customise.js index b9abf899c2..86894f0d63 100644 --- a/public/src/admin/appearance/customise.js +++ b/public/src/admin/appearance/customise.js @@ -23,7 +23,7 @@ define('admin/appearance/customise', ['admin/settings', 'ace/ace'], function (Se }); customJS.setTheme('ace/theme/twilight'); - customJS.getSession().setMode('ace/mode/js'); + customJS.getSession().setMode('ace/mode/javascript'); customJS.on('change', function () { app.flags = app.flags || {}; From 46fafb20b9079e68155db3e0f233a8df8003c47c Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 13 Oct 2017 21:02:41 -0600 Subject: [PATCH 08/16] Remove string.js dependency --- package.json | 1 - public/src/modules/helpers.js | 10 +- public/src/modules/string.js | 3 - public/src/modules/translator.js | 35 ++-- public/src/utils.js | 313 +++++++++++++++++++++++++++- src/controllers/accounts/profile.js | 4 +- src/controllers/topics.js | 3 +- src/flags.js | 3 +- src/messaging.js | 3 +- src/messaging/data.js | 3 +- src/meta/js.js | 1 - src/notifications.js | 5 +- src/posts/parse.js | 5 +- src/posts/summary.js | 4 +- src/socket.io/helpers.js | 6 +- src/socket.io/posts/edit.js | 4 +- src/topics/create.js | 4 +- src/topics/follow.js | 4 +- src/topics/teaser.js | 4 +- src/user/notifications.js | 4 +- src/user/profile.js | 3 +- test/build.js | 1 - test/utils.js | 22 ++ 23 files changed, 383 insertions(+), 62 deletions(-) delete mode 100644 public/src/modules/string.js diff --git a/package.json b/package.json index 89456f495d..2d1318e5d2 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "socket.io-redis": "5.2.0", "socketio-wildcard": "2.0.0", "spdx-license-list": "^3.0.1", - "string": "^3.3.3", "toobusy-js": "^0.5.1", "uglify-js": "^3.1.3", "validator": "9.0.0", diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index 736926101d..77a060ed7a 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -3,13 +3,13 @@ (function (factory) { if (typeof module === 'object' && module.exports) { var relative_path = require('nconf').get('relative_path'); - module.exports = factory(require('../utils'), require('benchpressjs'), require('string'), relative_path); + module.exports = factory(require('../utils'), require('benchpressjs'), relative_path); } else if (typeof define === 'function' && define.amd) { - define('helpers', ['benchpress', 'string'], function (Benchpress, string) { - return factory(utils, Benchpress, string, config.relative_path); + define('helpers', ['benchpress'], function (Benchpress) { + return factory(utils, Benchpress, config.relative_path); }); } -}(function (utils, Benchpress, S, relative_path) { +}(function (utils, Benchpress, relative_path) { var helpers = { displayMenuItem: displayMenuItem, buildMetaTag: buildMetaTag, @@ -92,7 +92,7 @@ } function stripTags(str) { - return S(String(str)).stripTags().s; + return utils.stripHTMLTags(str); } function generateCategoryBackground(category) { diff --git a/public/src/modules/string.js b/public/src/modules/string.js deleted file mode 100644 index a06e1862f9..0000000000 --- a/public/src/modules/string.js +++ /dev/null @@ -1,3 +0,0 @@ -/* -string.js - Copyright (C) 2012-2013, JP Richardson -*/!function(){"use strict";function n(e,t){t!==null&&t!==undefined?typeof t=="string"?e.s=t:e.s=t.toString():e.s=t,e.orig=t,t!==null&&t!==undefined?e.__defineGetter__?e.__defineGetter__("length",function(){return e.s.length}):e.length=t.length:e.length=-1}function r(e){n(this,e)}function u(){for(var e in s)(function(e){var t=s[e];i.hasOwnProperty(e)||(o.push(e),i[e]=function(){return String.prototype.s=this,t.apply(this,arguments)})})(e)}function a(){for(var e=0;er?n.slice(s,i):"")},camelize:function(){var e=this.trim().s.replace(/(\-|_|\s)+(.)?/g,function(e,t,n){return n?n.toUpperCase():""});return new this.constructor(e)},capitalize:function(){return new this.constructor(this.s.substr(0,1).toUpperCase()+this.s.substring(1).toLowerCase())},charAt:function(e){return this.s.charAt(e)},chompLeft:function(e){var t=this.s;return t.indexOf(e)===0?(t=t.slice(e.length),new this.constructor(t)):this},chompRight:function(e){if(this.endsWith(e)){var t=this.s;return t=t.slice(0,t.length-e.length),new this.constructor(t)}return this},collapseWhitespace:function(){var e=this.s.replace(/[\s\xa0]+/g," ").replace(/^\s+|\s+$/g,"");return new this.constructor(e)},contains:function(e){return this.s.indexOf(e)>=0},count:function(e){var t=0,n=this.s.indexOf(e);while(n>=0)t+=1,n=this.s.indexOf(e,n+1);return t},dasherize:function(){var e=this.trim().s.replace(/[_\s]+/g,"-").replace(/([A-Z])/g,"-$1").replace(/-+/g,"-").toLowerCase();return new this.constructor(e)},decodeHtmlEntities:function(){var e=this.s;return e=e.replace(/&#(\d+);?/g,function(e,t){return String.fromCharCode(t)}).replace(/&#[xX]([A-Fa-f0-9]+);?/g,function(e,t){return String.fromCharCode(parseInt(t,16))}).replace(/&([^;\W]+;?)/g,function(e,n){var r=n.replace(/;$/,""),i=t[n]||n.match(/;$/)&&t[r];return typeof i=="number"?String.fromCharCode(i):typeof i=="string"?i:e}),new this.constructor(e)},endsWith:function(e){var t=this.s.length-e.length;return t>=0&&this.s.indexOf(e,t)===t},escapeHTML:function(){return new this.constructor(this.s.replace(/[&<>"']/g,function(e){return"&"+m[e]+";"}))},ensureLeft:function(e){var t=this.s;return t.indexOf(e)===0?this:new this.constructor(e+t)},ensureRight:function(e){var t=this.s;return this.endsWith(e)?this:new this.constructor(t+e)},humanize:function(){if(this.s===null||this.s===undefined)return new this.constructor("");var e=this.underscore().replace(/_id$/,"").replace(/_/g," ").trim().capitalize();return new this.constructor(e)},isAlpha:function(){return!/[^a-z\xC0-\xFF]/.test(this.s.toLowerCase())},isAlphaNumeric:function(){return!/[^0-9a-z\xC0-\xFF]/.test(this.s.toLowerCase())},isEmpty:function(){return this.s===null||this.s===undefined?!0:/^[\s\xa0]*$/.test(this.s)},isLower:function(){return this.isAlpha()&&this.s.toLowerCase()===this.s},isNumeric:function(){return!/[^0-9]/.test(this.s)},isUpper:function(){return this.isAlpha()&&this.s.toUpperCase()===this.s},left:function(e){if(e>=0){var t=this.s.substr(0,e);return new this.constructor(t)}return this.right(-e)},lines:function(){return this.replaceAll("\r\n","\n").s.split("\n")},pad:function(e,t){t==null&&(t=" ");if(this.s.length>=e)return new this.constructor(this.s);e-=this.s.length;var n=Array(Math.ceil(e/2)+1).join(t),r=Array(Math.floor(e/2)+1).join(t);return new this.constructor(n+this.s+r)},padLeft:function(e,t){return t==null&&(t=" "),this.s.length>=e?new this.constructor(this.s):new this.constructor(Array(e-this.s.length+1).join(t)+this.s)},padRight:function(e,t){return t==null&&(t=" "),this.s.length>=e?new this.constructor(this.s):new this.constructor(this.s+Array(e-this.s.length+1).join(t))},parseCSV:function(e,t,n,r){e=e||",",n=n||"\\",typeof t=="undefined"&&(t='"');var i=0,s=[],o=[],u=this.s.length,a=!1,f=this,l=function(e){return f.s.charAt(e)};if(typeof r!="undefined")var c=[];t||(a=!0);while(i=0){var t=this.s.substr(this.s.length-e,e);return new this.constructor(t)}return this.left(-e)},setValue:function(e){return n(this,e),this},slugify:function(){var e=(new r(this.s.replace(/[^\w\s-]/g,"").toLowerCase())).dasherize().s;return e.charAt(0)==="-"&&(e=e.substr(1)),new this.constructor(e)},startsWith:function(e){return this.s.lastIndexOf(e,0)===0},stripPunctuation:function(){return new this.constructor(this.s.replace(/[^\w\s]|_/g,"").replace(/\s+/g," "))},stripTags:function(){var e=this.s,t=arguments.length>0?arguments:[""];return d(t,function(t){e=e.replace(RegExp("]*>","gi"),"")}),new this.constructor(e)},template:function(e,t,n){var r=this.s,t=t||p.TMPL_OPEN,n=n||p.TMPL_CLOSE,i=t.replace(/[-[\]()*\s]/g,"\\$&").replace(/\$/g,"\\$"),s=n.replace(/[-[\]()*\s]/g,"\\$&").replace(/\$/g,"\\$"),o=new RegExp(i+"(.+?)"+s,"g"),u=r.match(o)||[];return u.forEach(function(i){var s=i.substring(t.length,i.length-n.length);typeof e[s]!="undefined"&&(r=r.replace(i,e[s]))}),new this.constructor(r)},times:function(e){return new this.constructor((new Array(e+1)).join(this.s))},toBoolean:function(){if(typeof this.orig=="string"){var e=this.s.toLowerCase();return e==="true"||e==="yes"||e==="on"}return this.orig===!0||this.orig===1},toFloat:function(e){var t=parseFloat(this.s);return e?parseFloat(t.toFixed(e)):t},toInt:function(){return/^\s*-?0x/i.test(this.s)?parseInt(this.s,16):parseInt(this.s,10)},trim:function(){var e;return typeof i.trim=="undefined"?e=this.s.replace(/(^\s*|\s*$)/g,""):e=this.s.trim(),new this.constructor(e)},trimLeft:function(){var e;return i.trimLeft?e=this.s.trimLeft():e=this.s.replace(/(^\s*)/g,""),new this.constructor(e)},trimRight:function(){var e;return i.trimRight?e=this.s.trimRight():e=this.s.replace(/\s+$/,""),new this.constructor(e)},truncate:function(e,t){var n=this.s;e=~~e,t=t||"...";if(n.length<=e)return new this.constructor(n);var i=function(e){return e.toUpperCase()!==e.toLowerCase()?"A":" "},s=n.slice(0,e+1).replace(/.(?=\W*\w*$)/g,i);return s.slice(s.length-2).match(/\w\w/)?s=s.replace(/\s*\S+$/,""):s=(new r(s.slice(0,s.length-1))).trimRight().s,(s+t).length>n.length?new r(n):new r(n.slice(0,s.length)+t)},toCSV:function(){function u(e){return e!==null&&e!==""}var e=",",t='"',n="\\",i=!0,s=!1,o=[];typeof arguments[0]=="object"?(e=arguments[0].delimiter||e,e=arguments[0].separator||e,t=arguments[0].qualifier||t,i=!!arguments[0].encloseNumbers,n=arguments[0].escape||n,s=!!arguments[0].keys):typeof arguments[0]=="string"&&(e=arguments[0]),typeof arguments[1]=="string"&&(t=arguments[1]),arguments[1]===null&&(t=null);if(this.orig instanceof Array)o=this.orig;else for(var a in this.orig)this.orig.hasOwnProperty(a)&&(s?o.push(a):o.push(this.orig[a]));var f=n+t,l=[];for(var c=0;c",quot:'"',apos:"'",amp:"&"},m={};for(var g in v)m[v[g]]=g;t={amp:"&",gt:">",lt:"<",quot:'"',apos:"'",AElig:198,Aacute:193,Acirc:194,Agrave:192,Aring:197,Atilde:195,Auml:196,Ccedil:199,ETH:208,Eacute:201,Ecirc:202,Egrave:200,Euml:203,Iacute:205,Icirc:206,Igrave:204,Iuml:207,Ntilde:209,Oacute:211,Ocirc:212,Ograve:210,Oslash:216,Otilde:213,Ouml:214,THORN:222,Uacute:218,Ucirc:219,Ugrave:217,Uuml:220,Yacute:221,aacute:225,acirc:226,aelig:230,agrave:224,aring:229,atilde:227,auml:228,ccedil:231,eacute:233,ecirc:234,egrave:232,eth:240,euml:235,iacute:237,icirc:238,igrave:236,iuml:239,ntilde:241,oacute:243,ocirc:244,ograve:242,oslash:248,otilde:245,ouml:246,szlig:223,thorn:254,uacute:250,ucirc:251,ugrave:249,uuml:252,yacute:253,yuml:255,copy:169,reg:174,nbsp:160,iexcl:161,cent:162,pound:163,curren:164,yen:165,brvbar:166,sect:167,uml:168,ordf:170,laquo:171,not:172,shy:173,macr:175,deg:176,plusmn:177,sup1:185,sup2:178,sup3:179,acute:180,micro:181,para:182,middot:183,cedil:184,ordm:186,raquo:187,frac14:188,frac12:189,frac34:190,iquest:191,times:215,divide:247,"OElig;":338,"oelig;":339,"Scaron;":352,"scaron;":353,"Yuml;":376,"fnof;":402,"circ;":710,"tilde;":732,"Alpha;":913,"Beta;":914,"Gamma;":915,"Delta;":916,"Epsilon;":917,"Zeta;":918,"Eta;":919,"Theta;":920,"Iota;":921,"Kappa;":922,"Lambda;":923,"Mu;":924,"Nu;":925,"Xi;":926,"Omicron;":927,"Pi;":928,"Rho;":929,"Sigma;":931,"Tau;":932,"Upsilon;":933,"Phi;":934,"Chi;":935,"Psi;":936,"Omega;":937,"alpha;":945,"beta;":946,"gamma;":947,"delta;":948,"epsilon;":949,"zeta;":950,"eta;":951,"theta;":952,"iota;":953,"kappa;":954,"lambda;":955,"mu;":956,"nu;":957,"xi;":958,"omicron;":959,"pi;":960,"rho;":961,"sigmaf;":962,"sigma;":963,"tau;":964,"upsilon;":965,"phi;":966,"chi;":967,"psi;":968,"omega;":969,"thetasym;":977,"upsih;":978,"piv;":982,"ensp;":8194,"emsp;":8195,"thinsp;":8201,"zwnj;":8204,"zwj;":8205,"lrm;":8206,"rlm;":8207,"ndash;":8211,"mdash;":8212,"lsquo;":8216,"rsquo;":8217,"sbquo;":8218,"ldquo;":8220,"rdquo;":8221,"bdquo;":8222,"dagger;":8224,"Dagger;":8225,"bull;":8226,"hellip;":8230,"permil;":8240,"prime;":8242,"Prime;":8243,"lsaquo;":8249,"rsaquo;":8250,"oline;":8254,"frasl;":8260,"euro;":8364,"image;":8465,"weierp;":8472,"real;":8476,"trade;":8482,"alefsym;":8501,"larr;":8592,"uarr;":8593,"rarr;":8594,"darr;":8595,"harr;":8596,"crarr;":8629,"lArr;":8656,"uArr;":8657,"rArr;":8658,"dArr;":8659,"hArr;":8660,"forall;":8704,"part;":8706,"exist;":8707,"empty;":8709,"nabla;":8711,"isin;":8712,"notin;":8713,"ni;":8715,"prod;":8719,"sum;":8721,"minus;":8722,"lowast;":8727,"radic;":8730,"prop;":8733,"infin;":8734,"ang;":8736,"and;":8743,"or;":8744,"cap;":8745,"cup;":8746,"int;":8747,"there4;":8756,"sim;":8764,"cong;":8773,"asymp;":8776,"ne;":8800,"equiv;":8801,"le;":8804,"ge;":8805,"sub;":8834,"sup;":8835,"nsub;":8836,"sube;":8838,"supe;":8839,"oplus;":8853,"otimes;":8855,"perp;":8869,"sdot;":8901,"lceil;":8968,"rceil;":8969,"lfloor;":8970,"rfloor;":8971,"lang;":9001,"rang;":9002,"loz;":9674,"spades;":9824,"clubs;":9827,"hearts;":9829,"diams;":9830}}.call(this); diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 465a240e4e..cd0d3c75c1 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -10,8 +10,8 @@ } if (typeof define === 'function' && define.amd) { // AMD. Register as a named module - define('translator', ['string'], function (string) { - return factory(string, loadClient, warn); + define('translator', [], function () { + return factory(utils, loadClient, warn); }); } else if (typeof module === 'object' && module.exports) { // Node @@ -37,15 +37,23 @@ }); } - module.exports = factory(require('string'), loadServer, warn); + module.exports = factory(require('../utils'), loadServer, warn); }()); - } else { - window.translator = factory(window.string, loadClient, warn); } -}(function (string, load, warn) { +}(function (utils, load, warn) { var assign = Object.assign || jQuery.extend; function classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + function escapeHTML(str) { + return utils.decodeHTMLEntities( + String(str) + .replace(/[\s\xa0]+/g, ' ') + .replace(/^\s+|\s+$/g, '') + ).replace(/[<>]/g, function (c) { + return c === '<' ? '<' : '>'; + }); + } + var Translator = (function () { /** * Construct a new Translator object @@ -284,9 +292,7 @@ } var argsToTranslate = args.map(function (arg) { - return string(arg).collapseWhitespace().decodeHTMLEntities().escapeHTML().s.replace(/&/g, '&'); - }).map(function (arg) { - return self.translate(arg); + return self.translate(escapeHTML(arg)); }); return Promise.all(argsToTranslate).then(function (translatedArgs) { @@ -539,12 +545,13 @@ return cb(''); } - Translator.create(lang).translate(text).catch(function (err) { + return Translator.create(lang).translate(text).then(function (output) { + if (cb) { + setTimeout(cb, 0, output); + } + return output; + }, function (err) { warn('Translation failed: ' + err.stack); - }).then(function (output) { - cb(output); - }).catch(function (err) { - console.error(err); }); }, diff --git a/public/src/utils.js b/public/src/utils.js index 910082a430..328c73ebe0 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -25,6 +25,279 @@ window.utils = factory(window.XRegExp); } }(function (XRegExp) { + var freeze = Object.freeze || function (obj) { return obj; }; + + // add default escape function for escaping HTML entities + var escapeCharMap = freeze({ + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`', + '=': '=', + }); + function replaceChar(c) { + return escapeCharMap[c]; + } + var escapeChars = /[&<>"'`=]/g; + + var HTMLEntities = freeze({ + amp: '&', + gt: '>', + lt: '<', + quot: '"', + apos: "'", + AElig: 198, + Aacute: 193, + Acirc: 194, + Agrave: 192, + Aring: 197, + Atilde: 195, + Auml: 196, + Ccedil: 199, + ETH: 208, + Eacute: 201, + Ecirc: 202, + Egrave: 200, + Euml: 203, + Iacute: 205, + Icirc: 206, + Igrave: 204, + Iuml: 207, + Ntilde: 209, + Oacute: 211, + Ocirc: 212, + Ograve: 210, + Oslash: 216, + Otilde: 213, + Ouml: 214, + THORN: 222, + Uacute: 218, + Ucirc: 219, + Ugrave: 217, + Uuml: 220, + Yacute: 221, + aacute: 225, + acirc: 226, + aelig: 230, + agrave: 224, + aring: 229, + atilde: 227, + auml: 228, + ccedil: 231, + eacute: 233, + ecirc: 234, + egrave: 232, + eth: 240, + euml: 235, + iacute: 237, + icirc: 238, + igrave: 236, + iuml: 239, + ntilde: 241, + oacute: 243, + ocirc: 244, + ograve: 242, + oslash: 248, + otilde: 245, + ouml: 246, + szlig: 223, + thorn: 254, + uacute: 250, + ucirc: 251, + ugrave: 249, + uuml: 252, + yacute: 253, + yuml: 255, + copy: 169, + reg: 174, + nbsp: 160, + iexcl: 161, + cent: 162, + pound: 163, + curren: 164, + yen: 165, + brvbar: 166, + sect: 167, + uml: 168, + ordf: 170, + laquo: 171, + not: 172, + shy: 173, + macr: 175, + deg: 176, + plusmn: 177, + sup1: 185, + sup2: 178, + sup3: 179, + acute: 180, + micro: 181, + para: 182, + middot: 183, + cedil: 184, + ordm: 186, + raquo: 187, + frac14: 188, + frac12: 189, + frac34: 190, + iquest: 191, + times: 215, + divide: 247, + 'OElig;': 338, + 'oelig;': 339, + 'Scaron;': 352, + 'scaron;': 353, + 'Yuml;': 376, + 'fnof;': 402, + 'circ;': 710, + 'tilde;': 732, + 'Alpha;': 913, + 'Beta;': 914, + 'Gamma;': 915, + 'Delta;': 916, + 'Epsilon;': 917, + 'Zeta;': 918, + 'Eta;': 919, + 'Theta;': 920, + 'Iota;': 921, + 'Kappa;': 922, + 'Lambda;': 923, + 'Mu;': 924, + 'Nu;': 925, + 'Xi;': 926, + 'Omicron;': 927, + 'Pi;': 928, + 'Rho;': 929, + 'Sigma;': 931, + 'Tau;': 932, + 'Upsilon;': 933, + 'Phi;': 934, + 'Chi;': 935, + 'Psi;': 936, + 'Omega;': 937, + 'alpha;': 945, + 'beta;': 946, + 'gamma;': 947, + 'delta;': 948, + 'epsilon;': 949, + 'zeta;': 950, + 'eta;': 951, + 'theta;': 952, + 'iota;': 953, + 'kappa;': 954, + 'lambda;': 955, + 'mu;': 956, + 'nu;': 957, + 'xi;': 958, + 'omicron;': 959, + 'pi;': 960, + 'rho;': 961, + 'sigmaf;': 962, + 'sigma;': 963, + 'tau;': 964, + 'upsilon;': 965, + 'phi;': 966, + 'chi;': 967, + 'psi;': 968, + 'omega;': 969, + 'thetasym;': 977, + 'upsih;': 978, + 'piv;': 982, + 'ensp;': 8194, + 'emsp;': 8195, + 'thinsp;': 8201, + 'zwnj;': 8204, + 'zwj;': 8205, + 'lrm;': 8206, + 'rlm;': 8207, + 'ndash;': 8211, + 'mdash;': 8212, + 'lsquo;': 8216, + 'rsquo;': 8217, + 'sbquo;': 8218, + 'ldquo;': 8220, + 'rdquo;': 8221, + 'bdquo;': 8222, + 'dagger;': 8224, + 'Dagger;': 8225, + 'bull;': 8226, + 'hellip;': 8230, + 'permil;': 8240, + 'prime;': 8242, + 'Prime;': 8243, + 'lsaquo;': 8249, + 'rsaquo;': 8250, + 'oline;': 8254, + 'frasl;': 8260, + 'euro;': 8364, + 'image;': 8465, + 'weierp;': 8472, + 'real;': 8476, + 'trade;': 8482, + 'alefsym;': 8501, + 'larr;': 8592, + 'uarr;': 8593, + 'rarr;': 8594, + 'darr;': 8595, + 'harr;': 8596, + 'crarr;': 8629, + 'lArr;': 8656, + 'uArr;': 8657, + 'rArr;': 8658, + 'dArr;': 8659, + 'hArr;': 8660, + 'forall;': 8704, + 'part;': 8706, + 'exist;': 8707, + 'empty;': 8709, + 'nabla;': 8711, + 'isin;': 8712, + 'notin;': 8713, + 'ni;': 8715, + 'prod;': 8719, + 'sum;': 8721, + 'minus;': 8722, + 'lowast;': 8727, + 'radic;': 8730, + 'prop;': 8733, + 'infin;': 8734, + 'ang;': 8736, + 'and;': 8743, + 'or;': 8744, + 'cap;': 8745, + 'cup;': 8746, + 'int;': 8747, + 'there4;': 8756, + 'sim;': 8764, + 'cong;': 8773, + 'asymp;': 8776, + 'ne;': 8800, + 'equiv;': 8801, + 'le;': 8804, + 'ge;': 8805, + 'sub;': 8834, + 'sup;': 8835, + 'nsub;': 8836, + 'sube;': 8838, + 'supe;': 8839, + 'oplus;': 8853, + 'otimes;': 8855, + 'perp;': 8869, + 'sdot;': 8901, + 'lceil;': 8968, + 'rceil;': 8969, + 'lfloor;': 8970, + 'rfloor;': 8971, + 'lang;': 9001, + 'rang;': 9002, + 'loz;': 9674, + 'spades;': 9824, + 'clubs;': 9827, + 'hearts;': 9829, + 'diams;': 9830, + }); + var utils = { generateUUID: function () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { @@ -33,6 +306,35 @@ return v.toString(16); }); }, + // https://github.com/substack/node-ent/blob/master/index.js + decodeHTMLEntities: function (html) { + return String(html) + .replace(/&#(\d+);?/g, function (_, code) { + return String.fromCharCode(code); + }) + .replace(/&#[xX]([A-Fa-f0-9]+);?/g, function (_, hex) { + return String.fromCharCode(parseInt(hex, 16)); + }) + .replace(/&([^;\W]+;?)/g, function (m, e) { + var ee = e.replace(/;$/, ''); + var target = HTMLEntities[e] || (e.match(/;$/) && HTMLEntities[ee]); + + if (typeof target === 'number') { + return String.fromCharCode(target); + } else if (typeof target === 'string') { + return target; + } + + return m; + }); + }, + // https://github.com/jprichardson/string.js/blob/master/lib/string.js + stripHTMLTags: function (str, tags) { + var pattern = (tags || ['']).map(function (tag) { + return utils.escapeRegexChars(tag); + }).join('|'); + return String(str).replace(new RegExp(']*>', 'gi'), ''); + }, invalidUnicodeChars: XRegExp('[^\\p{L}\\s\\d\\-_]', 'g'), invalidLatinChars: /[^\w\s\d\-_]/g, @@ -232,8 +534,15 @@ return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); }, - escapeHTML: function (raw) { - return raw.replace(/&/gm, '&').replace(//gm, '>'); + escapeHTML: function (str) { + if (str == null) { + return ''; + } + if (!str) { + return String(str); + } + + return str.toString().replace(escapeChars, replaceChar); }, isAndroidBrowser: function () { diff --git a/src/controllers/accounts/profile.js b/src/controllers/accounts/profile.js index e067ed3ef5..fc9f9cb5b4 100644 --- a/src/controllers/accounts/profile.js +++ b/src/controllers/accounts/profile.js @@ -2,7 +2,6 @@ var nconf = require('nconf'); var async = require('async'); -var S = require('string'); var user = require('../../user'); var posts = require('../../posts'); @@ -13,6 +12,7 @@ var helpers = require('../helpers'); var pagination = require('../../pagination'); var messaging = require('../../messaging'); var translator = require('../../translator'); +var utils = require('../../utils'); var profileController = module.exports; @@ -87,7 +87,7 @@ profileController.get = function (req, res, callback) { userData.profileviews = 1; } - var plainAboutMe = userData.aboutme ? S(userData.aboutme).decodeHTMLEntities().stripTags().s : ''; + var plainAboutMe = userData.aboutme ? utils.stripHTMLTags(utils.decodeHTMLEntities(userData.aboutme)) : ''; res.locals.metaTags = [ { diff --git a/src/controllers/topics.js b/src/controllers/topics.js index d3a4846390..c75f5c3602 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -2,7 +2,6 @@ var async = require('async'); -var S = require('string'); var nconf = require('nconf'); var user = require('../user'); @@ -217,7 +216,7 @@ function addTags(topicData, req, res) { var postAtIndex = findPost(Math.max(0, req.params.post_index - 1)); if (postAtIndex && postAtIndex.content) { - description = S(postAtIndex.content).decodeHTMLEntities().stripTags().s; + description = utils.stripHTMLTags(utils.decodeHTMLEntities(postAtIndex.content)); } if (description.length > 255) { diff --git a/src/flags.js b/src/flags.js index 749878db33..a578accf18 100644 --- a/src/flags.js +++ b/src/flags.js @@ -2,7 +2,6 @@ var async = require('async'); var _ = require('lodash'); -var S = require('string'); var winston = require('winston'); var validator = require('validator'); @@ -660,7 +659,7 @@ Flags.notify = function (flagObj, uid, callback) { return callback(err); } - var title = S(results.title).decodeHTMLEntities().s; + var title = utils.decodeHTMLEntities(results.title); var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); notifications.create({ diff --git a/src/messaging.js b/src/messaging.js index 9a53f327ce..a90c36c2c5 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -2,7 +2,6 @@ var async = require('async'); -var S = require('string'); var validator = require('validator'); var db = require('./database'); @@ -215,7 +214,7 @@ Messaging.getTeaser = function (uid, roomId, callback) { return callback(); } if (teaser.content) { - teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s; + teaser.content = utils.stripHTMLTags(utils.decodeHTMLEntities(teaser.content)); teaser.content = validator.escape(String(teaser.content)); } diff --git a/src/messaging/data.js b/src/messaging/data.js index b3a2ba58dd..88b6683a11 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -1,7 +1,6 @@ 'use strict'; var async = require('async'); -var S = require('string'); var db = require('../database'); var user = require('../user'); @@ -73,7 +72,7 @@ module.exports = function (Messaging) { return next(err); } message.content = result; - message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s; + message.cleanedContent = utils.stripHTMLTags(utils.decodeHTMLEntities(result)); next(null, message); }); }, next); diff --git a/src/meta/js.js b/src/meta/js.js index 8082be2412..28c5255db0 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -72,7 +72,6 @@ JS.scripts = { 'public/src/modules/alerts.js', 'public/src/modules/taskbar.js', 'public/src/modules/helpers.js', - 'public/src/modules/string.js', 'public/src/modules/flags.js', 'public/src/modules/storage.js', ], diff --git a/src/notifications.js b/src/notifications.js index 098efe5d9f..919d5a22e4 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -4,7 +4,6 @@ var async = require('async'); var winston = require('winston'); var cron = require('cron').CronJob; var nconf = require('nconf'); -var S = require('string'); var _ = require('lodash'); var db = require('./database'); @@ -56,7 +55,7 @@ Notifications.getMultiple = function (nids, callback) { notification.datetimeISO = utils.toISOString(notification.datetime); if (notification.bodyLong) { - notification.bodyLong = S(notification.bodyLong).escapeHTML().s; + notification.bodyLong = utils.escapeHTML(notification.bodyLong); } notification.user = usersData[index]; @@ -470,7 +469,7 @@ Notifications.merge = function (notifications, callback) { }); var numUsers = usernames.length; - var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s; + var title = utils.decodeHTMLEntities(notifications[modifyIndex].topicTitle || ''); var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); titleEscaped = titleEscaped ? (', ' + titleEscaped) : ''; diff --git a/src/posts/parse.js b/src/posts/parse.js index d46eaa800d..d6f7165f19 100644 --- a/src/posts/parse.js +++ b/src/posts/parse.js @@ -4,12 +4,12 @@ var async = require('async'); var nconf = require('nconf'); var url = require('url'); var winston = require('winston'); -var S = require('string'); var meta = require('../meta'); var cache = require('./cache'); var plugins = require('../plugins'); var translator = require('../translator'); +var utils = require('../utils'); module.exports = function (Posts) { Posts.urlRegex = { @@ -82,7 +82,6 @@ module.exports = function (Posts) { function sanitizeSignature(signature) { signature = translator.escape(signature); - var string = S(signature); var tagsToStrip = []; if (parseInt(meta.config['signatures:disableLinks'], 10) === 1) { @@ -93,6 +92,6 @@ module.exports = function (Posts) { tagsToStrip.push('img'); } - return tagsToStrip.length ? string.stripTags.apply(string, tagsToStrip).s : signature; + return utils.stripHTMLTags(signature, tagsToStrip); } }; diff --git a/src/posts/summary.js b/src/posts/summary.js index 6c1b6d1958..902a003445 100644 --- a/src/posts/summary.js +++ b/src/posts/summary.js @@ -3,7 +3,6 @@ var async = require('async'); var validator = require('validator'); -var S = require('string'); var _ = require('lodash'); var topics = require('../topics'); @@ -144,8 +143,7 @@ module.exports = function (Posts) { function stripTags(content) { if (content) { - var s = S(content); - return s.stripTags.apply(s, utils.stripTags).s; + return utils.stripHTMLTags(content, utils.stripTags); } return content; } diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 0f158cd080..cbc01aff60 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -2,7 +2,6 @@ var async = require('async'); var winston = require('winston'); -var S = require('string'); var db = require('../database'); var websockets = require('./index'); @@ -12,6 +11,7 @@ var topics = require('../topics'); var privileges = require('../privileges'); var notifications = require('../notifications'); var plugins = require('../plugins'); +var utils = require('../utils'); var SocketHelpers = {}; @@ -105,7 +105,7 @@ SocketHelpers.sendNotificationToPostOwner = function (pid, fromuid, command, not }, next); }, function (results, next) { - var title = S(results.topicTitle).decodeHTMLEntities().s; + var title = utils.decodeHTMLEntities(results.topicTitle); var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); notifications.create({ @@ -151,7 +151,7 @@ SocketHelpers.sendNotificationToTopicOwner = function (tid, fromuid, command, no return; } ownerUid = results.topicData.uid; - var title = S(results.topicData.title).decodeHTMLEntities().s; + var title = utils.decodeHTMLEntities(results.topicData.title); var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); notifications.create({ diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js index 00ae540496..48f750c2ce 100644 --- a/src/socket.io/posts/edit.js +++ b/src/socket.io/posts/edit.js @@ -3,12 +3,12 @@ var async = require('async'); var validator = require('validator'); var _ = require('lodash'); -var S = require('string'); var posts = require('../../posts'); var groups = require('../../groups'); var events = require('../../events'); var meta = require('../../meta'); +var utils = require('../../utils'); var websockets = require('../index'); module.exports = function (SocketPosts) { @@ -20,7 +20,7 @@ module.exports = function (SocketPosts) { } // Trim and remove HTML (latter for composers that send in HTML, like redactor) - var contentLen = S(data.content).stripTags().s.trim().length; + var contentLen = utils.stripHTMLTags(data.content).trim().length; if (data.title && data.title.length < parseInt(meta.config.minimumTitleLength, 10)) { return callback(new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]')); diff --git a/src/topics/create.js b/src/topics/create.js index f5a51e7e77..edb1f1b84d 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -4,7 +4,7 @@ var async = require('async'); var _ = require('lodash'); var validator = require('validator'); -var S = require('string'); + var db = require('../database'); var utils = require('../utils'); var plugins = require('../plugins'); @@ -343,7 +343,7 @@ module.exports = function (Topics) { function check(item, min, max, minError, maxError, callback) { // Trim and remove HTML (latter for composers that send in HTML, like redactor) if (typeof item === 'string') { - item = S(item).stripTags().s.trim(); + item = utils.stripHTMLTags(item).trim(); } if (item === null || item === undefined || item.length < parseInt(min, 10)) { diff --git a/src/topics/follow.js b/src/topics/follow.js index f1981766a2..f1bad3ccf3 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -2,7 +2,6 @@ 'use strict'; var async = require('async'); -var S = require('string'); var winston = require('winston'); var db = require('../database'); @@ -13,6 +12,7 @@ var privileges = require('../privileges'); var meta = require('../meta'); var emailer = require('../emailer'); var plugins = require('../plugins'); +var utils = require('../utils'); module.exports = function (Topics) { Topics.toggleFollow = function (tid, uid, callback) { @@ -214,7 +214,7 @@ module.exports = function (Topics) { title = postData.topic.title; if (title) { - title = S(title).decodeHTMLEntities().s; + title = utils.decodeHTMLEntities(title); titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); } diff --git a/src/topics/teaser.js b/src/topics/teaser.js index 09d4d048ae..2827e74d41 100644 --- a/src/topics/teaser.js +++ b/src/topics/teaser.js @@ -3,7 +3,6 @@ var async = require('async'); var _ = require('lodash'); -var S = require('string'); var winston = require('winston'); var meta = require('../meta'); @@ -91,8 +90,7 @@ module.exports = function (Topics) { if (tidToPost[topic.tid]) { tidToPost[topic.tid].index = meta.config.teaserPost === 'first' ? 1 : counts[index]; if (tidToPost[topic.tid].content) { - var s = S(tidToPost[topic.tid].content); - tidToPost[topic.tid].content = s.stripTags.apply(s, utils.stripTags).s; + tidToPost[topic.tid].content = utils.stripHTMLTags(tidToPost[topic.tid].content, utils.stripTags); } } return tidToPost[topic.tid]; diff --git a/src/user/notifications.js b/src/user/notifications.js index 4e2dcba7e8..a446a9a822 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -3,12 +3,12 @@ var async = require('async'); var winston = require('winston'); -var S = require('string'); var db = require('../database'); var meta = require('../meta'); var notifications = require('../notifications'); var privileges = require('../privileges'); +var utils = require('../utils'); var UserNotifications = module.exports; @@ -281,7 +281,7 @@ UserNotifications.sendTopicNotificationToFollowers = function (uid, topicData, p var title = topicData.title; if (title) { - title = S(title).decodeHTMLEntities().s; + title = utils.decodeHTMLEntities(title); } notifications.create({ diff --git a/src/user/profile.js b/src/user/profile.js index dbdcbcba85..70dffeebe6 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -2,7 +2,6 @@ 'use strict'; var async = require('async'); -var S = require('string'); var utils = require('../utils'); var meta = require('../meta'); @@ -61,7 +60,7 @@ module.exports = function (User) { } else if (field === 'fullname') { return updateFullname(updateUid, data.fullname, next); } else if (field === 'signature') { - data[field] = S(data[field]).stripTags().s; + data[field] = utils.stripHTMLTags(data[field]); } User.setUserField(updateUid, field, data[field], next); diff --git a/test/build.js b/test/build.js index 1c346ad621..35471d0fd6 100644 --- a/test/build.js +++ b/test/build.js @@ -1,6 +1,5 @@ 'use strict'; -var string = require('string'); var path = require('path'); var fs = require('fs'); var assert = require('assert'); diff --git a/test/utils.js b/test/utils.js index d6821d9d4b..e76c1b9bff 100644 --- a/test/utils.js +++ b/test/utils.js @@ -16,6 +16,28 @@ describe('Utility Methods', function () { var $ = global.$; global.window = window; + // https://github.com/jprichardson/string.js/blob/master/test/string.test.js + it('should decode HTML entities', function (done) { + assert.strictEqual( + utils.decodeHTMLEntities('Ken Thompson & Dennis Ritchie'), + 'Ken Thompson & Dennis Ritchie' + ); + assert.strictEqual( + utils.decodeHTMLEntities('3 < 4'), + '3 < 4' + ); + assert.strictEqual( + utils.decodeHTMLEntities('http://'), + 'http://' + ); + done(); + }); + it('should strip HTML tags', function (done) { + assert.strictEqual(utils.stripHTMLTags('

just some text

'), 'just some text'); + assert.strictEqual(utils.stripHTMLTags('

just some text

', ['p']), 'just some text'); + done(); + }); + it('should preserve case if requested', function (done) { var slug = utils.slugify('UPPER CASE', true); assert.equal(slug, 'UPPER-CASE'); From 190eea5691ac5a4a8f89154d5ef7a1b77c7c32fa Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 13 Oct 2017 21:17:57 -0600 Subject: [PATCH 09/16] Remove this useless check --- public/src/modules/translator.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index cd0d3c75c1..18a42e4e8f 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -42,7 +42,6 @@ } }(function (utils, load, warn) { var assign = Object.assign || jQuery.extend; - function classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function escapeHTML(str) { return utils.decodeHTMLEntities( @@ -62,7 +61,6 @@ */ function Translator(language) { var self = this; - classCallCheck(self, Translator); if (!language) { throw new TypeError('Parameter `language` must be a language string. Received ' + language + (language === '' ? '(empty string)' : '')); From 189008af7533ca6518d376aecc6d66ec46c1eee4 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Sat, 14 Oct 2017 13:47:05 -0600 Subject: [PATCH 10/16] Optimize translator - Use `slice` less - Skip iterations with `indexOf` --- public/src/modules/translator.js | 202 ++++++++++++++++--------------- 1 file changed, 102 insertions(+), 100 deletions(-) diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 18a42e4e8f..b98ad68abe 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -117,10 +117,10 @@ var level = 0; while (i + 2 <= len) { - if (text.slice(i, i + 2) === '[[') { + if (text[i] === '[' && text[i + 1] === '[') { level += 1; i += 1; - } else if (text.slice(i, i + 2) === ']]') { + } else if (text[i] === ']' && text[i + 1] === ']') { level -= 1; i += 1; } else if (level === 0 && text[i] === ',' && text[i - 1] !== '\\') { @@ -134,111 +134,113 @@ return arr; } + // move to the first [[ + cursor = str.indexOf('[[', cursor); + // the loooop, we'll go to where the cursor // is equal to the length of the string since // slice doesn't include the ending index - while (cursor + 2 <= len) { - // if the current position in the string looks - // like the beginning of a translation string - if (str.slice(cursor, cursor + 2) === '[[') { - // split the string from the last break - // to the character before the cursor - // add that to the result array - toTranslate.push(str.slice(lastBreak, cursor)); - // set the cursor position past the beginning - // brackets of the translation string - cursor += 2; - // set the last break to our current - // spot since we just broke the string - lastBreak = cursor; - // we're in a token now - inToken = true; - - // the current level of nesting of the translation strings - var level = 0; - var sliced; - // validating the current string is actually a translation - var textBeforeColonFound = false; - var colonFound = false; - var textAfterColonFound = false; - var commaAfterNameFound = false; - - while (cursor + 2 <= len) { - sliced = str.slice(cursor, cursor + 2); - // found some text after the double bracket, - // so this is probably a translation string - if (!textBeforeColonFound && validTextRegex.test(sliced[0])) { - textBeforeColonFound = true; - cursor += 1; - // found a colon, so this is probably a translation string - } else if (textBeforeColonFound && !colonFound && sliced[0] === ':') { - colonFound = true; - cursor += 1; - // found some text after the colon, - // so this is probably a translation string - } else if (colonFound && !textAfterColonFound && validTextRegex.test(sliced[0])) { - textAfterColonFound = true; - cursor += 1; - } else if (textAfterColonFound && !commaAfterNameFound && sliced[0] === ',') { - commaAfterNameFound = true; - cursor += 1; - // a space or comma was found before the name - // this isn't a translation string, so back out - } else if (!(textBeforeColonFound && colonFound && textAfterColonFound && commaAfterNameFound) && - invalidTextRegex.test(sliced[0])) { - cursor += 1; - lastBreak -= 2; - // no longer in a token - inToken = false; - if (level > 0) { - level -= 1; - } else { - break; - } - // if we're at the beginning of another translation string, - // we're nested, so add to our level - } else if (sliced === '[[') { - level += 1; - cursor += 2; - // if we're at the end of a translation string - } else if (sliced === ']]') { - // if we're at the base level, then this is the end - if (level === 0) { - // so grab the name and args - var currentSlice = str.slice(lastBreak, cursor); - var result = split(currentSlice); - var name = result[0]; - var args = result.slice(1); - - // make a backup based on the raw string of the token - // if there are arguments to the token - var backup = ''; - if (args && args.length) { - backup = this.translate(currentSlice); - } - // add the translation promise to the array - toTranslate.push(this.translateKey(name, args, backup)); - // skip past the ending brackets - cursor += 2; - // set this as our last break - lastBreak = cursor; - // and we're no longer in a translation string, - // so continue with the main loop - inToken = false; - break; - } - // otherwise we lower the level + while (cursor + 2 <= len && cursor !== -1) { + // split the string from the last break + // to the character before the cursor + // add that to the result array + toTranslate.push(str.slice(lastBreak, cursor)); + // set the cursor position past the beginning + // brackets of the translation string + cursor += 2; + // set the last break to our current + // spot since we just broke the string + lastBreak = cursor; + // we're in a token now + inToken = true; + + // the current level of nesting of the translation strings + var level = 0; + var char0; + var char1; + // validating the current string is actually a translation + var textBeforeColonFound = false; + var colonFound = false; + var textAfterColonFound = false; + var commaAfterNameFound = false; + + while (cursor + 2 <= len) { + char0 = str[cursor]; + char1 = str[cursor + 1]; + // found some text after the double bracket, + // so this is probably a translation string + if (!textBeforeColonFound && validTextRegex.test(char0)) { + textBeforeColonFound = true; + cursor += 1; + // found a colon, so this is probably a translation string + } else if (textBeforeColonFound && !colonFound && char0 === ':') { + colonFound = true; + cursor += 1; + // found some text after the colon, + // so this is probably a translation string + } else if (colonFound && !textAfterColonFound && validTextRegex.test(char0)) { + textAfterColonFound = true; + cursor += 1; + } else if (textAfterColonFound && !commaAfterNameFound && char0 === ',') { + commaAfterNameFound = true; + cursor += 1; + // a space or comma was found before the name + // this isn't a translation string, so back out + } else if (!(textBeforeColonFound && colonFound && textAfterColonFound && commaAfterNameFound) && + invalidTextRegex.test(char0)) { + cursor += 1; + lastBreak -= 2; + // no longer in a token + inToken = false; + if (level > 0) { level -= 1; - // and skip past the ending brackets - cursor += 2; } else { - // otherwise just move to the next character - cursor += 1; + break; } + // if we're at the beginning of another translation string, + // we're nested, so add to our level + } else if (char0 === '[' && char1 === '[') { + level += 1; + cursor += 2; + // if we're at the end of a translation string + } else if (char0 === ']' && char1 === ']') { + // if we're at the base level, then this is the end + if (level === 0) { + // so grab the name and args + var currentSlice = str.slice(lastBreak, cursor); + var result = split(currentSlice); + var name = result[0]; + var args = result.slice(1); + + // make a backup based on the raw string of the token + // if there are arguments to the token + var backup = ''; + if (args && args.length) { + backup = this.translate(currentSlice); + } + // add the translation promise to the array + toTranslate.push(this.translateKey(name, args, backup)); + // skip past the ending brackets + cursor += 2; + // set this as our last break + lastBreak = cursor; + // and we're no longer in a translation string, + // so continue with the main loop + inToken = false; + break; + } + // otherwise we lower the level + level -= 1; + // and skip past the ending brackets + cursor += 2; + } else { + // otherwise just move to the next character + cursor += 1; } } - // move to the next character - cursor += 1; + + // skip to the next [[ + cursor = str.indexOf('[[', cursor); } // ending string of source @@ -308,7 +310,7 @@ * Load translation file (or use a cached version), and optionally return the translation of a certain key * @param {string} namespace - The file name of the translation namespace * @param {string} [key] - The key of the specific translation to getJSON - * @returns {Promise<{ [key: string]: string }>|Promise} + * @returns {Promise<{ [key: string]: string } | string>} */ Translator.prototype.getTranslation = function getTranslation(namespace, key) { var translation; From 6a6dc7ef061d1085a1d88d9c854958bab3174ffe Mon Sep 17 00:00:00 2001 From: Baris Usakli Date: Mon, 16 Oct 2017 15:35:46 -0400 Subject: [PATCH 11/16] closes #5990 --- public/src/client/topic/move.js | 8 ++++++-- src/socket.io/topics/move.js | 6 ++++-- src/topics/tools.js | 14 +++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/public/src/client/topic/move.js b/public/src/client/topic/move.js index 0b969ef6b9..b29ae964e0 100644 --- a/public/src/client/topic/move.js +++ b/public/src/client/topic/move.js @@ -63,11 +63,15 @@ define('forum/topic/move', ['categorySelector'], function (categorySelector) { } function moveTopics() { - socket.emit(Move.moveAll ? 'topics.moveAll' : 'topics.move', { + var data = { tids: Move.tids, cid: selectedCategory.cid, currentCid: Move.currentCid, - }, function (err) { + }; + + $(window).trigger('action:topic.move', data); + + socket.emit(Move.moveAll ? 'topics.moveAll' : 'topics.move', data, function (err) { modal.modal('hide'); if (err) { diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js index efd6562d18..21d7543fb8 100644 --- a/src/socket.io/topics/move.js +++ b/src/socket.io/topics/move.js @@ -28,7 +28,8 @@ module.exports = function (SocketTopics) { function (_topicData, next) { topicData = _topicData; topicData.tid = tid; - topics.tools.move(tid, data.cid, socket.uid, next); + data.uid = socket.uid; + topics.tools.move(tid, data, next); }, function (next) { socketHelpers.emitToTopicAndCategory('event:topic_moved', topicData); @@ -59,8 +60,9 @@ module.exports = function (SocketTopics) { categories.getAllTopicIds(data.currentCid, 0, -1, next); }, function (tids, next) { + data.uid = socket.uid; async.eachLimit(tids, 50, function (tid, next) { - topics.tools.move(tid, data.cid, socket.uid, next); + topics.tools.move(tid, data, next); }, next); }, ], callback); diff --git a/src/topics/tools.js b/src/topics/tools.js index 743aa9d38d..bcd0b537e0 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -243,9 +243,10 @@ module.exports = function (Topics) { ], callback); }; - topicTools.move = function (tid, cid, uid, callback) { + topicTools.move = function (tid, data, callback) { var topic; var oldCid; + var cid = data.cid; async.waterfall([ function (next) { Topics.exists(tid, next); @@ -311,12 +312,11 @@ module.exports = function (Topics) { }); }, function (next) { - plugins.fireHook('action:topic.move', { - tid: tid, - fromCid: oldCid, - toCid: cid, - uid: uid, - }); + var hookData = _.clone(data); + hookData.fromCid = oldCid; + hookData.toCid = cid; + hookData.tid = tid; + plugins.fireHook('action:topic.move', hookData); next(); }, ], callback); From 3c45b3edb61d8528b30390dbabb62c5946579b0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 31 Oct 2017 09:04:07 -0400 Subject: [PATCH 12/16] string is removed on develop --- src/messaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/messaging.js b/src/messaging.js index 51bd78e77a..9ceda65b7d 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -72,7 +72,7 @@ function canGet(hook, callerUid, uid, callback) { } Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) { - message = S(message).stripTags().decodeHTMLEntities().s; + message = utils.decodeHTMLEntities(utils.stripHTMLTags(message)); message = validator.escape(String(message)); plugins.fireHook('filter:parse.raw', message, function (err, parsed) { From c9465403f5b2dad6896c5c788c353bce7f3cf9ea Mon Sep 17 00:00:00 2001 From: Baris Usakli Date: Wed, 1 Nov 2017 12:05:06 -0400 Subject: [PATCH 13/16] closes #6023 --- public/src/utils.js | 2 +- test/utils.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/public/src/utils.js b/public/src/utils.js index 328c73ebe0..0e300d2772 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -333,7 +333,7 @@ var pattern = (tags || ['']).map(function (tag) { return utils.escapeRegexChars(tag); }).join('|'); - return String(str).replace(new RegExp(']*>', 'gi'), ''); + return String(str).replace(new RegExp('<(\\/)?(' + (pattern || '[^\\s>]+') + ')(\\s+[^<>]*?)?\\s*(\\/)?>', 'gi'), ''); }, invalidUnicodeChars: XRegExp('[^\\p{L}\\s\\d\\-_]', 'g'), diff --git a/test/utils.js b/test/utils.js index e76c1b9bff..4c69b235e0 100644 --- a/test/utils.js +++ b/test/utils.js @@ -35,6 +35,8 @@ describe('Utility Methods', function () { it('should strip HTML tags', function (done) { assert.strictEqual(utils.stripHTMLTags('

just some text

'), 'just some text'); assert.strictEqual(utils.stripHTMLTags('

just some text

', ['p']), 'just some text'); + assert.strictEqual(utils.stripHTMLTags('just some text', ['i']), 'just some text'); + assert.strictEqual(utils.stripHTMLTags('just some
text
', ['i', 'div']), 'just some text'); done(); }); From ec38b18e34cbb210b9b721291fd8df9b11392979 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 1 Nov 2017 18:57:52 -0600 Subject: [PATCH 14/16] Always compare password with a hash Prevents quick response when user / email doesn't exist --- src/password.js | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/password.js b/src/password.js index 6cc1e1776a..439c0592d7 100644 --- a/src/password.js +++ b/src/password.js @@ -4,16 +4,39 @@ var path = require('path'); var fork = require('./meta/debugFork'); -exports.hash = function (rounds, password, callback) { +function hash(rounds, password, callback) { forkChild({ type: 'hash', rounds: rounds, password: password }, callback); -}; +} + +exports.hash = hash; -exports.compare = function (password, hash, callback) { - if (!hash || !password) { - return setImmediate(callback, null, false); +var fakeHashCache; +function getFakeHash(callback) { + if (fakeHashCache) { + return callback(null, fakeHashCache); } - forkChild({ type: 'compare', password: password, hash: hash }, callback); -}; + + hash(12, Math.random().toString(), function (err, hash) { + if (err) { + return callback(err); + } + + fakeHashCache = hash; + callback(null, fakeHashCache); + }); +} + +function compare(password, hash, callback) { + getFakeHash(function (err, fakeHash) { + if (err) { + return callback(err); + } + + forkChild({ type: 'compare', password: password, hash: hash || fakeHash }, callback); + }); +} + +exports.compare = compare; function forkChild(message, callback) { var child = fork(path.join(__dirname, 'bcrypt')); From e609e497b3f6a9fe556147727731ea6a4ffe2bf7 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 1 Nov 2017 18:58:44 -0600 Subject: [PATCH 15/16] Provide more error information to logs Provides full stack instead of just message --- app.js | 8 +++++--- loader.js | 9 +++++---- nodebb | 23 +++++++++++++---------- src/analytics.js | 2 +- src/database/mongo.js | 4 ++-- src/database/redis.js | 4 ++-- src/events.js | 5 +++-- src/flags.js | 2 +- src/groups/membership.js | 2 +- src/install.js | 2 +- src/logger.js | 2 +- src/meta/build.js | 4 ++-- src/meta/cacheBuster.js | 2 +- src/meta/languages.js | 9 +-------- src/middleware/render.js | 2 +- src/notifications.js | 2 +- src/plugins.js | 6 +++--- src/plugins/data.js | 2 +- src/reset.js | 8 ++++---- src/user/digest.js | 2 +- src/webserver.js | 19 +++++++++---------- 21 files changed, 59 insertions(+), 60 deletions(-) diff --git a/app.js b/app.js index 7a222b930c..d1bf1c7c08 100644 --- a/app.js +++ b/app.js @@ -192,7 +192,8 @@ function setup() { process.stdout.write('\n' + separator + '\n\n'); if (err) { - winston.error('There was a problem completing NodeBB setup: ', err.message); + winston.error('There was a problem completing NodeBB setup', err); + throw err; } else { if (data.hasOwnProperty('password')) { process.stdout.write('An administrative user was automatically created for you:\n'); @@ -270,9 +271,10 @@ function activate() { }, ], function (err) { if (err) { - winston.error(err.message); + winston.error('An error occurred during plugin activation', err); + throw err; } - process.exit(err ? 1 : 0); + process.exit(0); }); } diff --git a/loader.js b/loader.js index a2df1b33b7..434e2cce5c 100644 --- a/loader.js +++ b/loader.js @@ -158,8 +158,8 @@ Loader.restart = function () { fs.readFile(pathToConfig, { encoding: 'utf-8' }, function (err, configFile) { if (err) { - console.log('Error reading config : ' + err.message); - process.exit(); + console.error('Error reading config'); + throw err; } var conf = JSON.parse(configFile); @@ -240,11 +240,12 @@ fs.open(path.join(__dirname, 'config.json'), 'r', function (err) { Loader.start, ], function (err) { if (err) { - console.log('[loader] Error during startup: ' + err.message); + console.error('[loader] Error during startup'); + throw err; } }); } else { // No config detected, kickstart web installer - require('child_process').fork('app'); + fork('app'); } }); diff --git a/nodebb b/nodebb index c0edb6e39c..d745bb1c6c 100755 --- a/nodebb +++ b/nodebb @@ -451,7 +451,8 @@ var commands = { return upgradeProc.on('close', function (err) { if (err) { - process.stdout.write('\nError'.red + ': ' + err.message + '\n'); + process.stdout.write('Error occurred during upgrade'); + throw err; } }); } @@ -472,19 +473,21 @@ var commands = { var upgradeProc = fork(arr); upgradeProc.on('close', next); + upgradeProc.on('error', next); }, ], function (err) { if (err) { - process.stdout.write('\nError'.red + ': ' + err.message + '\n'); - } else { - var message = 'NodeBB Upgrade Complete!'; - // some consoles will return undefined/zero columns, so just use 2 spaces in upgrade script if we can't get our column count - var columns = process.stdout.columns; - var spaces = columns ? new Array(Math.floor(columns / 2) - (message.length / 2) + 1).join(' ') : ' '; - - process.stdout.write('OK\n'.green); - process.stdout.write('\n' + spaces + message.green.bold + '\n\n'.reset); + process.stdout.write('Error occurred during upgrade'); + throw err; } + + var message = 'NodeBB Upgrade Complete!'; + // some consoles will return undefined/zero columns, so just use 2 spaces in upgrade script if we can't get our column count + var columns = process.stdout.columns; + var spaces = columns ? new Array(Math.floor(columns / 2) - (message.length / 2) + 1).join(' ') : ' '; + + process.stdout.write('OK\n'.green); + process.stdout.write('\n' + spaces + message.green.bold + '\n\n'.reset); }); }, }, diff --git a/src/analytics.js b/src/analytics.js index b4922cf184..1f98beb476 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -100,7 +100,7 @@ Analytics.writeData = function (callback) { async.parallel(dbQueue, function (err) { if (err) { - winston.error('[analytics] Encountered error while writing analytics to data store: ' + err.message); + winston.error('[analytics] Encountered error while writing analytics to data store', err); } callback(err); }); diff --git a/src/database/mongo.js b/src/database/mongo.js index fbe4b38a03..04addc07d5 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -104,7 +104,7 @@ mongoModule.init = function (callback) { mongoClient.connect(connString, connOptions, function (err, _db) { if (err) { - winston.error('NodeBB could not connect to your Mongo database. Mongo returned the following error: ' + err.message); + winston.error('NodeBB could not connect to your Mongo database. Mongo returned the following error', err); return callback(err); } @@ -164,7 +164,7 @@ mongoModule.createIndices = function (callback) { async.apply(createIndex, 'objects', { expireAt: 1 }, { expireAfterSeconds: 0, background: true }), ], function (err) { if (err) { - winston.error('Error creating index ' + err.message); + winston.error('Error creating index', err); return callback(err); } winston.info('[database] Checking database indices done!'); diff --git a/src/database/redis.js b/src/database/redis.js index 13d87c27bd..e45f8f0ef8 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -99,8 +99,8 @@ redisModule.connect = function (options) { if (dbIdx >= 0) { cxn.select(dbIdx, function (err) { if (err) { - winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + err.message); - process.exit(); + winston.error('NodeBB could not connect to your Redis database. Redis returned the following error', err); + throw err; } }); } diff --git a/src/events.js b/src/events.js index ffce83beb4..65a2c36ad8 100644 --- a/src/events.js +++ b/src/events.js @@ -3,6 +3,7 @@ var async = require('async'); var validator = require('validator'); +var winston = require('winston'); var db = require('./database'); var batch = require('./batch'); @@ -143,8 +144,8 @@ events.output = function () { process.stdout.write('\nDisplaying last ten administrative events...\n'.bold); events.getEvents(0, 9, function (err, events) { if (err) { - process.stdout.write(' Error '.red + String(err.message).reset); - process.exit(1); + winston.error('Error fetching events', err); + throw err; } events.forEach(function (event) { diff --git a/src/flags.js b/src/flags.js index a578accf18..209a5166d5 100644 --- a/src/flags.js +++ b/src/flags.js @@ -64,7 +64,7 @@ Flags.init = function (callback) { }, }, function (err, data) { if (err) { - winston.error('[flags/init] Could not retrieve filters (error: ' + err.message + ')'); + winston.error('[flags/init] Could not retrieve filters', err); data.filters = {}; } diff --git a/src/groups/membership.js b/src/groups/membership.js index 42c3932805..93dedc57c2 100644 --- a/src/groups/membership.js +++ b/src/groups/membership.js @@ -52,7 +52,7 @@ module.exports = function (Groups) { hidden: 1, }, function (err) { if (err && err.message !== '[[error:group-already-exists]]') { - winston.error('[groups.join] Could not create new hidden group: ' + err.message); + winston.error('[groups.join] Could not create new hidden group', err); return callback(err); } next(); diff --git a/src/install.js b/src/install.js index aa862f5338..3a69595599 100644 --- a/src/install.js +++ b/src/install.js @@ -542,7 +542,7 @@ install.save = function (server_conf, callback) { fs.writeFile(serverConfigPath, JSON.stringify(server_conf, null, 4), function (err) { if (err) { - winston.error('Error saving server configuration! ' + err.message); + winston.error('Error saving server configuration!', err); return callback(err); } diff --git a/src/logger.js b/src/logger.js index d00886b718..632c9b84ca 100644 --- a/src/logger.js +++ b/src/logger.js @@ -88,7 +88,7 @@ Logger.open = function (value) { if (stream) { stream.on('error', function (err) { - winston.error(err.message); + winston.error(err); }); } } else { diff --git a/src/meta/build.js b/src/meta/build.js index 26bec8b5e8..b01a92cb86 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -103,7 +103,7 @@ function beforeBuild(targets, callback) { async.apply(plugins.prepareForBuild, targets), ], function (err) { if (err) { - winston.error('[build] Encountered error preparing for build: ' + err.message); + winston.error('[build] Encountered error preparing for build', err); return callback(err); } @@ -203,7 +203,7 @@ function build(targets, callback) { }, ], function (err) { if (err) { - winston.error('[build] Encountered error during build step: ' + err.message); + winston.error('[build] Encountered error during build step', err); return callback(err); } diff --git a/src/meta/cacheBuster.js b/src/meta/cacheBuster.js index d402fe16f7..f88cebb680 100644 --- a/src/meta/cacheBuster.js +++ b/src/meta/cacheBuster.js @@ -33,7 +33,7 @@ exports.read = function read(callback) { fs.readFile(filePath, function (err, buffer) { if (err) { - winston.warn('[cache-buster] could not read cache buster: ' + err.message); + winston.warn('[cache-buster] could not read cache buster', err); return callback(null, generate()); } diff --git a/src/meta/languages.js b/src/meta/languages.js index 3cf1359f4a..3b9f3c3a9e 100644 --- a/src/meta/languages.js +++ b/src/meta/languages.js @@ -1,6 +1,5 @@ 'use strict'; -var winston = require('winston'); var path = require('path'); var async = require('async'); var fs = require('fs'); @@ -182,11 +181,5 @@ exports.build = function buildLanguages(callback) { }, getTranslationTree, writeLanguageFiles, - ], function (err) { - if (err) { - winston.error('[build] Language build failed: ' + err.message); - throw err; - } - callback(); - }); + ], callback); }; diff --git a/src/middleware/render.js b/src/middleware/render.js index bf3cb03638..851ee1f85f 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -132,7 +132,7 @@ module.exports = function (middleware) { try { p = decodeURIComponent(p); } catch (err) { - winston.error(err.message); + winston.error(err); p = ''; } p = validator.escape(String(p)); diff --git a/src/notifications.js b/src/notifications.js index 919d5a22e4..cbc58dae8e 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -396,7 +396,7 @@ Notifications.prune = function (callback) { }, ], function (err) { if (err) { - winston.error('Encountered error pruning notifications: ' + err.message); + winston.error('Encountered error pruning notifications', err); } callback(err); }); diff --git a/src/plugins.js b/src/plugins.js index 10db23eb65..cc15650357 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -63,7 +63,7 @@ Plugins.init = function (nbbApp, nbbMiddleware, callback) { Plugins.reload(function (err) { if (err) { - winston.error('[plugins] NodeBB encountered a problem while loading plugins', err.message); + winston.error('[plugins] NodeBB encountered a problem while loading plugins', err); return callback(err); } @@ -132,7 +132,7 @@ Plugins.reloadRoutes = function (callback) { var controllers = require('./controllers'); Plugins.fireHook('static:app.load', { app: app, router: router, middleware: middleware, controllers: controllers }, function (err) { if (err) { - winston.error('[plugins] Encountered error while executing post-router plugins hooks: ' + err.message); + winston.error('[plugins] Encountered error while executing post-router plugins hooks', err); return callback(err); } @@ -218,7 +218,7 @@ Plugins.list = function (matching, callback) { json: true, }, function (err, res, body) { if (err) { - winston.error('Error parsing plugins : ' + err.message); + winston.error('Error parsing plugins', err); return callback(err); } diff --git a/src/plugins/data.js b/src/plugins/data.js index 1fe4b2e861..b793a365d0 100644 --- a/src/plugins/data.js +++ b/src/plugins/data.js @@ -69,7 +69,7 @@ function loadPluginInfo(pluginPath, callback) { } catch (err) { var pluginDir = path.basename(pluginPath); - winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json! ' + err.message); + winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json!', err); return callback(new Error('[[error:parse-error]]')); } diff --git a/src/reset.js b/src/reset.js index 3d2fab2e0f..d45c48deb5 100644 --- a/src/reset.js +++ b/src/reset.js @@ -13,8 +13,8 @@ var Reset = {}; Reset.reset = function (callback) { db.init(function (err) { if (err) { - winston.error(err.message); - process.exit(1); + winston.error(err); + throw err; } if (nconf.get('t')) { @@ -50,7 +50,7 @@ Reset.reset = function (callback) { if (!err) { winston.info('[reset] Reset complete.'); } else { - winston.error('[reset] Errors were encountered while resetting your forum settings: %s', err.message); + winston.error('[reset] Errors were encountered while resetting your forum settings: %s', err); } callback(); @@ -141,7 +141,7 @@ function resetPlugin(pluginId, callback) { }, ], function (err) { if (err) { - winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err.message); + winston.error('[reset] Could not disable plugin: %s encountered error %s', pluginId, err); } else if (active) { winston.info('[reset] Plugin `%s` disabled', pluginId); } else { diff --git a/src/user/digest.js b/src/user/digest.js index ffd4f9031e..9a6a81cfcb 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -45,7 +45,7 @@ Digest.execute = function (payload, callback) { }, ], function (err, count) { if (err) { - winston.error('[user/jobs] Could not send digests (' + payload.interval + '): ' + err.message); + winston.error('[user/jobs] Could not send digests (' + payload.interval + ')', err); } else { winston.info('[user/jobs] Digest (' + payload.interval + ') scheduling completed. ' + count + ' email(s) sent.'); } diff --git a/src/webserver.js b/src/webserver.js index bfc472b8ea..dc7c70770c 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -43,13 +43,13 @@ if (nconf.get('ssl')) { module.exports.server = server; server.on('error', function (err) { - winston.error(err); if (err.code === 'EADDRINUSE') { - winston.error('NodeBB address in use, exiting...'); - process.exit(1); + winston.error('NodeBB address in use, exiting...', err); } else { - throw err; + winston.error(err); } + + throw err; }); module.exports.listen = function (callback) { @@ -301,13 +301,12 @@ function listen(callback) { if (isSocket) { oldUmask = process.umask('0000'); module.exports.testSocket(socketPath, function (err) { - if (!err) { - server.listen.apply(server, args); - } else { - winston.error('[startup] NodeBB was unable to secure domain socket access (' + socketPath + ')'); - winston.error('[startup] ' + err.message); - process.exit(); + if (err) { + winston.error('[startup] NodeBB was unable to secure domain socket access (' + socketPath + ')', err); + throw err; } + + server.listen.apply(server, args); }); } else { server.listen.apply(server, args); From dfad76120de02a99decf26708794159c9a5bf42a Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 2 Nov 2017 12:12:05 -0600 Subject: [PATCH 16/16] Support npm@5 and yarn (#6010) * Support npm@5 and yarn Use package.default.json Partial #6008 - Overwrite package.json with package.default.json values - `dependencies` field is merged with package.default.json version taking precidence - `./nodebb upgrade` automatically does those things and runs `git pull` - use `./nodebb upgrade --dev` to avoid the `git pull` * added logic to preserve extraneous plugins installed in node_modules/ * Don't automatically git pull * Simplify package-install, run it on upgrade just in case --- .gitignore | 1 + .travis.yml | 2 + nodebb | 18 +++++-- package.json => package.default.json | 0 src/meta/package-install.js | 72 ++++++++++++++++++++++++++++ src/plugins/install.js | 3 +- test/plugins.js | 8 ++++ 7 files changed, 98 insertions(+), 6 deletions(-) rename package.json => package.default.json (100%) create mode 100644 src/meta/package-install.js diff --git a/.gitignore b/.gitignore index b82cd9c314..68651e533d 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ build test/files/normalise.jpg.png test/files/normalise-resized.jpg package-lock.json +package.json diff --git a/.travis.yml b/.travis.yml index 7d7d1a3a4f..9a8a0fd66a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,8 @@ before_install: - "sudo service mongod start" before_script: - sleep 15 # wait for mongodb to be ready + - cp package.default.json package.json + - npm install - sh -c "if [ '$DB' = 'mongodb' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"mongo\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":27017,\\\"database\\\":0}\"; fi" - sh -c "if [ '$DB' = 'redis' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"redis\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":6379,\\\"database\\\":0}\"; fi" after_success: diff --git a/nodebb b/nodebb index d745bb1c6c..c632c72df9 100755 --- a/nodebb +++ b/nodebb @@ -6,18 +6,20 @@ var fs = require('fs'); var path = require('path'); var cproc = require('child_process'); +var packageInstall = require('./src/meta/package-install'); + // check to make sure dependencies are installed try { + fs.readFileSync(path.join(__dirname, './package.json')); fs.readFileSync(path.join(__dirname, 'node_modules/async/package.json')); } catch (e) { if (e.code === 'ENOENT') { process.stdout.write('Dependencies not yet installed.\n'); process.stdout.write('Installing them now...\n\n'); - cproc.execSync('npm i --production', { - cwd: __dirname, - stdio: [0, 1, 2], - }); + packageInstall.updatePackageFile(); + packageInstall.preserveExtraneousPlugins(); + packageInstall.npmInstallProduction(); } else { throw e; } @@ -458,9 +460,15 @@ var commands = { } async.series([ + function (next) { + packageInstall.updatePackageFile(); + packageInstall.preserveExtraneousPlugins(); + next(); + }, function (next) { process.stdout.write('1. '.bold + 'Bringing base dependencies up to date... '.yellow); - cproc.exec('npm i --production', { cwd: __dirname, stdio: 'ignore' }, next); + packageInstall.npmInstallProduction(); + next(); }, function (next) { process.stdout.write('OK\n'.green); diff --git a/package.json b/package.default.json similarity index 100% rename from package.json rename to package.default.json diff --git a/src/meta/package-install.js b/src/meta/package-install.js new file mode 100644 index 0000000000..3fee4cb9e4 --- /dev/null +++ b/src/meta/package-install.js @@ -0,0 +1,72 @@ +'use strict'; + +var path = require('path'); +var fs = require('fs'); +var cproc = require('child_process'); + +var packageFilePath = path.join(__dirname, '../../package.json'); +var packageDefaultFilePath = path.join(__dirname, '../../package.default.json'); +var modulesPath = path.join(__dirname, '../../node_modules'); + +function updatePackageFile() { + var oldPackageContents = {}; + + try { + oldPackageContents = JSON.parse(fs.readFileSync(packageFilePath, 'utf8')); + } catch (e) { + if (e.code !== 'ENOENT') { + throw e; + } + } + + var defaultPackageContents = JSON.parse(fs.readFileSync(packageDefaultFilePath, 'utf8')); + var packageContents = Object.assign({}, oldPackageContents, defaultPackageContents, { + dependencies: Object.assign({}, oldPackageContents.dependencies, defaultPackageContents.dependencies), + }); + + fs.writeFileSync(packageFilePath, JSON.stringify(packageContents, null, 2)); +} + +exports.updatePackageFile = updatePackageFile; + +function npmInstallProduction() { + cproc.execSync('npm i --production', { + cwd: path.join(__dirname, '../../'), + stdio: [0, 1, 2], + }); +} + +exports.npmInstallProduction = npmInstallProduction; + +function preserveExtraneousPlugins() { + // Skip if `node_modules/` is not found or inaccessible + try { + fs.accessSync(modulesPath, fs.constants.R_OK); + } catch (e) { + return; + } + + var isPackage = /^nodebb-(plugin|theme|widget|reward)-\w+/; + var packages = fs.readdirSync(modulesPath).filter(function (pkgName) { + return isPackage.test(pkgName); + }); + var packageContents = JSON.parse(fs.readFileSync(packageFilePath, 'utf8')); + + var extraneous = packages + // only extraneous plugins (ones not in package.json) + .filter(function (pkgName) { + return !packageContents.dependencies.hasOwnProperty(pkgName); + }) + // reduce to a map of package names to package versions + .reduce(function (map, pkgName) { + var pkgConfig = JSON.parse(fs.readFileSync(path.join(modulesPath, pkgName, 'package.json'))); + map[pkgName] = pkgConfig.version; + return map; + }, {}); + + // Add those packages to package.json + Object.assign(packageContents.dependencies, extraneous); + fs.writeFileSync(packageFilePath, JSON.stringify(packageContents, null, 2)); +} + +exports.preserveExtraneousPlugins = preserveExtraneousPlugins; diff --git a/src/plugins/install.js b/src/plugins/install.js index ced9f800f8..7bd407ca08 100644 --- a/src/plugins/install.js +++ b/src/plugins/install.js @@ -6,6 +6,7 @@ var path = require('path'); var fs = require('fs'); var nconf = require('nconf'); var os = require('os'); +var cproc = require('child_process'); var db = require('../database'); var meta = require('../meta'); @@ -107,7 +108,7 @@ module.exports = function (Plugins) { } function runNpmCommand(command, pkgName, version, callback) { - require('child_process').execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', [command, pkgName + (command === 'install' ? '@' + version : ''), '--no-save'], function (err, stdout) { + cproc.execFile((process.platform === 'win32') ? 'npm.cmd' : 'npm', [command, pkgName + (command === 'install' ? '@' + version : ''), '--save'], function (err, stdout) { if (err) { return callback(err); } diff --git a/test/plugins.js b/test/plugins.js index 40c6c9c97f..e948cbb160 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -5,6 +5,7 @@ var assert = require('assert'); var path = require('path'); var nconf = require('nconf'); var request = require('request'); +var fs = require('fs'); var db = require('./mocks/databasemock'); var plugins = require('../src/plugins'); @@ -128,6 +129,9 @@ describe('Plugins', function () { assert.equal(pluginData.active, false); assert.equal(pluginData.installed, true); + var packageFile = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')); + assert(packageFile.dependencies[pluginName]); + done(); }); }); @@ -160,6 +164,10 @@ describe('Plugins', function () { assert.ifError(err); assert.equal(pluginData.installed, false); assert.equal(pluginData.active, false); + + var packageFile = JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')); + assert(!packageFile.dependencies[pluginName]); + done(); }); });