diff --git a/.travis.yml b/.travis.yml index a28f734796..fbae31b895 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,9 @@ addons: packages: - g++-4.8 node_js: + - "8" - "7" - "6" - - "4" branches: only: - master diff --git a/Gruntfile.js b/Gruntfile.js index 7fa94e3eaf..2d89ad0178 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -73,8 +73,9 @@ module.exports = function (grunt) { watch: { lessUpdated_Client: { files: [ - 'public/*.less', - 'node_modules/nodebb-*/*.less', 'node_modules/nodebb-*/**/*.less', + 'public/less/*.less', + '!public/less/admin/**/*.less', + 'node_modules/nodebb-*/**/*.less', '!node_modules/nodebb-*/node_modules/**', '!node_modules/nodebb-*/.git/**', ], @@ -83,7 +84,12 @@ module.exports = function (grunt) { }, }, lessUpdated_Admin: { - files: ['public/**/*.less'], + files: [ + 'public/less/admin/**/*.less', + 'node_modules/nodebb-*/**/*.less', + '!node_modules/nodebb-*/node_modules/**', + '!node_modules/nodebb-*/.git/**', + ], options: { interval: 1000, }, @@ -91,9 +97,9 @@ module.exports = function (grunt) { clientUpdated: { files: [ 'public/src/**/*.js', - 'node_modules/nodebb-*/*.js', 'node_modules/nodebb-*/**/*.js', + 'node_modules/nodebb-*/**/*.js', '!node_modules/nodebb-*/node_modules/**', - 'node_modules/templates.js/lib/templates.js', + 'node_modules/benchpressjs/build/benchpress.js', '!node_modules/nodebb-*/.git/**', ], options: { @@ -109,7 +115,7 @@ module.exports = function (grunt) { templatesUpdated: { files: [ 'src/views/**/*.tpl', - 'node_modules/nodebb-*/*.tpl', 'node_modules/nodebb-*/**/*.tpl', + 'node_modules/nodebb-*/**/*.tpl', '!node_modules/nodebb-*/node_modules/**', '!node_modules/nodebb-*/.git/**', ], diff --git a/app.js b/app.js index 7a0b7bc2a5..d3b4af0697 100644 --- a/app.js +++ b/app.js @@ -37,6 +37,7 @@ var winston = require('winston'); var path = require('path'); var pkg = require('./package.json'); var file = require('./src/file'); +var debug = require('./src/meta/debugParams')().execArgv.length; global.env = process.env.NODE_ENV || 'production'; @@ -52,6 +53,22 @@ winston.add(winston.transports.Console, { stringify: (!!nconf.get('json-logging')), }); +if (debug) { + var winstonCommon = require('winston/lib/winston/common'); + // Override to use real console.log etc for VSCode debugger + winston.transports.Console.prototype.log = function (level, message, meta, callback) { + const output = winstonCommon.log(Object.assign({}, this, { + level, + message, + meta, + })); + + console[level in console ? level : 'log'](output); + + setImmediate(callback, null, true); + }; +} + // Alternate configuration file support var configFile = path.join(__dirname, '/config.json'); diff --git a/install/web.js b/install/web.js index 16bef11509..58767b776a 100644 --- a/install/web.js +++ b/install/web.js @@ -9,6 +9,8 @@ var less = require('less'); var async = require('async'); var uglify = require('uglify-js'); var nconf = require('nconf'); +var Benchpress = require('benchpressjs'); + var app = express(); var server; @@ -35,7 +37,7 @@ web.install = function (port) { winston.info('Launching web installer on port', port); app.use(express.static('public', {})); - app.engine('tpl', require('templates.js').__express); + app.engine('tpl', Benchpress.__express); app.set('view engine', 'tpl'); app.set('views', path.join(__dirname, '../src/views')); app.use(bodyParser.urlencoded({ diff --git a/package.json b/package.json index f9ece674ea..db23296aa1 100644 --- a/package.json +++ b/package.json @@ -17,44 +17,45 @@ "coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, "dependencies": { - "ace-builds": "^1.2.6", - "async": "2.4.1", - "autoprefixer": "7.1.1", + "ace-builds": "^1.2.8", + "async": "2.5.0", + "autoprefixer": "7.1.3", "bcryptjs": "2.4.3", - "body-parser": "^1.9.0", + "benchpressjs": "^1.0.3", + "body-parser": "^1.17.2", "bootstrap": "^3.3.7", - "chart.js": "^2.4.0", - "colors": "^1.1.0", - "compression": "^1.1.0", + "chart.js": "^2.6.0", + "colors": "^1.1.2", + "compression": "^1.7.0", "connect-ensure-login": "^0.1.1", "connect-flash": "^0.1.1", "connect-mongo": "1.3.2", "connect-multiparty": "^2.0.0", "connect-redis": "3.3.0", - "cookie-parser": "^1.3.3", - "cron": "^1.0.5", + "cookie-parser": "^1.4.3", + "cron": "^1.2.1", "cropperjs": "^0.8.1", - "csurf": "^1.6.1", - "daemon": "~1.1.0", - "express": "^4.14.0", - "express-session": "^1.8.2", + "csurf": "^1.9.0", + "daemon": "^1.1.0", + "express": "^4.15.4", + "express-session": "^1.15.5", "express-useragent": "1.0.7", "html-to-text": "3.3.0", - "ipaddr.js": "^1.5.0", + "ipaddr.js": "^1.5.2", "jimp": "0.2.28", - "jquery": "^3.1.0", - "json-2-csv": "^2.0.22", - "less": "^2.0.0", + "jquery": "^3.2.1", + "json-2-csv": "^2.1.1", + "less": "^2.7.2", "lodash": "^4.17.4", - "logrotate-stream": "^0.2.3", - "lru-cache": "4.0.2", - "mime": "^1.3.4", - "minimist": "^1.1.1", - "mkdirp": "~0.5.0", - "mongodb": "2.2.27", - "morgan": "^1.3.2", - "mousetrap": "^1.5.3", - "nconf": "~0.8.2", + "logrotate-stream": "^0.2.5", + "lru-cache": "4.1.1", + "mime": "^1.4.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "mongodb": "2.2.31", + "morgan": "^1.8.2", + "mousetrap": "^1.6.1", + "nconf": "^0.8.4", "nodebb-plugin-composer-default": "5.0.7", "nodebb-plugin-dbsearch": "2.0.6", "nodebb-plugin-emoji-extended": "1.1.1", @@ -69,34 +70,31 @@ "nodebb-theme-slick": "1.1.1", "nodebb-theme-vanilla": "7.0.0", "nodebb-widget-essentials": "3.0.4", - "nodemailer": "2.6.4", - "nodemailer-sendmail-transport": "1.0.0", - "nodemailer-smtp-transport": "^2.4.1", + "nodemailer": "4.1.0", "passport": "^0.3.0", "passport-local": "1.0.0", - "postcss": "6.0.1", - "postcss-clean": "1.0.2", + "postcss": "6.0.10", + "postcss-clean": "1.0.3", "promise-polyfill": "^6.0.2", "prompt": "^1.0.0", - "redis": "2.7.1", + "redis": "2.8.0", "request": "2.81.0", "rimraf": "2.6.1", - "rss": "^1.0.0", - "sanitize-html": "^1.13.0", - "semver": "^5.1.0", - "serve-favicon": "^2.1.5", - "sitemap": "^1.4.0", - "socket.io": "2.0.1", - "socket.io-client": "2.0.1", - "socket.io-redis": "5.0.1", + "rss": "^1.2.2", + "sanitize-html": "^1.14.0", + "semver": "^5.4.1", + "serve-favicon": "^2.4.3", + "sitemap": "^1.13.0", + "socket.io": "2.0.3", + "socket.io-client": "2.0.3", + "socket.io-redis": "5.2.0", "socketio-wildcard": "2.0.0", "spdx-license-list": "^3.0.1", - "string": "^3.0.0", - "templates.js": "0.3.11", + "string": "^3.3.3", "toobusy-js": "^0.5.1", - "uglify-js": "^3.0.11", - "validator": "7.0.0", - "winston": "^2.1.0", + "uglify-js": "^3.0.28", + "validator": "8.1.0", + "winston": "^2.3.1", "xml": "^1.0.1", "xregexp": "3.2.0", "zxcvbn": "^4.4.2" @@ -106,11 +104,11 @@ "eslint": "^3.12.0", "eslint-config-airbnb-base": "^11.1.0", "eslint-plugin-import": "^2.2.0", - "grunt": "~1.0.0", + "grunt": "^1.0.1", "grunt-contrib-watch": "^1.0.0", "istanbul": "^0.4.2", - "jsdom": "9.12.0", - "mocha": "~3.2.0", + "jsdom": "^11.2.0", + "mocha": "^3.5.0", "mocha-lcov-reporter": "^1.2.0", "xmlhttprequest": "1.8.0", "xmlhttprequest-ssl": "1.5.3" @@ -119,7 +117,7 @@ "url": "https://github.com/NodeBB/NodeBB/issues" }, "engines": { - "node": ">=4" + "node": ">=6" }, "maintainers": [ { diff --git a/public/.eslintrc b/public/.eslintrc index 7c80ae2ec1..865785241c 100644 --- a/public/.eslintrc +++ b/public/.eslintrc @@ -10,7 +10,6 @@ "overrides": true, "componentHandler": true, "bootbox": true, - "templates": true, "Visibility": true, "Tinycon": true, "Promise": true diff --git a/public/images/emails/banneduser.png b/public/images/emails/banneduser.png new file mode 100644 index 0000000000..4eac70d8ca Binary files /dev/null and b/public/images/emails/banneduser.png differ diff --git a/public/images/emails/digestheader.png b/public/images/emails/digestheader.png new file mode 100644 index 0000000000..44ff4dd203 Binary files /dev/null and b/public/images/emails/digestheader.png differ diff --git a/public/images/emails/emailconfirm.png b/public/images/emails/emailconfirm.png new file mode 100644 index 0000000000..d066015bd9 Binary files /dev/null and b/public/images/emails/emailconfirm.png differ diff --git a/public/images/emails/invitation.png b/public/images/emails/invitation.png new file mode 100644 index 0000000000..283372c9fa Binary files /dev/null and b/public/images/emails/invitation.png differ diff --git a/public/images/emails/newtopic.png b/public/images/emails/newtopic.png new file mode 100644 index 0000000000..bc5718563a Binary files /dev/null and b/public/images/emails/newtopic.png differ diff --git a/public/images/emails/nodebb.png b/public/images/emails/nodebb.png new file mode 100644 index 0000000000..9cfe1cd70c Binary files /dev/null and b/public/images/emails/nodebb.png differ diff --git a/public/images/emails/password.png b/public/images/emails/password.png new file mode 100644 index 0000000000..7a7358e082 Binary files /dev/null and b/public/images/emails/password.png differ diff --git a/public/images/emails/triangularbackground.png b/public/images/emails/triangularbackground.png new file mode 100644 index 0000000000..c47ad51810 Binary files /dev/null and b/public/images/emails/triangularbackground.png differ diff --git a/public/images/emails/unreadpost.png b/public/images/emails/unreadpost.png new file mode 100644 index 0000000000..8228478618 Binary files /dev/null and b/public/images/emails/unreadpost.png differ diff --git a/public/language/en-GB/admin/settings/email.json b/public/language/en-GB/admin/settings/email.json index 1e92c88490..97696e1f44 100644 --- a/public/language/en-GB/admin/settings/email.json +++ b/public/language/en-GB/admin/settings/email.json @@ -4,13 +4,21 @@ "address-help": "The following email address refers to the email that the recipient will see in the \"From\" and \"Reply To\" fields.", "from": "From Name", "from-help": "The from name to display in the email.", - "gmail-routing": "Gmail Routing", - "gmail-routing-help1": "There have been reports of Gmail Routing not working on accounts with heightened security. In those scenarios, you will have to configure your GMail account to allow less secure apps.", - "gmail-routing-help2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", - "gmail-transport": "Route emails through a Gmail/Google Apps account", - "gmail-transport.username": "Username", - "gmail-transport.username-help": "Enter the full email address here, especially if you are using a Google Apps managed domain.", - "gmail-transport.password": "Password", + + "smtp-transport": "SMTP Transport", + "smtp-transport.enabled": "Use an external email server to send emails", + "smtp-transport-help": "You can select from a list of well-known services or enter a custom one.", + "smtp-transport.service": "Select a service", + "smtp-transport.service-custom": "Custom Service", + "smtp-transport.service-help": "Select a service name above in order to use the known information about it. Alternatively, select 'Custom Service' and enter the details below.", + "smtp-transport.gmail-warning1": "There have been reports of the Gmail service not working on accounts with heightened security. In those scenarios, you will have to configure your GMail account to allow less secure apps.", + "smtp-transport.gmail-warning2": "For more information about this workaround, please consult this NodeMailer article on the issue. An alternative would be to utilise a third-party emailer plugin such as SendGrid, Mailgun, etc. Browse available plugins here.", + "smtp-transport.host": "SMTP Host", + "smtp-transport.port": "SMTP Port", + "smtp-transport.username": "Username", + "smtp-transport.username-help": "For the Gmail service, enter the full email address here, especially if you are using a Google Apps managed domain.", + "smtp-transport.password": "Password", + "template": "Edit Email Template", "template.select": "Select Email Template", "template.revert": "Revert to Original", diff --git a/public/less/admin/settings.less b/public/less/admin/settings.less index decfc8b7ed..b4fe92432e 100644 --- a/public/less/admin/settings.less +++ b/public/less/admin/settings.less @@ -1,5 +1,5 @@ .settings { - .row { + > .row { margin-bottom: 30px; } diff --git a/public/less/blacklist.less b/public/less/blacklist.less index 5f23cd5698..30add959d5 100644 --- a/public/less/blacklist.less +++ b/public/less/blacklist.less @@ -3,4 +3,5 @@ height: 450px; display: block; border: 1px solid #eee; + padding: 5px 10px; } \ No newline at end of file diff --git a/public/src/admin/admin.js b/public/src/admin/admin.js index 0b6867b81c..3dc22afd62 100644 --- a/public/src/admin/admin.js +++ b/public/src/admin/admin.js @@ -99,6 +99,10 @@ $('#main-menu li').removeClass('active'); $('#main-menu a').removeClass('active').filter('[href="' + url + '"]').each(function () { var menu = $(this); + if (menu.parent().attr('data-link')) { + return; + } + menu .parent().addClass('active') .parents('.menu-item').addClass('active'); diff --git a/public/src/admin/appearance/skins.js b/public/src/admin/appearance/skins.js index f234cac414..7ec56349f7 100644 --- a/public/src/admin/appearance/skins.js +++ b/public/src/admin/appearance/skins.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/appearance/skins', ['translator'], function (translator) { +define('admin/appearance/skins', ['translator', 'benchpress'], function (translator, Benchpress) { var Skins = {}; Skins.init = function () { @@ -52,7 +52,7 @@ define('admin/appearance/skins', ['translator'], function (translator) { Skins.render = function (bootswatch) { var themeContainer = $('#bootstrap_themes'); - templates.parse('admin/partials/theme_list', { + Benchpress.parse('admin/partials/theme_list', { themes: bootswatch.themes.map(function (theme) { return { type: 'bootswatch', diff --git a/public/src/admin/appearance/themes.js b/public/src/admin/appearance/themes.js index 68165268b2..64d7a202fd 100644 --- a/public/src/admin/appearance/themes.js +++ b/public/src/admin/appearance/themes.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/appearance/themes', ['translator'], function (translator) { +define('admin/appearance/themes', ['translator', 'benchpress'], function (translator, Benchpress) { var Themes = {}; Themes.init = function () { @@ -73,7 +73,7 @@ define('admin/appearance/themes', ['translator'], function (translator) { if (!themes.length) { instListEl.append($('
  • ').addClass('no-themes').translateHtml('[[admin/appearance/themes:no-themes]]')); } else { - templates.parse('admin/partials/theme_list', { + Benchpress.parse('admin/partials/theme_list', { themes: themes, }, function (html) { translator.translate(html, function (html) { diff --git a/public/src/admin/extend/plugins.js b/public/src/admin/extend/plugins.js index 8723a02a03..1debd9b168 100644 --- a/public/src/admin/extend/plugins.js +++ b/public/src/admin/extend/plugins.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/extend/plugins', ['jqueryui', 'translator'], function (jqueryui, translator) { +define('admin/extend/plugins', ['jqueryui', 'translator', 'benchpress'], function (jqueryui, translator, Benchpress) { var Plugins = {}; Plugins.init = function () { var pluginsList = $('.plugins'); @@ -21,6 +21,7 @@ define('admin/extend/plugins', ['jqueryui', 'translator'], function (jqueryui, t var pluginEl = $(this).parents('li'); pluginID = pluginEl.attr('data-plugin-id'); var btn = $('#' + pluginID + ' [data-action="toggleActive"]'); + var pluginData = ajaxify.data.installed[pluginEl.attr('data-plugin-index')]; function toggleActivate() { @@ -57,7 +58,7 @@ define('admin/extend/plugins', ['jqueryui', 'translator'], function (jqueryui, t } if (pluginData.license && pluginData.active !== true) { - templates.parse('admin/partials/plugins/license', pluginData, function (html) { + Benchpress.parse('admin/partials/plugins/license', pluginData, function (html) { bootbox.dialog({ title: '[[admin/extend/plugins:license.title]]', message: html, diff --git a/public/src/admin/extend/rewards.js b/public/src/admin/extend/rewards.js index fe1322b8e2..acfcb0bb39 100644 --- a/public/src/admin/extend/rewards.js +++ b/public/src/admin/extend/rewards.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/extend/rewards', ['translator'], function (translator) { +define('admin/extend/rewards', ['translator', 'benchpress'], function (translator, Benchpress) { var rewards = {}; @@ -139,7 +139,7 @@ define('admin/extend/rewards', ['translator'], function (translator) { rewards: available, }; - templates.parse('admin/extend/rewards', 'active', data, function (li) { + Benchpress.parse('admin/extend/rewards', 'active', data, function (li) { translator.translate(li, function (li) { li = $(li); ul.append(li); diff --git a/public/src/admin/general/dashboard.js b/public/src/admin/general/dashboard.js index 2d08c73685..9bc29a2ceb 100644 --- a/public/src/admin/general/dashboard.js +++ b/public/src/admin/general/dashboard.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (semver, Chart, translator) { +define('admin/general/dashboard', ['semver', 'Chart', 'translator', 'benchpress'], function (semver, Chart, translator, Benchpress) { var Admin = {}; var intervals = { rooms: false, @@ -317,7 +317,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s $('[data-action="updateGraph"][data-units="custom"]').on('click', function () { var targetEl = $(this); - templates.parse('admin/partials/pageviews-range-select', {}, function (html) { + Benchpress.parse('admin/partials/pageviews-range-select', {}, function (html) { var modal = bootbox.dialog({ title: '[[admin/general/dashboard:page-views-custom]]', message: html, diff --git a/public/src/admin/general/navigation.js b/public/src/admin/general/navigation.js index 5269c1c2c0..9e44ac0468 100644 --- a/public/src/admin/general/navigation.js +++ b/public/src/admin/general/navigation.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/general/navigation', ['translator', 'iconSelect', 'jqueryui'], function (translator, iconSelect) { +define('admin/general/navigation', ['translator', 'iconSelect', 'benchpress', 'jqueryui'], function (translator, iconSelect, Benchpress) { var navigation = {}; var available; @@ -68,7 +68,7 @@ define('admin/general/navigation', ['translator', 'iconSelect', 'jqueryui'], fun data.enabled = false; data.index = (parseInt($('#enabled').children().last().attr('data-index'), 10) || 0) + 1; - templates.parse('admin/general/navigation', 'navigation', { navigation: [data] }, function (li) { + Benchpress.parse('admin/general/navigation', 'navigation', { navigation: [data] }, function (li) { translator.translate(li, function (li) { li = $(translator.unescape(li)); el.after(li); @@ -76,7 +76,7 @@ define('admin/general/navigation', ['translator', 'iconSelect', 'jqueryui'], fun }); }); - templates.parse('admin/general/navigation', 'enabled', { enabled: [data] }, function (li) { + Benchpress.parse('admin/general/navigation', 'enabled', { enabled: [data] }, function (li) { translator.translate(li, function (li) { li = $(translator.unescape(li)); $('#enabled').append(li); diff --git a/public/src/admin/manage/categories.js b/public/src/admin/manage/categories.js index 2951481411..5d211d00ca 100644 --- a/public/src/admin/manage/categories.js +++ b/public/src/admin/manage/categories.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-serializeobject.min', 'translator'], function (serialize, translator) { +define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-serializeobject.min', 'translator', 'benchpress'], function (serialize, translator, Benchpress) { var Categories = {}; var newCategoryId = -1; var sortables; @@ -45,7 +45,7 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri return app.alertError(err.message); } - templates.parse('admin/partials/categories/create', { + Benchpress.parse('admin/partials/categories/create', { categories: categories, }, function (html) { var modal = bootbox.dialog({ @@ -185,7 +185,7 @@ define('admin/manage/categories', ['vendor/jquery/serializeObject/jquery.ba-seri } function continueRender() { - templates.parse('admin/partials/categories/category-rows', { + Benchpress.parse('admin/partials/categories/category-rows', { cid: parentId, categories: categories, }, function (html) { diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 2115db338e..8ac0ecdf15 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -8,7 +8,8 @@ define('admin/manage/category', [ 'autocomplete', 'translator', 'categorySelector', -], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector) { + 'benchpress', +], function (uploader, iconSelect, colorpicker, autocomplete, translator, categorySelector, Benchpress) { var Category = {}; var modified_categories = {}; @@ -248,7 +249,7 @@ define('admin/manage/category', [ return app.alertError(err.message); } - templates.parse('admin/partials/categories/privileges', { + Benchpress.parse('admin/partials/categories/privileges', { privileges: privileges, }, function (html) { translator.translate(html, function (html) { @@ -407,7 +408,7 @@ define('admin/manage/category', [ callback = categories; categories = ajaxify.data.allCategories; } - templates.parse('admin/partials/categories/select-category', { + Benchpress.parse('admin/partials/categories/select-category', { categories: categories, }, function (html) { translator.translate(html, function (html) { diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js index b334dcb3fb..910db07af7 100644 --- a/public/src/admin/manage/group.js +++ b/public/src/admin/manage/group.js @@ -6,7 +6,8 @@ define('admin/manage/group', [ 'iconSelect', 'admin/modules/colorpicker', 'translator', -], function (memberList, iconSelect, colorpicker, translator) { + 'benchpress', +], function (memberList, iconSelect, colorpicker, translator, Benchpress) { var Groups = {}; Groups.init = function () { @@ -103,7 +104,7 @@ define('admin/manage/group', [ 'icon:text': userLabel.attr('data-usericon-text'), }; - templates.parse('admin/partials/groups/memberlist', 'members', { + Benchpress.parse('admin/partials/groups/memberlist', 'members', { group: { isOwner: ajaxify.data.group.isOwner, members: [member], diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index 5aa7b969b7..5b42e9eb64 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/manage/groups', ['translator'], function (translator) { +define('admin/manage/groups', ['translator', 'benchpress'], function (translator, Benchpress) { var Groups = {}; var intervalId = 0; @@ -94,7 +94,7 @@ define('admin/manage/groups', ['translator'], function (translator) { return app.alertError(err.message); } - templates.parse('admin/manage/groups', 'groups', { + Benchpress.parse('admin/manage/groups', 'groups', { groups: groups, }, function (html) { translator.translate(html, function (html) { diff --git a/public/src/admin/manage/ip-blacklist.js b/public/src/admin/manage/ip-blacklist.js index 1a6760b14f..801c014a49 100644 --- a/public/src/admin/manage/ip-blacklist.js +++ b/public/src/admin/manage/ip-blacklist.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/manage/ip-blacklist', ['Chart'], function (Chart) { +define('admin/manage/ip-blacklist', ['Chart', 'benchpress'], function (Chart, Benchpress) { var Blacklist = {}; Blacklist.init = function () { @@ -32,7 +32,7 @@ define('admin/manage/ip-blacklist', ['Chart'], function (Chart) { return app.alertError(err.message); } - templates.parse('admin/partials/blacklist-validate', data, function (html) { + Benchpress.parse('admin/partials/blacklist-validate', data, function (html) { bootbox.alert(html); }); }); diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index 507067337c..8c22a65720 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -1,7 +1,7 @@ 'use strict'; -define('admin/manage/users', ['translator'], function (translator) { +define('admin/manage/users', ['translator', 'benchpress'], function (translator, Benchpress) { var Users = {}; Users.init = function () { @@ -81,7 +81,7 @@ define('admin/manage/users', ['translator'], function (translator) { return false; // specifically to keep the menu open } - templates.parse('admin/partials/temporary-ban', {}, function (html) { + Benchpress.parse('admin/partials/temporary-ban', {}, function (html) { bootbox.dialog({ className: 'ban-modal', title: '[[user:ban_account]]', @@ -248,7 +248,7 @@ define('admin/manage/users', ['translator'], function (translator) { function handleUserCreate() { $('#createUser').on('click', function () { - templates.parse('admin/partials/create_user_modal', {}, function (html) { + Benchpress.parse('admin/partials/create_user_modal', {}, function (html) { bootbox.dialog({ message: html, title: '[[admin/manage/users:alerts.create]]', @@ -323,7 +323,7 @@ define('admin/manage/users', ['translator'], function (translator) { return app.alertError(err.message); } - templates.parse('admin/manage/users', 'users', data, function (html) { + Benchpress.parse('admin/manage/users', 'users', data, function (html) { translator.translate(html, function (html) { html = $(html); $('.users-table tr').not(':first').remove(); diff --git a/public/src/admin/settings.js b/public/src/admin/settings.js index 5495e3b71b..6198984e19 100644 --- a/public/src/admin/settings.js +++ b/public/src/admin/settings.js @@ -17,6 +17,13 @@ define('admin/settings', ['uploader'], function (uploader) { $(this).prepend(''); $('.section-content ul').append('
  • ' + header + '
  • '); }); + + var scrollTo = $('a[name="' + window.location.hash.replace('#', '') + '"]'); + if (scrollTo.length) { + $('html, body').animate({ + scrollTop: (scrollTo.offset().top) + 'px', + }, 400); + } }; Settings.prepare = function (callback) { diff --git a/public/src/admin/settings/email.js b/public/src/admin/settings/email.js index d6a1983102..336abf34fa 100644 --- a/public/src/admin/settings/email.js +++ b/public/src/admin/settings/email.js @@ -9,11 +9,13 @@ define('admin/settings/email', ['ace/ace', 'admin/settings'], function (ace) { configureEmailTester(); configureEmailEditor(); handleDigestHourChange(); + handleSmtpServiceChange(); $(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange); $(window).on('action:admin.settingsSaved', function () { socket.emit('admin.user.restartJobs'); }); + $('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange); }; function configureEmailTester() { @@ -100,5 +102,10 @@ define('admin/settings/email', ['ace/ace', 'admin/settings'], function (ace) { }); } + function handleSmtpServiceChange() { + var isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp'; + $('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom); + } + return module; }); diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index e2303c501c..c655cdca96 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -10,13 +10,15 @@ $(document).ready(function () { var ajaxifyTimer; var translator; + var Benchpress; var retry = true; var previousBodyClass = ''; // Dumb hack to fool ajaxify into thinking translator is still a global // When ajaxify is migrated to a require.js module, then this can be merged into the "define" call - require(['translator'], function (_translator) { + require(['translator', 'benchpress'], function (_translator, _Benchpress) { translator = _translator; + Benchpress = _Benchpress; }); $(window).on('popstate', function (ev) { @@ -174,7 +176,7 @@ $(document).ready(function () { function renderTemplate(url, tpl_url, data, callback) { $(window).trigger('action:ajaxify.loadingTemplates', {}); - templates.parse(tpl_url, data, function (template) { + Benchpress.parse(tpl_url, data, function (template) { translator.translate(template, function (translatedTemplate) { translatedTemplate = translator.unescape(translatedTemplate); $('body').removeClass(previousBodyClass).addClass(data.bodyClass); @@ -328,20 +330,10 @@ $(document).ready(function () { }; ajaxify.loadTemplate = function (template, callback) { - if (templates.cache[template]) { - callback(templates.cache[template]); - } else { - $.ajax({ - url: config.relative_path + '/assets/templates/' + template + '.tpl?' + config['cache-buster'], - type: 'GET', - success: function (data) { - callback(data.toString()); - }, - error: function (error) { - throw new Error('Unable to load template: ' + template + ' (' + error.statusText + ')'); - }, - }); - } + require([config.relative_path + '/assets/templates/' + template + '.jst'], callback, function (err) { + console.error('Unable to load template: ' + template); + throw err; + }); }; function ajaxifyAnchors() { @@ -424,7 +416,9 @@ $(document).ready(function () { }); } - templates.registerLoader(ajaxify.loadTemplate); + require(['benchpress'], function (Benchpress) { + Benchpress.registerLoader(ajaxify.loadTemplate); + }); if (window.history && window.history.pushState) { // Progressive Enhancement, ajaxify available only to modern browsers @@ -432,9 +426,4 @@ $(document).ready(function () { } app.load(); - - $('[type="text/tpl"][data-template]').each(function () { - templates.cache[$(this).attr('data-template')] = $('
    ').html($(this).html()).text(); - $(this).parent().remove(); - }); }); diff --git a/public/src/app.js b/public/src/app.js index ccd324b68c..913360c615 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -11,7 +11,21 @@ app.cacheBuster = null; (function () { var showWelcomeMessage = !!utils.params().loggedin; - templates.setGlobal('config', config); + require(['benchpress'], function (Benchpress) { + Benchpress.setGlobal('config', config); + if (Object.defineProperty) { + Object.defineProperty(window, 'templates', { + configurable: true, + enumerable: true, + get: function () { + console.warn('[deprecated] Accessing benchpress (formerly known as templates.js) globally is deprecated. Use `require(["benchpress"], function (Benchpress) { ... })` instead'); + return Benchpress; + }, + }); + } else { + window.templates = Benchpress; + } + }); app.cacheBuster = config['cache-buster']; @@ -35,7 +49,8 @@ app.cacheBuster = null; app.handleSearch(); } - $('body').on('click', '#new_topic', function () { + $('body').on('click', '#new_topic', function (e) { + e.preventDefault(); app.newTopic(); }); @@ -89,7 +104,8 @@ app.cacheBuster = null; }); }; - app.logout = function () { + app.logout = function (e) { + e.preventDefault(); $(window).trigger('action:app.logout'); /* @@ -608,7 +624,7 @@ app.cacheBuster = null; }; app.parseAndTranslate = function (template, blockName, data, callback) { - require(['translator'], function (translator) { + require(['translator', 'benchpress'], function (translator, Benchpress) { function translate(html, callback) { translator.translate(html, function (translatedHTML) { translatedHTML = translator.unescape(translatedHTML); @@ -617,13 +633,13 @@ app.cacheBuster = null; } if (typeof blockName === 'string') { - templates.parse(template, blockName, data, function (html) { + Benchpress.parse(template, blockName, data, function (html) { translate(html, callback); }); } else { callback = data; data = blockName; - templates.parse(template, data, function (html) { + Benchpress.parse(template, data, function (html) { translate(html, callback); }); } diff --git a/public/src/client/account/edit.js b/public/src/client/account/edit.js index 0f9ae7cf96..7fa2026823 100644 --- a/public/src/client/account/edit.js +++ b/public/src/client/account/edit.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/account/edit', ['forum/account/header', 'translator', 'components', 'pictureCropper'], function (header, translator, components, pictureCropper) { +define('forum/account/edit', ['forum/account/header', 'translator', 'components', 'pictureCropper', 'benchpress'], function (header, translator, components, pictureCropper, Benchpress) { var AccountEdit = {}; AccountEdit.init = function () { @@ -84,7 +84,7 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components' return memo || cur.type === 'uploaded'; }, false); - templates.parse('partials/modals/change_picture_modal', { + Benchpress.parse('partials/modals/change_picture_modal', { pictures: pictures, uploaded: uploaded, icon: { text: ajaxify.data['icon:text'], bgColor: ajaxify.data['icon:bgColor'] }, @@ -229,7 +229,7 @@ define('forum/account/edit', ['forum/account/header', 'translator', 'components' modal.find('[data-action="upload-url"]').on('click', function () { modal.modal('hide'); - templates.parse('partials/modals/upload_picture_from_url_modal', {}, function (html) { + Benchpress.parse('partials/modals/upload_picture_from_url_modal', {}, function (html) { translator.translate(html, function (html) { var uploadModal = $(html); uploadModal.modal('show'); diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js index 0db3d77b7d..e88eda06f0 100644 --- a/public/src/client/account/header.js +++ b/public/src/client/account/header.js @@ -6,7 +6,8 @@ define('forum/account/header', [ 'pictureCropper', 'components', 'translator', -], function (coverPhoto, pictureCropper, components, translator) { + 'benchpress', +], function (coverPhoto, pictureCropper, components, translator, Benchpress) { var AccountHeader = {}; var isAdminOrSelfOrGlobalMod; @@ -114,7 +115,7 @@ define('forum/account/header', [ } function banAccount() { - templates.parse('admin/partials/temporary-ban', {}, function (html) { + Benchpress.parse('admin/partials/temporary-ban', {}, function (html) { bootbox.dialog({ className: 'ban-modal', title: '[[user:ban_account]]', diff --git a/public/src/client/categories.js b/public/src/client/categories.js index ff1dfa3401..8357ff8a29 100644 --- a/public/src/client/categories.js +++ b/public/src/client/categories.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/categories', ['components', 'translator'], function (components, translator) { +define('forum/categories', ['components', 'translator', 'benchpress'], function (components, translator, Benchpress) { var categories = {}; $(window).on('action:ajaxify.start', function (ev, data) { @@ -58,7 +58,7 @@ define('forum/categories', ['components', 'translator'], function (components, t } function parseAndTranslate(posts, callback) { - templates.parse('categories', '(categories.)?posts', { categories: { posts: posts } }, function (html) { + Benchpress.parse('partials/categories/lastpost', 'posts', { posts: posts }, function (html) { translator.translate(html, function (translatedHTML) { translatedHTML = $(translatedHTML); translatedHTML.find('.post-content img:not(.not-responsive)').addClass('img-responsive'); diff --git a/public/src/client/category.js b/public/src/client/category.js index c852d199ff..69bb615b75 100644 --- a/public/src/client/category.js +++ b/public/src/client/category.js @@ -12,7 +12,8 @@ define('forum/category', [ 'topicSelect', 'forum/pagination', 'storage', -], function (infinitescroll, share, navigator, categoryTools, sort, components, translator, topicSelect, pagination, storage) { + 'benchpress', +], function (infinitescroll, share, navigator, categoryTools, sort, components, translator, topicSelect, pagination, storage, Benchpress) { var Category = {}; $(window).on('action:ajaxify.start', function (ev, data) { @@ -204,7 +205,7 @@ define('forum/category', [ var editable = !!$('.thread-tools').length; - templates.parse('category', 'topics', { + Benchpress.parse('category', 'topics', { privileges: { editable: editable }, showSelect: editable, topics: [topic], @@ -267,7 +268,7 @@ define('forum/category', [ var topics = $('[component="category/topic"]'); var afterEl = direction > 0 ? topics.last() : topics.first(); - var after = (parseInt(afterEl.attr('data-index'), 10) || 0) + 1; + var after = (parseInt(afterEl.attr('data-index'), 10) || 0) + (direction > 0 ? 1 : 0); loadTopicsAfter(after, direction); }; @@ -284,8 +285,7 @@ define('forum/category', [ cid: ajaxify.data.cid, after: after, direction: direction, - author: params.author, - tag: params.tag, + query: params, categoryTopicSort: config.categoryTopicSort, }, function (data, done) { if (data.topics && data.topics.length) { diff --git a/public/src/client/chats.js b/public/src/client/chats.js index c7cb1921ae..932be20281 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -8,7 +8,8 @@ define('forum/chats', [ 'forum/chats/recent', 'forum/chats/search', 'forum/chats/messages', -], function (components, translator, mousetrap, recentChats, search, messages) { + 'benchpress', +], function (components, translator, mousetrap, recentChats, search, messages, Benchpress) { var Chats = { initialised: false, }; @@ -23,6 +24,8 @@ define('forum/chats', [ Chats.addGlobalEventListeners(); } + recentChats.init(); + Chats.addEventListeners(); Chats.createTagsInput($('[component="chat/messages"] .users-tag-input'), ajaxify.data); Chats.createAutoComplete($('[component="chat/input"]')); @@ -48,15 +51,6 @@ define('forum/chats', [ }; Chats.addEventListeners = function () { - $('[component="chat/recent"]').on('click', '[component="chat/leave"]', function () { - Chats.leave($(this).parents('[data-roomid]')); - return false; - }); - - $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function () { - Chats.switchChat($(this).attr('data-roomid')); - }); - Chats.addSendHandlers(ajaxify.data.roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]')); $('[data-action="pop-out"]').on('click', function () { @@ -79,8 +73,6 @@ define('forum/chats', [ Chats.addEditDeleteHandler(components.get('chat/messages'), ajaxify.data.roomId); - recentChats.init(); - Chats.addRenameHandler(ajaxify.data.roomId, $('[component="chat/room/name"]')); Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); Chats.addCharactersLeftHandler(components.get('chat/input')); @@ -319,7 +311,36 @@ define('forum/chats', [ }; Chats.switchChat = function (roomid) { - ajaxify.go('user/' + ajaxify.data.userslug + '/chats/' + roomid); + var url = 'user/' + ajaxify.data.userslug + '/chats/' + roomid; + if (self.fetch) { + fetch(config.relative_path + '/api/' + url, { credentials: 'include' }) + .then(function (response) { + if (response.ok) { + response.json().then(function (payload) { + app.parseAndTranslate('partials/chats/message-window', payload, function (html) { + components.get('chat/main-wrapper').html(html); + Chats.resizeMainWindow(); + ajaxify.data = payload; + Chats.setActive(); + Chats.addEventListeners(); + + if (history.pushState) { + history.pushState({ + url: 'user/' + payload.userslug + '/chats/' + payload.roomId, + }, null, window.location.protocol + '//' + window.location.host + config.relative_path + '/user/' + payload.userslug + '/chats/' + payload.roomId); + } + }); + }); + } else { + console.warn('[search] Received ' + response.status); + } + }) + .catch(function (error) { + console.warn('[search] ' + error.message); + }); + } else { + ajaxify.go(url); + } }; Chats.addGlobalEventListeners = function () { @@ -346,7 +367,7 @@ define('forum/chats', [ roomEl.addClass('unread'); } else { var recentEl = components.get('chat/recent'); - templates.parse('partials/chats/recent_room', { + Benchpress.parse('partials/chats/recent_room', { rooms: { roomId: data.roomId, lastUser: data.message.fromUser, @@ -375,17 +396,19 @@ define('forum/chats', [ Chats.resizeMainWindow = function () { var messagesList = $('.expanded-chat .chat-content'); + var searchHeight = $('.chat-search').height(); + var searchListHeight = $('[component="chat/search/list"]').outerHeight(true) - $('[component="chat/search/list"]').height(); + var fromTop = components.get('chat/recent').offset().top; if (messagesList.length) { var margin = $('.expanded-chat ul').outerHeight(true) - $('.expanded-chat ul').height(); var inputHeight = $('.chat-input').outerHeight(true); - var fromTop = messagesList.offset().top; - var searchHeight = $('.chat-search').height(); - var searchListHeight = $('[component="chat/search/list"]').outerHeight(true) - $('[component="chat/search/list"]').height(); messagesList.height($(window).height() - (fromTop + inputHeight + (margin * 4))); components.get('chat/recent').height($('.expanded-chat').height() - (searchHeight + searchListHeight)); $('[component="chat/search/list"]').css('max-height', (components.get('chat/recent').height() / 2) + 'px'); + } else { + components.get('chat/recent').height($(window).height() - (fromTop + searchHeight + searchListHeight)); } Chats.setActive(); diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index 27f19ca33c..8f8caa2a1f 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/chats/messages', ['components', 'sounds', 'translator'], function (components, sounds, translator) { +define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpress'], function (components, sounds, translator, Benchpress) { var messages = {}; messages.sendMessage = function (roomId, inputEl) { @@ -86,7 +86,7 @@ define('forum/chats/messages', ['components', 'sounds', 'translator'], function messages.parseMessage = function (data, callback) { - templates.parse('partials/chats/message' + (Array.isArray(data) ? 's' : ''), { + Benchpress.parse('partials/chats/message' + (Array.isArray(data) ? 's' : ''), { messages: data, }, function (html) { translator.translate(html, callback); diff --git a/public/src/client/chats/recent.js b/public/src/client/chats/recent.js index f52f95fbbf..51650c8795 100644 --- a/public/src/client/chats/recent.js +++ b/public/src/client/chats/recent.js @@ -5,12 +5,28 @@ define('forum/chats/recent', function () { var recent = {}; recent.init = function () { - $('[component="chat/recent"]').on('scroll', function () { - var $this = $(this); - var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; - if ($this.scrollTop() > bottom) { - loadMoreRecentChats(); - } + require(['forum/chats'], function (Chats) { + $('[component="chat/recent"]').on('click', '[component="chat/leave"]', function () { + Chats.leave($(this).parents('[data-roomid]')); + return false; + }); + + $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function () { + var env = utils.findBootstrapEnvironment(); + if (env === 'xs' || env === 'sm') { + app.openChat($(this).attr('data-roomid')); + } else { + Chats.switchChat($(this).attr('data-roomid')); + } + }); + + $('[component="chat/recent"]').on('scroll', function () { + var $this = $(this); + var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; + if ($this.scrollTop() > bottom) { + loadMoreRecentChats(); + } + }); }); }; diff --git a/public/src/client/flags/detail.js b/public/src/client/flags/detail.js index c32bae1e5e..7b8691f37d 100644 --- a/public/src/client/flags/detail.js +++ b/public/src/client/flags/detail.js @@ -2,7 +2,7 @@ /* globals define */ -define('forum/flags/detail', ['forum/flags/list', 'components', 'translator'], function (FlagsList, components, translator) { +define('forum/flags/detail', ['forum/flags/list', 'components', 'translator', 'benchpress'], function (FlagsList, components, translator, Benchpress) { var Flags = {}; Flags.init = function () { @@ -48,7 +48,7 @@ define('forum/flags/detail', ['forum/flags/list', 'components', 'translator'], f }; Flags.reloadNotes = function (notes) { - templates.parse('flags/detail', 'notes', { + Benchpress.parse('flags/detail', 'notes', { notes: notes, }, function (html) { var wrapperEl = components.get('flag/notes'); @@ -60,7 +60,7 @@ define('forum/flags/detail', ['forum/flags/list', 'components', 'translator'], f }; Flags.reloadHistory = function (history) { - templates.parse('flags/detail', 'history', { + Benchpress.parse('flags/detail', 'history', { history: history, }, function (html) { translator.translate(html, function (translated) { diff --git a/public/src/client/groups/list.js b/public/src/client/groups/list.js index 882d59eef1..72536a1352 100644 --- a/public/src/client/groups/list.js +++ b/public/src/client/groups/list.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/groups/list', ['forum/infinitescroll'], function (infinitescroll) { +define('forum/groups/list', ['forum/infinitescroll', 'benchpress'], function (infinitescroll, Benchpress) { var Groups = {}; Groups.init = function () { @@ -44,7 +44,7 @@ define('forum/groups/list', ['forum/infinitescroll'], function (infinitescroll) after: $('[component="groups/container"]').attr('data-nextstart'), }, function (data, done) { if (data && data.groups.length) { - templates.parse('partials/groups/list', { + Benchpress.parse('partials/groups/list', { groups: data.groups, }, function (html) { $('#groups-list').append(html); @@ -78,7 +78,7 @@ define('forum/groups/list', ['forum/infinitescroll'], function (infinitescroll) groups = groups.filter(function (group) { return group.name !== 'registered-users' && group.name !== 'guests'; }); - templates.parse('partials/groups/list', { + Benchpress.parse('partials/groups/list', { groups: groups, }, function (html) { groupsEl.empty().append(html); diff --git a/public/src/client/login.js b/public/src/client/login.js index 0ee61e48f7..1a8f96861e 100644 --- a/public/src/client/login.js +++ b/public/src/client/login.js @@ -74,6 +74,7 @@ define('forum/login', [], function () { } else { $('#content #username').focus(); } + $('#content #noscript').val('false'); }; return Login; diff --git a/public/src/client/register.js b/public/src/client/register.js index 8070263906..4289c75d10 100644 --- a/public/src/client/register.js +++ b/public/src/client/register.js @@ -16,6 +16,7 @@ define('forum/register', ['translator', 'zxcvbn'], function (translator, zxcvbn) handleLanguageOverride(); $('#referrer').val(app.previousUrl); + $('#content #noscript').val('false'); email.on('blur', function () { if (email.val().length) { diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index 22a6c69d6c..ac4c41e96b 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -9,7 +9,8 @@ define('forum/topic/events', [ 'forum/topic/images', 'components', 'translator', -], function (postTools, threadTools, posts, images, components, translator) { + 'benchpress', +], function (postTools, threadTools, posts, images, components, translator, Benchpress) { var Events = {}; var events = { @@ -139,7 +140,7 @@ define('forum/topic/events', [ editedISO: utils.toISOString(data.post.edited), }; - templates.parse('partials/topic/post-editor', editData, function (html) { + Benchpress.parse('partials/topic/post-editor', editData, function (html) { translator.translate(html, function (translated) { html = $(translated); editorEl.replaceWith(html); @@ -150,7 +151,7 @@ define('forum/topic/events', [ }); if (data.topic.tags && tagsUpdated(data.topic.tags)) { - templates.parse('partials/post_bar', 'tags', { tags: data.topic.tags }, function (html) { + Benchpress.parse('partials/post_bar', 'tags', { tags: data.topic.tags }, function (html) { var tags = $('.tags'); tags.fadeOut(250, function () { diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 90b845538e..934a3942b7 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -8,7 +8,8 @@ define('forum/topic/postTools', [ 'translator', 'forum/topic/votes', 'forum/topic/move-post', -], function (share, navigator, components, translator, votes, movePost) { + 'benchpress', +], function (share, navigator, components, translator, votes, movePost, Benchpress) { var PostTools = {}; var staleReplyAnyway = false; @@ -44,7 +45,7 @@ define('forum/topic/postTools', [ } data.posts.display_move_tools = data.posts.display_move_tools && index !== 0; - templates.parse('partials/topic/post-menu-list', data, function (html) { + Benchpress.parse('partials/topic/post-menu-list', data, function (html) { translator.translate(html, function (html) { dropdownMenu.html(html); $(window).trigger('action:post.tools.load'); @@ -85,7 +86,8 @@ define('forum/topic/postTools', [ onReplyClicked($(this), tid); }); - $('.topic').on('click', '[component="topic/reply"]', function () { + $('.topic').on('click', '[component="topic/reply"]', function (e) { + e.preventDefault(); onReplyClicked($(this), tid); }); diff --git a/public/src/client/topic/threadTools.js b/public/src/client/topic/threadTools.js index 70065678f0..e220e6305d 100644 --- a/public/src/client/topic/threadTools.js +++ b/public/src/client/topic/threadTools.js @@ -7,7 +7,8 @@ define('forum/topic/threadTools', [ 'forum/topic/delete-posts', 'components', 'translator', -], function (fork, move, deletePosts, components, translator) { + 'benchpress', +], function (fork, move, deletePosts, components, translator, Benchpress) { var ThreadTools = {}; ThreadTools.init = function (tid) { @@ -138,7 +139,7 @@ define('forum/topic/threadTools', [ return app.alertError(err); } - templates.parse('partials/topic/topic-menu-list', data, function (html) { + Benchpress.parse('partials/topic/topic-menu-list', data, function (html) { translator.translate(html, function (html) { dropdownMenu.html(html); $(window).trigger('action:topic.tools.load'); diff --git a/public/src/client/topic/votes.js b/public/src/client/topic/votes.js index 99ca469fe3..9bd20fc1a6 100644 --- a/public/src/client/topic/votes.js +++ b/public/src/client/topic/votes.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/topic/votes', ['components', 'translator'], function (components, translator) { +define('forum/topic/votes', ['components', 'translator', 'benchpress'], function (components, translator, Benchpress) { var Votes = {}; Votes.addVoteHandler = function () { @@ -92,7 +92,7 @@ define('forum/topic/votes', ['components', 'translator'], function (components, return app.alertError(err.message); } - templates.parse('partials/modals/votes_modal', data, function (html) { + Benchpress.parse('partials/modals/votes_modal', data, function (html) { translator.translate(html, function (translated) { var dialog = bootbox.dialog({ title: 'Voters', diff --git a/public/src/client/users.js b/public/src/client/users.js index ab9ed7f5c5..3d541e0863 100644 --- a/public/src/client/users.js +++ b/public/src/client/users.js @@ -1,7 +1,7 @@ 'use strict'; -define('forum/users', ['translator'], function (translator) { +define('forum/users', ['translator', 'benchpress'], function (translator, Benchpress) { var Users = {}; var searchTimeoutID = 0; @@ -98,11 +98,11 @@ define('forum/users', ['translator'], function (translator) { } function renderSearchResults(data) { - templates.parse('partials/paginator', { pagination: data.pagination }, function (html) { + Benchpress.parse('partials/paginator', { pagination: data.pagination }, function (html) { $('.pagination-container').replaceWith(html); }); - templates.parse('users', 'users', data, function (html) { + Benchpress.parse('users', 'users', data, function (html) { translator.translate(html, function (translated) { translated = $(translated); $('#users-container').html(translated); diff --git a/public/src/modules/alerts.js b/public/src/modules/alerts.js index 3e55ce1ff1..ccd90eb790 100644 --- a/public/src/modules/alerts.js +++ b/public/src/modules/alerts.js @@ -1,7 +1,7 @@ 'use strict'; -define('alerts', ['translator', 'components'], function (translator, components) { +define('alerts', ['translator', 'components', 'benchpress'], function (translator, components, Benchpress) { var module = {}; module.alert = function (params) { @@ -19,7 +19,7 @@ define('alerts', ['translator', 'components'], function (translator, components) }; function createNew(params) { - templates.parse('alert', params, function (alertTpl) { + Benchpress.parse('alert', params, function (alertTpl) { translator.translate(alertTpl, function (translatedHTML) { var alert = $('#' + params.alert_id); if (alert.length) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index d245e5ce23..96b94dbf8b 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -10,7 +10,8 @@ define('chat', [ 'forum/chats/messages', 'translator', 'scrollStop', -], function (components, taskbar, S, sounds, Chats, ChatsMessages, translator, scrollStop) { + 'benchpress', +], function (components, taskbar, S, sounds, Chats, ChatsMessages, translator, scrollStop, Benchpress) { var module = {}; var newMessage = false; @@ -125,7 +126,7 @@ define('chat', [ return room.teaser; }); - templates.parse('partials/chats/dropdown', { + Benchpress.parse('partials/chats/dropdown', { rooms: rooms, }, function (html) { translator.translate(html, function (translated) { diff --git a/public/src/modules/flags.js b/public/src/modules/flags.js index f95953c65e..afd63c6ab2 100644 --- a/public/src/modules/flags.js +++ b/public/src/modules/flags.js @@ -1,7 +1,7 @@ 'use strict'; -define('flags', [], function () { +define('flags', ['benchpress'], function (Benchpress) { var Flag = {}; var flagModal; var flagCommit; @@ -31,7 +31,7 @@ define('flags', [], function () { }; function parseModal(tplData, callback) { - templates.parse('partials/modals/flag_modal', tplData, function (html) { + Benchpress.parse('partials/modals/flag_modal', tplData, function (html) { require(['translator'], function (translator) { translator.translate(html, callback); }); diff --git a/public/src/modules/helpers.js b/public/src/modules/helpers.js index e4759935b6..28f853ed60 100644 --- a/public/src/modules/helpers.js +++ b/public/src/modules/helpers.js @@ -3,15 +3,13 @@ (function (factory) { if (typeof module === 'object' && module.exports) { var relative_path = require('nconf').get('relative_path'); - module.exports = factory(require('../utils'), require('templates.js'), require('string'), relative_path); + module.exports = factory(require('../utils'), require('benchpressjs'), require('string'), relative_path); } else if (typeof define === 'function' && define.amd) { - define('helpers', ['string'], function (string) { - return factory(utils, templates, string, config.relative_path); + define('helpers', ['benchpress', 'string'], function (Benchpress, string) { + return factory(utils, Benchpress, string, config.relative_path); }); - } else { - window.helpers = factory(utils, templates, window.String, config.relative_path); } -}(function (utils, templates, S, relative_path) { +}(function (utils, Benchpress, S, relative_path) { var helpers = { displayMenuItem: displayMenuItem, buildMetaTag: buildMetaTag, @@ -30,8 +28,13 @@ renderDigestAvatar: renderDigestAvatar, userAgentIcons: userAgentIcons, register: register, + __escape: identity, }; + function identity(str) { + return str; + } + function displayMenuItem(data, index) { var item = data.navigation[index]; if (!item) { @@ -270,7 +273,7 @@ function register() { Object.keys(helpers).forEach(function (helperName) { - templates.registerHelper(helperName, helpers[helperName]); + Benchpress.registerHelper(helperName, helpers[helperName]); }); } diff --git a/public/src/modules/iconSelect.js b/public/src/modules/iconSelect.js index 96a38e0df6..2b9fcb4865 100644 --- a/public/src/modules/iconSelect.js +++ b/public/src/modules/iconSelect.js @@ -1,7 +1,7 @@ 'use strict'; -define('iconSelect', function () { +define('iconSelect', ['benchpress'], function (Benchpress) { var iconSelect = {}; iconSelect.init = function (el, onModified) { @@ -19,7 +19,7 @@ define('iconSelect', function () { } } - templates.parse('partials/fontawesome', {}, function (html) { + Benchpress.parse('partials/fontawesome', {}, function (html) { html = $(html); html.find('.fa-icons').prepend($('')); diff --git a/public/src/modules/notifications.js b/public/src/modules/notifications.js index f99e1b8de4..a215c19475 100644 --- a/public/src/modules/notifications.js +++ b/public/src/modules/notifications.js @@ -1,7 +1,7 @@ 'use strict'; -define('notifications', ['sounds', 'translator', 'components', 'navigator'], function (sounds, translator, components, navigator) { +define('notifications', ['sounds', 'translator', 'components', 'navigator', 'benchpress'], function (sounds, translator, components, navigator, Benchpress) { var Notifications = {}; var unreadNotifs = {}; @@ -143,7 +143,7 @@ define('notifications', ['sounds', 'translator', 'components', 'navigator'], fun } translator.toggleTimeagoShorthand(); - templates.parse('partials/notifications_list', { notifications: notifs }, function (html) { + Benchpress.parse('partials/notifications_list', { notifications: notifs }, function (html) { notifList.translateHtml(html); }); }); diff --git a/public/src/modules/pictureCropper.js b/public/src/modules/pictureCropper.js index d413c3d156..3bd8cf1516 100644 --- a/public/src/modules/pictureCropper.js +++ b/public/src/modules/pictureCropper.js @@ -1,7 +1,7 @@ 'use strict'; -define('pictureCropper', ['translator', 'cropper'], function (translator, cropper) { +define('pictureCropper', ['translator', 'cropper', 'benchpress'], function (translator, cropper, Benchpress) { var module = {}; module.show = function (data, callback) { @@ -32,7 +32,7 @@ define('pictureCropper', ['translator', 'cropper'], function (translator, croppe module.handleImageCrop = function (data, callback) { $('#crop-picture-modal').remove(); - templates.parse('modals/crop_picture', { + Benchpress.parse('modals/crop_picture', { url: data.url, }, function (cropperHtml) { translator.translate(cropperHtml, function (translated) { @@ -179,7 +179,7 @@ define('pictureCropper', ['translator', 'cropper'], function (translator, croppe } function parseModal(tplVals, callback) { - templates.parse('partials/modals/upload_file_modal', tplVals, function (html) { + Benchpress.parse('partials/modals/upload_file_modal', tplVals, function (html) { translator.translate(html, callback); }); } diff --git a/public/src/modules/taskbar.js b/public/src/modules/taskbar.js index 49c57c5aa1..15769ced56 100644 --- a/public/src/modules/taskbar.js +++ b/public/src/modules/taskbar.js @@ -1,13 +1,13 @@ 'use strict'; -define('taskbar', function () { +define('taskbar', ['benchpress'], function (Benchpress) { var taskbar = {}; taskbar.init = function () { var self = this; - templates.parse('modules/taskbar', {}, function (html) { + Benchpress.parse('modules/taskbar', {}, function (html) { self.taskbar = $(html); self.tasklist = self.taskbar.find('ul'); $(document.body).append(self.taskbar); diff --git a/public/src/modules/topicSelect.js b/public/src/modules/topicSelect.js index 185e5e36a0..3040b36e98 100644 --- a/public/src/modules/topicSelect.js +++ b/public/src/modules/topicSelect.js @@ -39,6 +39,9 @@ define('topicSelect', ['components'], function (components) { TopicSelect.getSelectedTids = function () { var tids = []; + if (!topicsContainer) { + return tids; + } topicsContainer.find('[component="category/topic"].selected').each(function () { tids.push($(this).attr('data-tid')); }); @@ -46,8 +49,10 @@ define('topicSelect', ['components'], function (components) { }; TopicSelect.unselectAll = function () { - topicsContainer.find('[component="category/topic"].selected').removeClass('selected'); - topicsContainer.find('[component="topic/select"]').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true); + if (topicsContainer) { + topicsContainer.find('[component="category/topic"].selected').removeClass('selected'); + topicsContainer.find('[component="topic/select"]').toggleClass('fa-check-square-o', false).toggleClass('fa-square-o', true); + } }; function selectRange(clickedTid) { diff --git a/public/src/modules/uploader.js b/public/src/modules/uploader.js index aca2460ee5..6fd4a09336 100644 --- a/public/src/modules/uploader.js +++ b/public/src/modules/uploader.js @@ -1,7 +1,7 @@ 'use strict'; -define('uploader', ['translator'], function (translator) { +define('uploader', ['translator', 'benchpress'], function (translator, Benchpress) { var module = {}; module.open = function (route, params, fileSize, callback) { @@ -102,7 +102,7 @@ define('uploader', ['translator'], function (translator) { } function parseModal(tplVals, callback) { - templates.parse('partials/modals/upload_file_modal', tplVals, function (html) { + Benchpress.parse('partials/modals/upload_file_modal', tplVals, function (html) { translator.translate(html, callback); }); } diff --git a/public/src/utils.js b/public/src/utils.js index 6e7f0400d1..910082a430 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -447,13 +447,5 @@ }; } - // DEPRECATED: remove in 1.6 - if (typeof String.prototype.rtrim !== 'function') { - String.prototype.rtrim = function () { - console.warn('[deprecated] `String.prototype.rtrim` is deprecated as of NodeBB v1.5; use `utils.rtrim` instead.'); - return utils.rtrim(this); - }; - } - return utils; })); diff --git a/src/admin/search.js b/src/admin/search.js index 6ffd9db768..dfc9034658 100644 --- a/src/admin/search.js +++ b/src/admin/search.js @@ -14,10 +14,12 @@ function filterDirectories(directories) { // get the relative path return dir.replace(/^.*(admin.*?).tpl$/, '$1'); }).filter(function (dir) { + // exclude .jst files // exclude partials // only include subpaths // exclude category.tpl, group.tpl, category-analytics.tpl - return !dir.includes('/partials/') && + return !dir.endsWith('.jst') && + !dir.includes('/partials/') && /\/.*\//.test(dir) && !/manage\/(category|group|category-analytics)$/.test(dir); }); diff --git a/src/categories.js b/src/categories.js index 4fd6ec3b1f..1487b543e2 100644 --- a/src/categories.js +++ b/src/categories.js @@ -35,17 +35,13 @@ Categories.getCategoryById = function (data, callback) { return next(new Error('[[error:invalid-cid]]')); } category = categories[0]; - + data.category = category; async.parallel({ topics: function (next) { Categories.getCategoryTopics(data, next); }, topicCount: function (next) { - if (Array.isArray(data.set)) { - db.sortedSetIntersectCard(data.set, next); - } else { - next(null, category.topic_count); - } + Categories.getTopicCount(data, next); }, isIgnored: function (next) { Categories.isIgnored([data.cid], data.uid, next); diff --git a/src/categories/delete.js b/src/categories/delete.js index a4215b5ff2..87ae9813d9 100644 --- a/src/categories/delete.js +++ b/src/categories/delete.js @@ -19,7 +19,7 @@ module.exports = function (Categories) { }, { alwaysStartAt: 0 }, next); }, function (next) { - Categories.getPinnedTids('cid:' + cid + ':tids:pinned', 0, -1, next); + db.getSortedSetRevRange('cid:' + cid + ':tids:pinned', 0, -1, next); }, function (pinnedTids, next) { async.eachLimit(pinnedTids, 10, function (tid, next) { diff --git a/src/categories/topics.js b/src/categories/topics.js index 50278f529b..bc776b01d1 100644 --- a/src/categories/topics.js +++ b/src/categories/topics.js @@ -1,10 +1,12 @@ 'use strict'; var async = require('async'); +var _ = require('lodash'); var db = require('../database'); var topics = require('../topics'); var plugins = require('../plugins'); +var meta = require('../meta'); module.exports = function (Categories) { Categories.getCategoryTopics = function (data, callback) { @@ -13,7 +15,7 @@ module.exports = function (Categories) { plugins.fireHook('filter:category.topics.prepare', data, next); }, function (data, next) { - Categories.getTopicIds(data.cid, data.set, data.reverse, data.start, data.stop, next); + Categories.getTopicIds(data, next); }, function (tids, next) { topics.getTopicsByTids(tids, data.uid, next); @@ -35,38 +37,62 @@ module.exports = function (Categories) { ], callback); }; - Categories.getTopicIds = function (cid, set, reverse, start, stop, callback) { + Categories.getTopicIds = function (data, callback) { var pinnedTids; - var pinnedCount; - var totalPinnedCount; async.waterfall([ function (next) { - Categories.getPinnedTids(cid, 0, -1, next); + var dataForPinned = _.cloneDeep(data); + dataForPinned.start = 0; + dataForPinned.stop = -1; + + async.parallel({ + pinnedTids: async.apply(Categories.getPinnedTids, dataForPinned), + set: async.apply(Categories.buildTopicsSortedSet, data), + direction: async.apply(Categories.getSortedSetRangeDirection, data.sort), + }, next); }, - function (_pinnedTids, next) { - totalPinnedCount = _pinnedTids.length; + function (results, next) { + var totalPinnedCount = results.pinnedTids.length; - pinnedTids = _pinnedTids.slice(start, stop === -1 ? undefined : stop + 1); + pinnedTids = results.pinnedTids.slice(data.start, data.stop === -1 ? undefined : data.stop + 1); - pinnedCount = pinnedTids.length; + var pinnedCount = pinnedTids.length; - var topicsPerPage = stop - start + 1; + var topicsPerPage = data.stop - data.start + 1; var normalTidsToGet = Math.max(0, topicsPerPage - pinnedCount); - if (!normalTidsToGet && stop !== -1) { + if (!normalTidsToGet && data.stop !== -1) { return next(null, []); } + + if (plugins.hasListeners('filter:categories.getTopicIds')) { + return plugins.fireHook('filter:categories.getTopicIds', { + tids: [], + data: data, + pinnedTids: pinnedTids, + allPinnedTids: results.pinnedTids, + totalPinnedCount: totalPinnedCount, + normalTidsToGet: normalTidsToGet, + }, function (err, data) { + callback(err, data && data.tids); + }); + } + + var set = results.set; + var direction = results.direction; + var start = data.start; if (start > 0 && totalPinnedCount) { start -= totalPinnedCount - pinnedCount; } - stop = stop === -1 ? stop : start + normalTidsToGet - 1; + + var stop = data.stop === -1 ? data.stop : start + normalTidsToGet - 1; if (Array.isArray(set)) { - db[reverse ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ sets: set, start: start, stop: stop }, next); + db[direction === 'highest-to-lowest' ? 'getSortedSetRevIntersect' : 'getSortedSetIntersect']({ sets: set, start: start, stop: stop }, next); } else { - db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, next); + db[direction === 'highest-to-lowest' ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, next); } }, function (normalTids, next) { @@ -79,12 +105,85 @@ module.exports = function (Categories) { ], callback); }; + Categories.getTopicCount = function (data, callback) { + if (plugins.hasListeners('filter:categories.getTopicCount')) { + return plugins.fireHook('filter:categories.getTopicCount', { + topicCount: data.category.topic_count, + data: data, + }, function (err, data) { + callback(err, data && data.topicCount); + }); + } + async.waterfall([ + function (next) { + Categories.buildTopicsSortedSet(data, next); + }, + function (set, next) { + if (Array.isArray(set)) { + db.sortedSetIntersectCard(set, next); + } else { + next(null, data.category.topic_count); + } + }, + ], callback); + }; + + Categories.buildTopicsSortedSet = function (data, callback) { + var cid = data.cid; + var set = 'cid:' + cid + ':tids'; + var sort = data.sort || (data.settings && data.settings.categoryTopicSort) || meta.config.categoryTopicSort || 'newest_to_oldest'; + + if (sort === 'most_posts') { + set = 'cid:' + cid + ':tids:posts'; + } + + if (data.targetUid) { + set = 'cid:' + cid + ':uid:' + data.targetUid + ':tids'; + } + + if (data.tag) { + if (Array.isArray(data.tag)) { + set = [set].concat(data.tag.map(function (tag) { + return 'tag:' + tag + ':topics'; + })); + } else { + set = [set, 'tag:' + data.tag + ':topics']; + } + } + plugins.fireHook('filter:categories.buildTopicsSortedSet', { + set: set, + data: data, + }, function (err, data) { + callback(err, data && data.set); + }); + }; + + Categories.getSortedSetRangeDirection = function (sort, callback) { + sort = sort || 'newest_to_oldest'; + var direction = sort === 'newest_to_oldest' || sort === 'most_posts' ? 'highest-to-lowest' : 'lowest-to-highest'; + plugins.fireHook('filter:categories.getSortedSetRangeDirection', { + sort: sort, + direction: direction, + }, function (err, data) { + callback(err, data && data.direction); + }); + }; + Categories.getAllTopicIds = function (cid, start, stop, callback) { db.getSortedSetRange(['cid:' + cid + ':tids:pinned', 'cid:' + cid + ':tids'], start, stop, callback); }; - Categories.getPinnedTids = function (cid, start, stop, callback) { - db.getSortedSetRevRange('cid:' + cid + ':tids:pinned', start, stop, callback); + Categories.getPinnedTids = function (data, callback) { + if (plugins.hasListeners('filter:categories.getPinnedTids')) { + return plugins.fireHook('filter:categories.getPinnedTids', { + pinnedTids: [], + data: data, + }, function (err, data) { + callback(err, data && data.pinnedTids); + }); + } + + db.getSortedSetRevRange('cid:' + data.cid + ':tids:pinned', data.start, data.stop, callback); }; Categories.modifyTopicsByPrivilege = function (topics, privileges) { diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index 8ffac0a4dc..c3b9990c26 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -7,26 +7,22 @@ var meta = require('../../meta'); var user = require('../../user'); var helpers = require('../helpers'); -var chatsController = {}; +var chatsController = module.exports; chatsController.get = function (req, res, callback) { if (parseInt(meta.config.disableChat, 10) === 1) { return callback(); } + var uid; - var username; var recentChats; async.waterfall([ function (next) { - async.parallel({ - uid: async.apply(user.getUidByUserslug, req.params.userslug), - username: async.apply(user.getUsernameByUserslug, req.params.userslug), - }, next); + user.getUidByUserslug(req.params.userslug, next); }, - function (results, next) { - uid = results.uid; - username = results.username; + function (_uid, next) { + uid = _uid; if (!uid) { return callback(); } @@ -45,7 +41,6 @@ chatsController.get = function (req, res, callback) { nextStart: recentChats.nextStart, allowed: true, title: '[[pages:chats]]', - breadcrumbs: helpers.buildBreadcrumbs([{ text: username, url: '/user/' + req.params.userslug }, { text: '[[pages:chats]]' }]), }); } messaging.isUserInRoom(req.uid, req.params.roomid, next); @@ -66,37 +61,30 @@ chatsController.get = function (req, res, callback) { }), }, next); }, - ], function (err, data) { - if (err) { - return callback(err); - } - var room = data.room; - room.messages = data.messages; + function (data) { + var room = data.room; + room.messages = data.messages; - room.isOwner = parseInt(room.owner, 10) === parseInt(req.uid, 10); - room.users = data.users.filter(function (user) { - return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid; - }); + room.isOwner = parseInt(room.owner, 10) === parseInt(req.uid, 10); + room.users = data.users.filter(function (user) { + return user && parseInt(user.uid, 10) && parseInt(user.uid, 10) !== req.uid; + }); - room.canReply = data.canReply; - room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; - room.rooms = recentChats.rooms; - room.uid = uid; - room.userslug = req.params.userslug; - room.nextStart = recentChats.nextStart; - room.usernames = messaging.generateUsernames(room.users, req.uid); - room.title = room.roomName || room.usernames || '[[pages:chats]]'; - room.breadcrumbs = helpers.buildBreadcrumbs([ - { text: username, url: '/user/' + req.params.userslug }, - { text: '[[pages:chats]]', url: '/user/' + req.params.userslug + '/chats' }, - { text: room.roomName || room.usernames || '[[pages:chats]]' }, - ]); - room.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0; - room.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000; - room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2; + room.canReply = data.canReply; + room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; + room.rooms = recentChats.rooms; + room.uid = uid; + room.userslug = req.params.userslug; + room.nextStart = recentChats.nextStart; + room.usernames = messaging.generateUsernames(room.users, req.uid); + room.title = room.roomName || room.usernames || '[[pages:chats]]'; + room.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0; + room.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000; + room.showUserInput = !room.maximumUsersInChatRoom || room.maximumUsersInChatRoom > 2; - res.render('chats', room); - }); + res.render('chats', room); + }, + ], callback); }; chatsController.redirectToChat = function (req, res, next) { @@ -104,14 +92,15 @@ chatsController.redirectToChat = function (req, res, next) { if (!req.uid) { return next(); } - user.getUserField(req.uid, 'userslug', function (err, userslug) { - if (err || !userslug) { - return next(err); - } - - helpers.redirect(res, '/user/' + userslug + '/chats' + (roomid ? '/' + roomid : '')); - }); + async.waterfall([ + function (next) { + user.getUserField(req.uid, 'userslug', next); + }, + function (userslug, next) { + if (!userslug) { + return next(); + } + helpers.redirect(res, '/user/' + userslug + '/chats' + (roomid ? '/' + roomid : '')); + }, + ], next); }; - - -module.exports = chatsController; diff --git a/src/controllers/accounts/notifications.js b/src/controllers/accounts/notifications.js index d398003782..3ec4708e7a 100644 --- a/src/controllers/accounts/notifications.js +++ b/src/controllers/accounts/notifications.js @@ -75,6 +75,17 @@ notificationsController.get = function (req, res, next) { user.notifications.getNotifications(nids, req.uid, next); }, + function (notifications, next) { + plugins.fireHook('filter:notifications.get', { + notifications: notifications, + }, function (err, data) { + if (err) { + return next(err); + } + + next(null, data.notifications); + }); + }, function (notifications) { res.render('notifications', { notifications: notifications, diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index 97d9b17262..0410d4ea9e 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -1,9 +1,13 @@ 'use strict'; - var async = require('async'); var nconf = require('nconf'); +var fs = require('fs'); +var path = require('path'); + var meta = require('../../meta'); +var file = require('../../file'); +var emailer = require('../../emailer'); var settingsController = module.exports; @@ -22,44 +26,54 @@ settingsController.get = function (req, res, next) { function renderEmail(req, res, next) { - var fs = require('fs'); - var path = require('path'); - var file = require('../../file'); - var emailsPath = path.join(nconf.get('views_dir'), 'emails'); - async.waterfall([ - function (next) { - file.walk(emailsPath, next); - }, - function (emails, next) { - async.map(emails, function (email, next) { - var path = email.replace(emailsPath, '').substr(1).replace('.tpl', ''); + async.parallel({ + emails: function (cb) { + async.waterfall([ + function (next) { + file.walk(emailsPath, next); + }, + function (emails, next) { + // exclude .jst files + emails = emails.filter(function (email) { + return !email.endsWith('.jst'); + }); - async.waterfall([ - function (next) { - fs.readFile(email, next); - }, - function (original, next) { - var text = meta.config['email:custom:' + path] ? meta.config['email:custom:' + path] : original.toString(); + async.map(emails, function (email, next) { + var path = email.replace(emailsPath, '').substr(1).replace('.tpl', ''); - next(null, { - path: path, - fullpath: email, - text: text, - original: original.toString(), - }); - }, - ], next); - }, next); - }, - function (emails) { - res.render('admin/settings/email', { - emails: emails, - sendable: emails.filter(function (email) { - return email.path.indexOf('_plaintext') === -1 && email.path.indexOf('partials') === -1; - }), - }); + async.waterfall([ + function (next) { + fs.readFile(email, next); + }, + function (original, next) { + var text = meta.config['email:custom:' + path] ? meta.config['email:custom:' + path] : original.toString(); + + next(null, { + path: path, + fullpath: email, + text: text, + original: original.toString(), + }); + }, + ], next); + }, next); + }, + ], cb); }, - ], next); + services: emailer.listServices, + }, function (err, results) { + if (err) { + return next(err); + } + + res.render('admin/settings/email', { + emails: results.emails, + sendable: results.emails.filter(function (email) { + return email.path.indexOf('_plaintext') === -1 && email.path.indexOf('partials') === -1; + }), + services: results.services, + }); + }); } diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index a040f12ba2..42cade14cc 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -146,6 +146,25 @@ function uploadImage(filename, folder, uploadedFile, req, res, next) { file.saveFileToLocal(filename, folder, uploadedFile.path, next); } }, + function (imageData, next) { + // Post-processing for site-logo + if (path.basename(filename, path.extname(filename)) === 'site-logo' && folder === 'system') { + var uploadPath = path.join(nconf.get('upload_path'), folder, 'site-logo-x50.png'); + async.series([ + async.apply(image.resizeImage, { + path: uploadedFile.path, + target: uploadPath, + extension: 'png', + height: 50, + }), + async.apply(meta.configs.set, 'brand:emailLogo', path.join(nconf.get('upload_url'), 'system/site-logo-x50.png')), + ], function (err) { + next(err, imageData); + }); + } else { + setImmediate(next, null, imageData); + } + }, ], function (err, image) { file.delete(uploadedFile.path); if (err) { diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 5e4a3486b8..f0f64f42fd 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -14,6 +14,7 @@ var plugins = require('../plugins'); var utils = require('../utils'); var Password = require('../password'); var translator = require('../translator'); +var helpers = require('./helpers'); var sockets = require('../socket.io'); @@ -49,6 +50,10 @@ authenticationController.register = function (req, res) { return next(new Error('[[error:username-too-long]]')); } + if (userData.password !== userData['password-confirm']) { + return next(new Error('[[user:change_password_error_match]]')); + } + user.isPasswordValid(userData.password, next); }, function (next) { @@ -67,7 +72,7 @@ authenticationController.register = function (req, res) { }, ], function (err, data) { if (err) { - return res.status(400).send(err.message); + return helpers.noScriptErrors(req, res, err.message, 400); } if (data.uid && req.body.userLang) { @@ -96,6 +101,10 @@ function registerAndLoginUser(req, res, userData, callback) { } userData.register = true; req.session.registration = userData; + + if (req.body.noscript === 'true') { + return res.redirect(nconf.get('relative_path') + '/register/complete'); + } return res.json({ referrer: nconf.get('relative_path') + '/register/complete' }); }, function (next) { @@ -200,22 +209,22 @@ authenticationController.login = function (req, res, next) { } else if (loginWith.indexOf('username') !== -1 && !validator.isEmail(req.body.username)) { continueLogin(req, res, next); } else { - res.status(500).send('[[error:wrong-login-type-' + loginWith + ']]'); + var err = '[[error:wrong-login-type-' + loginWith + ']]'; + helpers.noScriptErrors(req, res, err, 500); } }; function continueLogin(req, res, next) { passport.authenticate('local', function (err, userData, info) { if (err) { - return res.status(403).send(err.message); + return helpers.noScriptErrors(req, res, err.message, 403); } if (!userData) { if (typeof info === 'object') { info = '[[error:invalid-username-or-password]]'; } - - return res.status(403).send(info); + return helpers.noScriptErrors(req, res, info, 403); } var passwordExpiry = userData.passwordExpiry !== undefined ? parseInt(userData.passwordExpiry, 10) : null; @@ -235,7 +244,7 @@ function continueLogin(req, res, next) { req.session.passwordExpired = true; user.reset.generate(userData.uid, function (err, code) { if (err) { - return res.status(403).send(err.message); + return helpers.noScriptErrors(req, res, err.message, 403); } res.status(200).send(nconf.get('relative_path') + '/reset/' + code); @@ -243,16 +252,21 @@ function continueLogin(req, res, next) { } else { authenticationController.doLogin(req, userData.uid, function (err) { if (err) { - return res.status(403).send(err.message); + return helpers.noScriptErrors(req, res, err.message, 403); } + var destination; if (!req.session.returnTo) { - res.status(200).send(nconf.get('relative_path') + '/'); + destination = nconf.get('relative_path') + '/'; } else { - var next = req.session.returnTo; + destination = req.session.returnTo; delete req.session.returnTo; + } - res.status(200).send(next); + if (req.body.noscript === 'true') { + res.redirect(destination + '?loggedin'); + } else { + res.status(200).send(destination); } }); } @@ -414,7 +428,11 @@ authenticationController.logout = function (req, res, next) { function () { // Force session check for all connected socket.io clients with the same session id sockets.in('sess_' + req.sessionID).emit('checkSession', 0); - res.status(200).send(''); + if (req.body.noscript === 'true') { + res.redirect(nconf.get('relative_path') + '/'); + } else { + res.status(200).send(''); + } }, ], next); }; diff --git a/src/controllers/category.js b/src/controllers/category.js index 96e3d8c2bf..8edc68d076 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -28,6 +28,8 @@ categoryController.get = function (req, res, callback) { return callback(); } + var topicIndex = utils.isNumber(req.params.topic_index) ? parseInt(req.params.topic_index, 10) - 1 : 0; + async.waterfall([ function (next) { async.parallel({ @@ -62,7 +64,7 @@ categoryController.get = function (req, res, callback) { } settings = results.userSettings; - var topicIndex = utils.isNumber(req.params.topic_index) ? parseInt(req.params.topic_index, 10) - 1 : 0; + var topicCount = parseInt(results.categoryData.topic_count, 10); pageCount = Math.max(1, Math.ceil(topicCount / settings.topicsPerPage)); @@ -82,52 +84,22 @@ categoryController.get = function (req, res, callback) { topicIndex = 0; } - var set = 'cid:' + cid + ':tids'; - var reverse = false; - // `sort` qs has priority over user setting - var sort = req.query.sort || settings.categoryTopicSort; - if (sort === 'newest_to_oldest') { - reverse = true; - } else if (sort === 'most_posts') { - reverse = true; - set = 'cid:' + cid + ':tids:posts'; - } - + user.getUidByUserslug(req.query.author, next); + }, + function (targetUid, next) { var start = ((currentPage - 1) * settings.topicsPerPage) + topicIndex; var stop = start + settings.topicsPerPage - 1; - - var payload = { + categories.getCategoryById({ + uid: req.uid, cid: cid, - set: set, - reverse: reverse, start: start, stop: stop, - uid: req.uid, + sort: req.query.sort || settings.categoryTopicSort, settings: settings, - }; - - async.waterfall([ - function (next) { - user.getUidByUserslug(req.query.author, next); - }, - function (uid, next) { - payload.targetUid = uid; - if (uid) { - payload.set = 'cid:' + cid + ':uid:' + uid + ':tids'; - } - - if (req.query.tag) { - if (Array.isArray(req.query.tag)) { - payload.set = [payload.set].concat(req.query.tag.map(function (tag) { - return 'tag:' + tag + ':topics'; - })); - } else { - payload.set = [payload.set, 'tag:' + req.query.tag + ':topics']; - } - } - categories.getCategoryById(payload, next); - }, - ], next); + query: req.query, + tag: req.query.tag, + targetUid: targetUid, + }, next); }, function (categoryData, next) { categories.modifyTopicsByPrivilege(categoryData.topics, userPrivileges); diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 732652aa38..ee0ba75482 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -14,6 +14,24 @@ var middleware = require('../middleware'); var helpers = module.exports; +helpers.noScriptErrors = function (req, res, error, httpStatus) { + if (req.body.noscript !== 'true') { + return res.status(httpStatus).send(error); + } + + var middleware = require('../middleware'); + var httpStatusString = httpStatus.toString(); + middleware.buildHeader(req, res, function () { + res.status(httpStatus).render(httpStatusString, { + path: req.path, + loggedIn: true, + error: error, + returnLink: true, + title: '[[global:' + httpStatusString + '.title]]', + }); + }); +}; + helpers.notAllowed = function (req, res, error) { plugins.fireHook('filter:helpers.notAllowed', { req: req, diff --git a/src/controllers/index.js b/src/controllers/index.js index 9136b21534..dbd41d360c 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -7,6 +7,7 @@ var validator = require('validator'); var meta = require('../meta'); var user = require('../user'); var plugins = require('../plugins'); +var topics = require('../topics'); var helpers = require('./helpers'); var Controllers = module.exports; @@ -279,6 +280,47 @@ Controllers.compose = function (req, res, next) { }); }; +Controllers.composePost = function (req, res) { + var body = req.body; + var data = { + uid: req.uid, + req: req, + timestamp: Date.now(), + content: body.content, + }; + req.body.noscript = 'true'; + + if (!data.content) { + return helpers.noScriptErrors(req, res, '[[error:invalid-data]]', 400); + } + + if (body.tid) { + data.tid = body.tid; + + topics.reply(data, function (err, result) { + if (err) { + return helpers.noScriptErrors(req, res, err.message, 400); + } + user.updateOnlineUsers(result.uid); + + res.redirect(nconf.get('relative_path') + '/post/' + result.pid); + }); + } else if (body.cid) { + data.cid = body.cid; + data.title = body.title; + data.tags = []; + data.thumb = ''; + + topics.post(data, function (err, result) { + if (err) { + return helpers.noScriptErrors(req, res, err.message, 400); + } + + res.redirect(nconf.get('relative_path') + '/topic/' + result.topicData.slug); + }); + } +}; + Controllers.confirmEmail = function (req, res) { user.email.confirm(req.params.code, function (err) { res.render('confirm', { diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 51d66dda30..f350e1c880 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -6,6 +6,7 @@ var user = require('../user'); var categories = require('../categories'); var flags = require('../flags'); var analytics = require('../analytics'); +var plugins = require('../plugins'); var modsController = module.exports; modsController.flags = {}; @@ -13,11 +14,13 @@ modsController.flags = {}; modsController.flags.list = function (req, res, next) { var filters; var hasFilter; + var validFilters = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick']; async.waterfall([ function (next) { async.parallel({ isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid), moderatedCids: async.apply(user.getModeratedCids, req.uid), + validFilters: async.apply(plugins.fireHook, 'filter:flags.validateFilters', { filters: validFilters }), }, next); }, function (results, next) { @@ -29,10 +32,12 @@ modsController.flags.list = function (req, res, next) { res.locals.cids = results.moderatedCids; } + validFilters = results.validFilters.filters; + // Parse query string params for filters hasFilter = false; - var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick']; - filters = valid.reduce(function (memo, cur) { + + filters = validFilters.reduce(function (memo, cur) { if (req.query.hasOwnProperty(cur)) { memo[cur] = req.query[cur]; } diff --git a/src/emailer.js b/src/emailer.js index 38d68102c1..d82228d712 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -3,10 +3,9 @@ var async = require('async'); var winston = require('winston'); var nconf = require('nconf'); -var templates = require('templates.js'); +var Benchpress = require('benchpressjs'); var nodemailer = require('nodemailer'); -var sendmailTransport = require('nodemailer-sendmail-transport'); -var smtpTransport = require('nodemailer-smtp-transport'); +var wellKnownServices = require('nodemailer/lib/well-known/services'); var htmlToText = require('html-to-text'); var url = require('url'); @@ -14,10 +13,15 @@ var User = require('./user'); var Plugins = require('./plugins'); var meta = require('./meta'); var translator = require('./translator'); +var pubsub = require('./pubsub'); var transports = { - sendmail: nodemailer.createTransport(sendmailTransport()), - gmail: undefined, + sendmail: nodemailer.createTransport({ + sendmail: true, + newline: 'unix', + }), + smtp: undefined, + // gmail: undefined, }; var app; @@ -25,26 +29,63 @@ var fallbackTransport; var Emailer = module.exports; +Emailer.listServices = function (callback) { + var services = Object.keys(wellKnownServices); + setImmediate(callback, null, services); +}; + +Emailer._defaultPayload = {}; Emailer.registerApp = function (expressApp) { app = expressApp; + var logo = null; + if (meta.configs.hasOwnProperty('brand:emailLogo')) { + logo = (!meta.config['brand:emailLogo'].startsWith('http') ? nconf.get('url') : '') + meta.config['brand:emailLogo']; + } + + Emailer._defaultPayload = { + url: nconf.get('url'), + site_title: meta.config.title || 'NodeBB', + logo: { + src: logo, + height: meta.config['brand:emailLogo:height'], + width: meta.config['brand:emailLogo:width'], + }, + }; + // Enable Gmail transport if enabled in ACP - if (parseInt(meta.config['email:GmailTransport:enabled'], 10) === 1) { - transports.gmail = nodemailer.createTransport(smtpTransport({ - host: 'smtp.gmail.com', - port: 465, - secure: true, + if (parseInt(meta.config['email:smtpTransport:enabled'], 10) === 1) { + var smtpOptions = { auth: { - user: meta.config['email:GmailTransport:user'], - pass: meta.config['email:GmailTransport:pass'], + user: meta.config['email:smtpTransport:user'], + pass: meta.config['email:smtpTransport:pass'], }, - })); - fallbackTransport = transports.gmail; + }; + + if (meta.config['email:smtpTransport:serice'] === 'nodebb-custom-smtp') { + smtpOptions.port = meta.config['email:smtpTransport:port']; + smtpOptions.host = meta.config['email:smtpTransport:host']; + smtpOptions.secure = true; + } else { + smtpOptions.service = meta.config['email:smtpTransport:service']; + } + + transports.smtp = nodemailer.createTransport(smtpOptions); + fallbackTransport = transports.smtp; } else { fallbackTransport = transports.sendmail; } + // Update default payload if new logo is uploaded + pubsub.on('config:update', function (config) { + if (config) { + Emailer._defaultPayload.logo.src = config['brand:emailLogo']; + Emailer._defaultPayload.logo.height = config['brand:emailLogo:height']; + Emailer._defaultPayload.logo.width = config['brand:emailLogo:width']; + } + }); + return Emailer; }; @@ -55,6 +96,9 @@ Emailer.send = function (template, uid, params, callback) { return callback(); } + // Combined passed-in payload with default values + params = Object.assign({}, Emailer._defaultPayload, params); + async.waterfall([ function (next) { async.parallel({ @@ -144,9 +188,9 @@ Emailer.sendViaFallback = function (data, callback) { }; function render(tpl, params, next) { - if (meta.config['email:custom:' + tpl.replace('emails/', '')]) { - var text = templates.parse(meta.config['email:custom:' + tpl.replace('emails/', '')], params); - next(null, text); + var customTemplate = meta.config['email:custom:' + tpl.replace('emails/', '')]; + if (customTemplate) { + Benchpress.compileParse(customTemplate, params, next); } else { app.render(tpl, params, next); } diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 5d433b2c33..a3512f478b 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -1,7 +1,6 @@ 'use strict'; var async = require('async'); -var nconf = require('nconf'); var winston = require('winston'); var user = require('../user'); @@ -124,8 +123,6 @@ module.exports = function (Messaging) { subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]', summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', message: messageObj, - site_title: meta.config.title || 'NodeBB', - url: nconf.get('url'), roomId: messageObj.roomId, username: userData.username, userslug: userData.userslug, diff --git a/src/meta/configs.js b/src/meta/configs.js index 925ff61255..5148a11a5e 100644 --- a/src/meta/configs.js +++ b/src/meta/configs.js @@ -3,6 +3,7 @@ var async = require('async'); var nconf = require('nconf'); +var path = require('path'); var db = require('../database'); var pubsub = require('../pubsub'); @@ -80,13 +81,30 @@ Configs.setMultiple = function (data, callback) { }; function processConfig(data, callback) { - if (data.customCSS) { - return saveRenderedCss(data, callback); - } - setImmediate(callback); + async.parallel([ + async.apply(saveRenderedCss, data), + function (next) { + var image = require('../image'); + if (data['brand:logo']) { + image.size(path.join(nconf.get('upload_path'), 'system', 'site-logo-x50.png'), function (err, size) { + data['brand:emailLogo:height'] = size.height; + data['brand:emailLogo:width'] = size.width; + next(err); + }); + } else { + setImmediate(next); + } + }, + ], function (err) { + callback(err); + }); } function saveRenderedCss(data, callback) { + if (!data.customCSS) { + return setImmediate(callback); + } + var less = require('less'); async.waterfall([ function (next) { diff --git a/src/meta/css.js b/src/meta/css.js index 0f20ed7aca..7621f3b6dd 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -118,6 +118,20 @@ function getBundleMetadata(target, callback) { }, ], cb); }, + acpLess: function (cb) { + if (target === 'client') { + return cb(null, ''); + } + + async.waterfall([ + function (next) { + filterMissingFiles(plugins.acpLessFiles, next); + }, + function (acpLessFiles, next) { + getImports(acpLessFiles, '\n@import ".', '.less', next); + }, + ], cb); + }, css: function (cb) { async.waterfall([ function (next) { @@ -133,8 +147,9 @@ function getBundleMetadata(target, callback) { function (result, next) { var cssImports = result.css; var lessImports = result.less; + var acpLessImports = result.acpLess; - var imports = cssImports + '\n' + lessImports; + var imports = cssImports + '\n' + lessImports + '\n' + acpLessImports; imports = buildImports[target](imports); next(null, { paths: paths, imports: imports }); diff --git a/src/meta/js.js b/src/meta/js.js index 8f483779da..8082be2412 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -29,7 +29,7 @@ JS.scripts = { 'public/vendor/tinycon/tinycon.js', 'public/vendor/xregexp/xregexp.js', 'public/vendor/xregexp/unicode/unicode-base.js', - 'node_modules/templates.js/lib/templates.js', + 'node_modules/benchpressjs/build/benchpress.js', 'public/src/utils.js', 'public/src/sockets.js', 'public/src/app.js', diff --git a/src/meta/templates.js b/src/meta/templates.js index f6776f64e1..18991fc149 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -11,6 +11,8 @@ var nconf = require('nconf'); var plugins = require('../plugins'); var file = require('../file'); +var viewsPath = nconf.get('views_dir'); + var Templates = module.exports; Templates.compile = function (callback) { @@ -18,7 +20,6 @@ Templates.compile = function (callback) { var themeConfig = require(nconf.get('theme_config')); var baseTemplatesPaths = themeConfig.baseTheme ? getBaseTemplates(themeConfig.baseTheme) : [nconf.get('base_templates_path')]; - var viewsPath = nconf.get('views_dir'); function processImports(paths, relativePath, source, callback) { var regex = //; @@ -63,9 +64,9 @@ Templates.compile = function (callback) { var source = file.toString(); processImports(paths, relativePath, source, next); }, - function (compiled, next) { + function (source, next) { mkdirp(path.join(viewsPath, path.dirname(relativePath)), function (err) { - next(err, compiled); + next(err, source); }); }, function (compiled, next) { @@ -74,6 +75,9 @@ Templates.compile = function (callback) { ], next); }, next); }, + function (next) { + rimraf(path.join(viewsPath, '*.jst'), next); + }, function (next) { winston.verbose('[meta/templates] Successfully compiled templates.'); next(); @@ -99,7 +103,6 @@ function getBaseTemplates(theme) { function preparePaths(baseTemplatesPaths, callback) { var coreTemplatesPath = nconf.get('core_templates_path'); - var viewsPath = nconf.get('views_dir'); var pluginTemplates; async.waterfall([ function (next) { diff --git a/src/middleware/index.js b/src/middleware/index.js index 9072b0441d..511ae1ddd4 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -2,11 +2,13 @@ var async = require('async'); var path = require('path'); +var fs = require('fs'); var csrf = require('csurf'); var validator = require('validator'); var nconf = require('nconf'); var ensureLoggedIn = require('connect-ensure-login'); var toobusy = require('toobusy-js'); +var Benchpress = require('benchpressjs'); var plugins = require('../plugins'); var meta = require('../meta'); @@ -186,3 +188,33 @@ middleware.delayLoading = function (req, res, next) { // Introduces an artificial delay during load so that brute force attacks are effectively mitigated setTimeout(next, 1000); }; + +var viewsDir = nconf.get('views_dir'); +middleware.templatesOnDemand = function (req, res, next) { + var filePath = req.filePath || path.join(viewsDir, req.path); + if (!filePath.endsWith('.jst')) { + return next(); + } + + async.waterfall([ + function (cb) { + file.exists(filePath, cb); + }, + function (exists, cb) { + if (exists) { + return next(); + } + + fs.readFile(filePath.replace(/\.jst$/, '.tpl'), cb); + }, + function (source, cb) { + Benchpress.precompile({ + source: source.toString(), + minify: global.env !== 'development', + }, cb); + }, + function (compiled, cb) { + fs.writeFile(filePath, compiled, cb); + }, + ], next); +}; diff --git a/src/plugins.js b/src/plugins.js index 2dd377ae6d..10db23eb65 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -30,6 +30,7 @@ Plugins.loadedHooks = {}; Plugins.staticDirs = {}; Plugins.cssFiles = []; Plugins.lessFiles = []; +Plugins.acpLessFiles = []; Plugins.clientScripts = []; Plugins.acpScripts = []; Plugins.libraryPaths = []; @@ -83,6 +84,7 @@ Plugins.reload = function (callback) { Plugins.versionWarning = []; Plugins.cssFiles.length = 0; Plugins.lessFiles.length = 0; + Plugins.acpLessFiles.length = 0; Plugins.clientScripts.length = 0; Plugins.acpScripts.length = 0; Plugins.libraryPaths.length = 0; @@ -302,39 +304,76 @@ Plugins.normalise = function (apiReturn, callback) { }); }; +Plugins.nodeModulesPath = path.join(__dirname, '../node_modules'); + Plugins.showInstalled = function (callback) { - var npmPluginPath = path.join(__dirname, '../node_modules'); + var pluginNamePattern = /^(@.*?\/)?nodebb-(theme|plugin|widget|rewards)-.*$/; async.waterfall([ - async.apply(fs.readdir, npmPluginPath), - + function (next) { + fs.readdir(Plugins.nodeModulesPath, next); + }, function (dirs, next) { - dirs = dirs.filter(function (dir) { - return dir.startsWith('nodebb-plugin-') || - dir.startsWith('nodebb-widget-') || - dir.startsWith('nodebb-rewards-') || - dir.startsWith('nodebb-theme-'); - }).map(function (dir) { - return path.join(npmPluginPath, dir); - }); + var pluginPaths = []; - async.filter(dirs, function (dir, callback) { - fs.stat(dir, function (err, stats) { - if (err) { - if (err.code === 'ENOENT') { - return callback(null, false); - } - return callback(err); - } - callback(null, stats.isDirectory()); - }); - }, next); + async.each(dirs, function (dirname, next) { + var dirPath = path.join(Plugins.nodeModulesPath, dirname); + + async.waterfall([ + function (cb) { + fs.stat(dirPath, function (err, stats) { + if (err && err.code !== 'ENOENT') { + return cb(err); + } + if (err || !stats.isDirectory()) { + return next(); + } + + if (pluginNamePattern.test(dirname)) { + pluginPaths.push(dirname); + return next(); + } + + if (dirname[0] !== '@') { + return next(); + } + fs.readdir(dirPath, cb); + }); + }, + function (subdirs, cb) { + async.each(subdirs, function (subdir, next) { + if (!pluginNamePattern.test(subdir)) { + return next(); + } + + var subdirPath = path.join(dirPath, subdir); + fs.stat(subdirPath, function (err, stats) { + if (err && err.code !== 'ENOENT') { + return next(err); + } + + if (err || !stats.isDirectory()) { + return next(); + } + + pluginPaths.push(dirname + '/' + subdir); + next(); + }); + }, cb); + }, + ], next); + }, function (err) { + next(err, pluginPaths); + }); }, - function (files, next) { + function (dirs, next) { + dirs = dirs.map(function (dir) { + return path.join(Plugins.nodeModulesPath, dir); + }); var plugins = []; - async.each(files, function (file, next) { + async.each(dirs, function (file, next) { async.waterfall([ function (next) { Plugins.loadPluginInfo(file, next); diff --git a/src/plugins/load.js b/src/plugins/load.js index b077a3ed4e..8745a833a0 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -25,6 +25,9 @@ module.exports = function (Plugins) { lessFiles: function (next) { Plugins.data.getFiles(pluginData, 'less', next); }, + acpLessFiles: function (next) { + Plugins.data.getFiles(pluginData, 'acpLess', next); + }, clientScripts: function (next) { Plugins.data.getScripts(pluginData, 'client', next); }, @@ -60,6 +63,7 @@ module.exports = function (Plugins) { Object.assign(Plugins.staticDirs, results.staticDirs || {}); add(Plugins.cssFiles, results.cssFiles); add(Plugins.lessFiles, results.lessFiles); + add(Plugins.acpLessFiles, results.acpLessFiles); add(Plugins.clientScripts, results.clientScripts); add(Plugins.acpScripts, results.acpScripts); Object.assign(meta.js.scripts.modules, results.modules || {}); @@ -79,6 +83,7 @@ module.exports = function (Plugins) { Plugins.prepareForBuild = function (targets, callback) { Plugins.cssFiles.length = 0; Plugins.lessFiles.length = 0; + Plugins.acpLessFiles.length = 0; Plugins.clientScripts.length = 0; Plugins.acpScripts.length = 0; Plugins.soundpacks.length = 0; @@ -91,7 +96,7 @@ module.exports = function (Plugins) { 'client js bundle': ['clientScripts'], 'admin js bundle': ['acpScripts'], 'client side styles': ['cssFiles', 'lessFiles'], - 'admin control panel styles': ['cssFiles', 'lessFiles'], + 'admin control panel styles': ['cssFiles', 'lessFiles', 'acpLessFiles'], sounds: ['soundpack'], languages: ['languageData'], }; diff --git a/src/routes/index.js b/src/routes/index.js index f12a97b660..60c97bd8d3 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -4,11 +4,12 @@ var nconf = require('nconf'); var winston = require('winston'); var path = require('path'); var async = require('async'); +var express = require('express'); + var meta = require('../meta'); var controllers = require('../controllers'); var plugins = require('../plugins'); var user = require('../user'); -var express = require('express'); var accountRoutes = require('./accounts'); var metaRoutes = require('./meta'); @@ -34,6 +35,8 @@ function mainRoutes(app, middleware, controllers) { setupPageRoute(app, '/search', middleware, [], controllers.search.search); setupPageRoute(app, '/reset/:code?', middleware, [middleware.delayLoading], controllers.reset); setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse); + + app.post('/compose', middleware.applyCSRF, controllers.composePost); } function modRoutes(app, middleware, controllers) { @@ -145,6 +148,7 @@ module.exports = function (app, middleware, hotswapIds, callback) { } app.use(middleware.privateUploads); + app.use(relativePath + '/assets/templates', middleware.templatesOnDemand); var statics = [ { route: '/assets', path: path.join(__dirname, '../../build/public') }, diff --git a/src/search.js b/src/search.js index fdfcf6fdfb..71f212632e 100644 --- a/src/search.js +++ b/src/search.js @@ -42,6 +42,7 @@ function searchInContent(data, callback) { var matchCount = 0; var pids; var metadata; + async.waterfall([ function (next) { async.parallel({ @@ -96,6 +97,7 @@ function searchInContent(data, callback) { function (_metadata, next) { metadata = _metadata; matchCount = metadata.pids.length; + if (data.page) { var start = Math.max(0, (data.page - 1)) * 10; metadata.pids = metadata.pids.slice(start, start + 10); @@ -128,7 +130,10 @@ function filterAndSort(pids, data, callback) { sortPosts(posts, data); - pids = posts.map(function (post) { + plugins.fireHook('filter:search.filterAndSort', { pids: pids, posts: posts, data: data }, next); + }, + function (result, next) { + pids = result.posts.map(function (post) { return post && post.pid; }); @@ -138,25 +143,12 @@ function filterAndSort(pids, data, callback) { } function getMatchedPosts(pids, data, callback) { - var postFields = ['pid', 'tid', 'timestamp', 'deleted']; - var topicFields = ['deleted']; + var postFields = ['pid', 'uid', 'tid', 'timestamp', 'deleted']; var categoryFields = []; - if (data.replies) { - topicFields.push('postcount'); - } - if (data.sortBy && data.sortBy !== 'relevance') { - if (data.sortBy.startsWith('category')) { - topicFields.push('cid'); - } else if (data.sortBy.startsWith('topic.')) { - topicFields.push(data.sortBy.split('.')[1]); - } else if (data.sortBy.startsWith('user.')) { - postFields.push('uid'); - } else if (data.sortBy.startsWith('category.')) { + if (data.sortBy.startsWith('category.')) { categoryFields.push(data.sortBy.split('.')[1]); - } else if (data.sortBy.startsWith('teaser')) { - topicFields.push('teaserPid'); } } @@ -166,7 +158,6 @@ function getMatchedPosts(pids, data, callback) { var keys = pids.map(function (pid) { return 'post:' + pid; }); - db.getObjectsFields(keys, postFields, next); }, function (_posts, next) { @@ -192,14 +183,14 @@ function getMatchedPosts(pids, data, callback) { var topicKeys = posts.map(function (post) { return 'topic:' + post.tid; }); - db.getObjectsFields(topicKeys, topicFields, next); + db.getObjects(topicKeys, next); }, function (_topics, next) { topicsData = _topics; async.parallel({ teasers: function (next) { - if (topicFields.indexOf('teaserPid') !== -1) { + if (data.sortBy && data.sortBy.startsWith('teaser')) { var teaserKeys = topicsData.map(function (topic) { return 'post:' + topic.teaserPid; }); diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index df169aba0e..322108330b 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -2,7 +2,6 @@ var async = require('async'); var winston = require('winston'); -var nconf = require('nconf'); var meta = require('../meta'); var plugins = require('../plugins'); @@ -228,8 +227,6 @@ SocketAdmin.email.test = function (socket, data, callback) { var site_title = meta.config.title || 'NodeBB'; var payload = { subject: '[' + site_title + '] Test Email', - site_title: site_title, - url: nconf.get('url'), }; switch (data.template) { @@ -252,7 +249,7 @@ SocketAdmin.email.test = function (socket, data, callback) { case 'welcome': userEmail.sendValidationEmail(socket.uid, { force: 1, - }); + }, callback); break; default: diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index db47d0e865..9dfb285d82 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -59,6 +59,7 @@ SocketCategories.loadMore = function (socket, data, callback) { if (!data) { return callback(new Error('[[error:invalid-data]]')); } + data.query = data.query || {}; var userPrivileges; async.waterfall([ function (next) { @@ -70,8 +71,8 @@ SocketCategories.loadMore = function (socket, data, callback) { user.getSettings(socket.uid, next); }, targetUid: function (next) { - if (data.author) { - user.getUidByUserslug(data.author, next); + if (data.query.author) { + user.getUidByUserslug(data.query.author, next); } else { next(); } @@ -84,44 +85,28 @@ SocketCategories.loadMore = function (socket, data, callback) { return callback(new Error('[[error:no-privileges]]')); } var infScrollTopicsPerPage = 20; - var set = 'cid:' + data.cid + ':tids'; - var reverse = false; - - if (data.categoryTopicSort === 'newest_to_oldest') { - reverse = true; - } else if (data.categoryTopicSort === 'most_posts') { - reverse = true; - set = 'cid:' + data.cid + ':tids:posts'; - } + var sort = data.sort || data.categoryTopicSort; var start = Math.max(0, parseInt(data.after, 10)); if (data.direction === -1) { - start -= reverse ? infScrollTopicsPerPage : -infScrollTopicsPerPage; + start -= infScrollTopicsPerPage; } var stop = start + infScrollTopicsPerPage - 1; start = Math.max(0, start); stop = Math.max(0, stop); - - if (results.targetUid) { - set = 'cid:' + data.cid + ':uid:' + results.targetUid + ':tids'; - } - - if (data.tag) { - set = [set, 'tag:' + data.tag + ':topics']; - } - categories.getCategoryTopics({ + uid: socket.uid, cid: data.cid, - set: set, - reverse: reverse, start: start, stop: stop, - uid: socket.uid, - targetUid: results.targetUid, + sort: sort, settings: results.settings, + query: data.query, + tag: data.query.tag, + targetUid: results.targetUid, }, next); }, function (data, next) { diff --git a/src/socket.io/user.js b/src/socket.io/user.js index d59fad3c58..1ed67276c4 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -128,7 +128,6 @@ SocketUser.reset.commit = function (socket, data, callback) { emailer.send('reset_notify', uid, { username: username, date: parsedDate, - site_title: meta.config.title || 'NodeBB', subject: '[[email:reset.notify.subject]]', }); diff --git a/src/socket.io/user/ban.js b/src/socket.io/user/ban.js index d6cfbd6f68..a61a9b83ee 100644 --- a/src/socket.io/user/ban.js +++ b/src/socket.io/user/ban.js @@ -107,7 +107,6 @@ module.exports = function (SocketUser) { var siteTitle = meta.config.title || 'NodeBB'; var data = { subject: '[[email:banned.subject, ' + siteTitle + ']]', - site_title: siteTitle, username: username, until: until ? utils.toISOString(until) : false, reason: reason, diff --git a/src/topics.js b/src/topics.js index 45e0f5bb19..d4f78801aa 100644 --- a/src/topics.js +++ b/src/topics.js @@ -182,7 +182,8 @@ Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse, function (next) { async.parallel({ posts: async.apply(getMainPostAndReplies, topicData, set, uid, start, stop, reverse), - category: async.apply(Topics.getCategoryData, topicData.tid), + category: async.apply(categories.getCategoryData, topicData.cid), + tagWhitelist: async.apply(categories.getTagWhitelist, [topicData.cid]), threadTools: async.apply(plugins.fireHook, 'filter:topic.thread_tools', { topic: topicData, uid: uid, tools: [] }), isFollowing: async.apply(Topics.isFollowing, [topicData.tid], uid), isIgnoring: async.apply(Topics.isIgnoring, [topicData.tid], uid), @@ -205,6 +206,7 @@ Topics.getTopicWithPosts = function (topicData, set, uid, start, stop, reverse, function (results, next) { topicData.posts = results.posts; topicData.category = results.category; + topicData.tagWhitelist = results.tagWhitelist[0]; topicData.thread_tools = results.threadTools.tools; topicData.isFollowing = results.isFollowing[0]; topicData.isNotFollowing = !results.isFollowing[0] && !results.isIgnoring[0]; diff --git a/src/topics/follow.js b/src/topics/follow.js index 89db6b3d13..f1981766a2 100644 --- a/src/topics/follow.js +++ b/src/topics/follow.js @@ -2,7 +2,6 @@ 'use strict'; var async = require('async'); -var nconf = require('nconf'); var S = require('string'); var winston = require('winston'); @@ -259,13 +258,10 @@ module.exports = function (Topics) { subject: '[' + (meta.config.title || 'NodeBB') + '] ' + title, intro: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]', postBody: postData.content.replace(/"\/\//g, '"https://'), - site_title: meta.config.title || 'NodeBB', username: data.userData.username, userslug: data.userData.userslug, - url: nconf.get('url') + '/topic/' + postData.topic.tid, topicSlug: postData.topic.slug, - postCount: postData.topic.postcount, - base_url: nconf.get('url'), + showUnsubscribe: true, }, next); } else { winston.debug('[topics.notifyFollowers] uid ' + toUid + ' does not have post notifications enabled, skipping.'); diff --git a/src/topics/recent.js b/src/topics/recent.js index b2a9d28a63..135a15859b 100644 --- a/src/topics/recent.js +++ b/src/topics/recent.js @@ -27,7 +27,12 @@ module.exports = function (Topics) { async.waterfall([ function (next) { if (cid) { - categories.getTopicIds(cid, 'cid:' + cid + ':tids', true, 0, 199, next); + categories.getTopicIds({ + cid: cid, + start: 0, + stop: 199, + sort: 'newest_to_oldest', + }, next); } else { db.getSortedSetRevRange('topics:recent', 0, 199, next); } diff --git a/src/topics/suggested.js b/src/topics/suggested.js index e2b965fd4e..830631d302 100644 --- a/src/topics/suggested.js +++ b/src/topics/suggested.js @@ -73,7 +73,11 @@ module.exports = function (Topics) { Topics.getTopicField(tid, 'cid', next); }, function (cid, next) { - categories.getTopicIds(cid, 'cid:' + cid + ':tids', true, 0, 9, next); + categories.getTopicIds({ + cid: cid, + start: 0, + stop: 9, + }, next); }, ], callback); } diff --git a/src/topics/tools.js b/src/topics/tools.js index fffecbd943..743aa9d38d 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -157,16 +157,13 @@ module.exports = function (Topics) { var topicData; async.waterfall([ function (next) { - Topics.exists(tid, next); - }, - function (exists, next) { - if (!exists) { - return callback(new Error('[[error:no-topic]]')); - } - Topics.getTopicFields(tid, ['uid', 'tid', 'cid', 'lastposttime', 'postcount'], next); + Topics.getTopicData(tid, next); }, function (_topicData, next) { topicData = _topicData; + if (!topicData) { + return callback(new Error('[[error:no-topic]]')); + } privileges.categories.isAdminOrMod(_topicData.cid, uid, next); }, function (isAdminOrMod, next) { diff --git a/src/upgrades/1.6.0/generate-email-logo.js b/src/upgrades/1.6.0/generate-email-logo.js new file mode 100644 index 0000000000..2f713f675f --- /dev/null +++ b/src/upgrades/1.6.0/generate-email-logo.js @@ -0,0 +1,53 @@ +'use strict'; + +var image = require('../../image'); +var meta = require('../../meta'); + +var async = require('async'); +var path = require('path'); +var nconf = require('nconf'); +var fs = require('fs'); + +module.exports = { + name: 'Generate email logo for use in email header', + timestamp: Date.UTC(2017, 6, 17), + method: function (callback) { + var skip = false; + + async.series([ + function (next) { + // Resize existing logo (if present) to email header size + var uploadPath = path.join(nconf.get('upload_path'), 'system', 'site-logo-x50.png'); + var sourcePath = meta.config['brand:logo'] ? path.join(nconf.get('upload_path'), 'system', path.basename(meta.config['brand:logo'])) : null; + + if (!sourcePath) { + skip = true; + return setImmediate(next); + } + + fs.access(sourcePath, function (err) { + if (err) { + skip = true; + return setImmediate(next); + } + + image.resizeImage({ + path: sourcePath, + target: uploadPath, + extension: 'png', + height: 50, + }, next); + }); + }, + function (next) { + if (skip) { + return setImmediate(next); + } + + meta.configs.setMultiple({ + 'brand:logo': path.join(nconf.get('upload_path'), 'system', path.basename(meta.config['brand:logo'])), + }, next); + }, + ], callback); + }, +}; diff --git a/src/user/approval.js b/src/user/approval.js index 64f892cb3d..96c25d8ad5 100644 --- a/src/user/approval.js +++ b/src/user/approval.js @@ -89,7 +89,6 @@ module.exports = function (User) { var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function (subject) { var data = { - site_title: title, username: username, subject: subject, template: 'registration_accepted', diff --git a/src/user/digest.js b/src/user/digest.js index 61b727de3a..ffd4f9031e 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -136,8 +136,6 @@ Digest.send = function (data, callback) { subject: '[' + meta.config.title + '] [[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', username: userObj.username, userslug: userObj.userslug, - url: nconf.get('url'), - site_title: meta.config.title || meta.config.browserTitle || 'NodeBB', notifications: notifications, recent: data.topics, interval: data.interval, diff --git a/src/user/email.js b/src/user/email.js index 734e247eb1..9c61211d9a 100644 --- a/src/user/email.js +++ b/src/user/email.js @@ -102,7 +102,6 @@ UserEmail.sendValidationEmail = function (uid, options, callback) { var title = meta.config.title || meta.config.browserTitle || 'NodeBB'; translator.translate('[[email:welcome-to, ' + title + ']]', meta.config.defaultLang, function (subject) { var data = { - site_title: title, username: username, confirm_link: confirm_link, confirm_code: confirm_code, diff --git a/src/user/invite.js b/src/user/invite.js index 3e6fdef359..d3d7189296 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -85,6 +85,9 @@ module.exports = function (User) { template: 'invitation', }; + // Append default data to this email payload + data = Object.assign({}, emailer._defaultPayload, data); + emailer.sendToEmail('invitation', email, meta.config.defaultLang, data, next); }); }, diff --git a/src/user/reset.js b/src/user/reset.js index db52ed033e..fd0b5daa53 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -86,7 +86,6 @@ UserReset.send = function (email, callback) { function (subject, code, next) { var reset_link = nconf.get('url') + '/reset/' + code; emailer.send('reset', uid, { - site_title: (meta.config.title || 'NodeBB'), reset_link: reset_link, subject: subject, template: 'reset', diff --git a/src/user/settings.js b/src/user/settings.js index bc8797655a..42c78a54ba 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -176,7 +176,10 @@ module.exports = function (User) { async.waterfall([ function (next) { - db.setObject('user:' + uid + ':settings', settings, next); + plugins.fireHook('filter:user.saveSettings', { settings: settings, data: data }, next); + }, + function (result, next) { + db.setObject('user:' + uid + ':settings', result.settings, next); }, function (next) { User.updateDigestSetting(uid, data.dailyDigestFreq, next); diff --git a/src/views/400.tpl b/src/views/400.tpl index 9c263fcff1..c36f1b2f48 100644 --- a/src/views/400.tpl +++ b/src/views/400.tpl @@ -1,4 +1,12 @@
    [[global:400.title]] + +

    {error}

    +

    [[global:400.message, {config.relative_path}]]

    + + + +

    [[error:goback]]

    +
    diff --git a/src/views/403.tpl b/src/views/403.tpl index e6800ed72d..bf93b496cd 100644 --- a/src/views/403.tpl +++ b/src/views/403.tpl @@ -6,6 +6,10 @@

    [[global:403.message]]

    + +

    [[error:goback]]

    + +

    [[global:403.login, {config.relative_path}]]

    diff --git a/src/views/500-embed.tpl b/src/views/500-embed.tpl index 9d911e9848..b1045d431f 100644 --- a/src/views/500-embed.tpl +++ b/src/views/500-embed.tpl @@ -1,8 +1,12 @@ - \ No newline at end of file diff --git a/src/views/500.tpl b/src/views/500.tpl index 3abb9e8e27..7795fbbf8a 100644 --- a/src/views/500.tpl +++ b/src/views/500.tpl @@ -3,4 +3,8 @@

    [[global:500.message]]

    {path}

    {error}

    + + +

    [[error:goback]]

    +
    diff --git a/src/views/admin/manage/registration.tpl b/src/views/admin/manage/registration.tpl index 961d4863f7..4f538ecfb8 100644 --- a/src/views/admin/manage/registration.tpl +++ b/src/views/admin/manage/registration.tpl @@ -6,7 +6,7 @@

    - [[admin/manage/registration:description, {config.relative_path}/admin/settings/user]] + [[admin/manage/registration:description, {config.relative_path}/admin/settings/user#user-registration]]

    diff --git a/src/views/admin/partials/categories/privileges.tpl b/src/views/admin/partials/categories/privileges.tpl index 67dce37de6..c5bfc3ec63 100644 --- a/src/views/admin/partials/categories/privileges.tpl +++ b/src/views/admin/partials/categories/privileges.tpl @@ -31,7 +31,7 @@ {privileges.users.username} - {function.spawnPrivilegeStates, privileges.users.username, privileges} + {function.spawnPrivilegeStates, privileges.users.username, ../privileges} @@ -85,7 +85,7 @@ {privileges.groups.name} - {function.spawnPrivilegeStates, privileges.groups.name, privileges} + {function.spawnPrivilegeStates, privileges.groups.name, ../privileges} diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl index 36e84a0f23..50a3423ece 100644 --- a/src/views/admin/settings/email.tpl +++ b/src/views/admin/settings/email.tpl @@ -7,7 +7,7 @@

    - + [[admin/settings/email:address-help]]


    @@ -23,33 +23,57 @@
    -
    [[admin/settings/email:gmail-routing]]
    +
    [[admin/settings/email:smtp-transport]]

    - [[admin/settings/email:gmail-routing-help1]] -

    -

    - [[admin/settings/email:gmail-routing-help2]] + [[admin/settings/email:smtp-transport-help]]

    -
    - - + + +

    + [[admin/settings/email:smtp-transport.service-help]] +
    + [[admin/settings/email:smtp-transport.gmail-warning1]] +
    + [[admin/settings/email:smtp-transport.gmail-warning2]] +

    +
    + +
    + +

    - [[admin/settings/email:gmail-transport.username-help]] + [[admin/settings/email:smtp-transport.username-help]]

    - - + +
    diff --git a/src/views/admin/settings/post.tpl b/src/views/admin/settings/post.tpl index da379a74d7..2c77c48692 100644 --- a/src/views/admin/settings/post.tpl +++ b/src/views/admin/settings/post.tpl @@ -43,13 +43,19 @@
    -
    - - -
    -
    - - +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    diff --git a/src/views/admin/settings/web-crawler.tpl b/src/views/admin/settings/web-crawler.tpl index 45f7156ed9..bc14fc72fd 100644 --- a/src/views/admin/settings/web-crawler.tpl +++ b/src/views/admin/settings/web-crawler.tpl @@ -36,7 +36,7 @@

    - [[admin/settings/web-crawler:view-sitemap]] + [[admin/settings/web-crawler:view-sitemap]]

    diff --git a/src/views/emails/banned.tpl b/src/views/emails/banned.tpl index 2384fe1549..db8975c993 100644 --- a/src/views/emails/banned.tpl +++ b/src/views/emails/banned.tpl @@ -1,18 +1,53 @@ -

    - [[email:banned.text1, {username}, {site_title}]] - - [[email:banned.text2, {until}]] - -

    + - -

    - [[email:banned.text3]] -

    + + -

    - {reason} -

    - + + + + + - \ No newline at end of file + + + + + + +
    + +
    + + + + + + + + + + + + +
    +

    [[email:greeting_with_name, {username}]]

    +

    + [[email:banned.text1, {username}, {site_title}]] + +

    [[email:banned.text2, {until}]] + +

    +
    +

    + [[email:banned.text3]] +

    {reason} +

    +
    +

    [[email:closing]]

    +

    {site_title}

    +
    +
    + + + diff --git a/src/views/emails/digest.tpl b/src/views/emails/digest.tpl index 23d20a89c9..24692949cb 100644 --- a/src/views/emails/digest.tpl +++ b/src/views/emails/digest.tpl @@ -1,50 +1,93 @@ -

    [[email:greeting_with_name, {username}]],

    + - -

    [[email:digest.notifications, {site_title}]]

    + + - + + + + + -
    - + + + + + -

    [[email:digest.latest_topics, {site_title}]]

    - - -

    - [[email:digest.cta, {site_title}]] -

    +
    + +
    + + + + + + + + + + + + + + + + + + +
    +

    [[email:greeting_with_name, {username}]],

    +
    +

    [[email:digest.notifications, {site_title}]]

    + +
    +

    [[email:digest.latest_topics, {site_title}]]

    + +
    + + + + + +
    + +     [[email:digest.cta, {site_title}]]     + +
    + +
    +

    [[email:closing]]

    +

    {site_title}

    +
    +
    + - -
    -

    - [[email:digest.unsub.info]] [[email:unsub.cta]]. -

    \ No newline at end of file diff --git a/src/views/emails/invitation.tpl b/src/views/emails/invitation.tpl index 1b574dde08..da800eebdb 100644 --- a/src/views/emails/invitation.tpl +++ b/src/views/emails/invitation.tpl @@ -1,11 +1,53 @@ -

    [[email:greeting_no_name]],

    + -

    - [[email:invitation.text1, {username}, {site_title}]] -

    + + -
    - [[email:invitation.ctr]] -
    + + + + + - \ No newline at end of file + + + + + + +
    + +
    + + + + + + + + + + +
    +

    [[email:greeting_no_name]]

    +

    [[email:invitation.text1, {username}, {site_title}]]

    +
    + + + + + +
    + +     [[email:invitation.ctr]]     + +
    + +
    +

    [[email:closing]]

    +

    {site_title}

    +
    +
    + + + diff --git a/src/views/emails/notif_chat.tpl b/src/views/emails/notif_chat.tpl index f1d5a5fc42..275a5bf782 100644 --- a/src/views/emails/notif_chat.tpl +++ b/src/views/emails/notif_chat.tpl @@ -1,13 +1,58 @@ -

    [[email:greeting_with_name, {username}]],

    + -

    {summary}:

    -
    {message.content}
    + + -[[email:notif.chat.cta]] + + + + + - + + + + + + +
    + +
    + + + + + + + + + + + + + +
    +

    [[email:greeting_with_name, {username}]],

    +

    {summary}:

    +
    + {message.content} +
    + + + + + +
    + +     [[email:notif.chat.cta]]     + +
    + +
    +

    [[email:closing]]

    +

    {site_title}

    +
    +
    + -
    -

    - [[email:notif.chat.unsub.info]] [[email:unsub.cta]]. -

    \ No newline at end of file + diff --git a/src/views/emails/notif_post.tpl b/src/views/emails/notif_post.tpl index 4fd360575f..3f0d8ad3a6 100644 --- a/src/views/emails/notif_post.tpl +++ b/src/views/emails/notif_post.tpl @@ -1,13 +1,57 @@ -

    [[email:greeting_with_name, {username}]],

    + -

    {intro}:

    -
    {postBody}
    + + -[[email:notif.post.cta]] + + + + + - + + + + + + +
    + +
    + + + + + + + + + + + + + +
    +

    {intro}

    +
    + {postBody} +
    + + + + + +
    + +     [[email:notif.post.cta]]     + +
    + +
    +

    [[email:closing]]

    +

    {site_title}

    +
    +
    + -
    -

    - [[email:notif.post.unsub.info]] [[email:unsub.cta]]. -

    + diff --git a/src/views/emails/partials/footer.tpl b/src/views/emails/partials/footer.tpl index 256a850914..23a9371134 100644 --- a/src/views/emails/partials/footer.tpl +++ b/src/views/emails/partials/footer.tpl @@ -1,4 +1,25 @@ -

    - [[email:closing]]
    - {site_title} -

    \ No newline at end of file + + + + + +
    +

    + + [[email:notif.post.unsub.info]] [[email:unsub.cta]]. + +

    +
    + + + +
    + + + + + \ No newline at end of file diff --git a/src/views/emails/partials/header.tpl b/src/views/emails/partials/header.tpl new file mode 100644 index 0000000000..61867b16d3 --- /dev/null +++ b/src/views/emails/partials/header.tpl @@ -0,0 +1,183 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + +