diff --git a/.jshintrc b/.jshintrc
index 3a7e093b8f..172f05cea8 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -83,5 +83,7 @@
"white" : false, // true: Check against strict whitespace and indentation rules
// Custom Globals
- "globals" : {} // additional predefined global variables
+ "globals" : {
+ "Promise": true
+ } // additional predefined global variables
}
\ No newline at end of file
diff --git a/package.json b/package.json
index 839a817c27..a897b57966 100644
--- a/package.json
+++ b/package.json
@@ -78,6 +78,7 @@
"request": "^2.44.0",
"rimraf": "~2.5.0",
"rss": "^1.0.0",
+ "sanitize-html": "^1.13.0",
"semver": "^5.1.0",
"serve-favicon": "^2.1.5",
"sitemap": "^1.4.0",
@@ -86,7 +87,7 @@
"socket.io-redis": "2.0.0",
"socketio-wildcard": "~0.3.0",
"string": "^3.0.0",
- "templates.js": "0.3.4",
+ "templates.js": "0.3.5",
"toobusy-js": "^0.5.1",
"uglify-js": "^2.6.0",
"underscore": "^1.8.3",
diff --git a/public/language/en-GB/admin/appearance/skins.json b/public/language/en-GB/admin/appearance/skins.json
new file mode 100644
index 0000000000..4db6fbdd8a
--- /dev/null
+++ b/public/language/en-GB/admin/appearance/skins.json
@@ -0,0 +1,9 @@
+{
+ "loading": "Loading Skins...",
+ "homepage": "Homepage",
+ "select-skin": "Select Skin",
+ "current-skin": "Current Skin",
+ "skin-updated": "Skin Updated",
+ "applied-success": "%1 skin was succesfully applied",
+ "revert-success": "Skin reverted to base colours"
+}
\ No newline at end of file
diff --git a/public/language/en-GB/admin/appearance/themes.json b/public/language/en-GB/admin/appearance/themes.json
new file mode 100644
index 0000000000..3148a01337
--- /dev/null
+++ b/public/language/en-GB/admin/appearance/themes.json
@@ -0,0 +1,11 @@
+{
+ "checking-for-installed": "Checking for installed themes...",
+ "homepage": "Homepage",
+ "select-theme": "Select Theme",
+ "current-theme": "Current Theme",
+ "no-themes": "No installed themes found",
+ "revert-confirm": "Are you sure you wish to restore the default NodeBB theme?",
+ "theme-changed": "Theme Changed",
+ "revert-success": "You have successfully reverted your NodeBB back to it's default theme.",
+ "restart-to-activate": "Please restart your NodeBB to fully activate this theme"
+}
\ No newline at end of file
diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less
index 0ceeb050e5..aa5efff256 100644
--- a/public/less/admin/admin.less
+++ b/public/less/admin/admin.less
@@ -29,6 +29,7 @@
@import "./modules/selectable";
@import "./modules/snackbar";
@import "./modules/nprogress";
+@import "./modules/search";
body {
overflow-y: scroll;
diff --git a/public/less/admin/modules/search.less b/public/less/admin/modules/search.less
new file mode 100644
index 0000000000..d2286005bf
--- /dev/null
+++ b/public/less/admin/modules/search.less
@@ -0,0 +1,45 @@
+#acp-search {
+ .dropdown-menu {
+ max-height: 75vh;
+ overflow-y: auto;
+
+ > li > a {
+ &.focus {
+ &:extend(.dropdown-menu>li>a:focus);
+ }
+ &:focus {
+ outline: none;
+ }
+ }
+ }
+
+ .state-start-typing {
+ .keep-typing, .search-forum, .no-results {
+ display: none;
+ }
+ }
+
+ .state-keep-typing {
+ .start-typing, .search-forum, .no-results {
+ display: none;
+ }
+ }
+
+ .state-no-results {
+ .keep-typing, .start-typing {
+ display: none;
+ }
+ }
+
+ .state-yes-results {
+ .keep-typing, .start-typing, .no-results {
+ display: none;
+ }
+ }
+
+ .search-disabled {
+ .search-forum {
+ display: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/src/admin/appearance/skins.js b/public/src/admin/appearance/skins.js
index 7583dc952d..35ad5a2289 100644
--- a/public/src/admin/appearance/skins.js
+++ b/public/src/admin/appearance/skins.js
@@ -1,7 +1,7 @@
"use strict";
/* global define, app, socket, templates */
-define('admin/appearance/skins', function () {
+define('admin/appearance/skins', ['translator'], function (translator) {
var Skins = {};
Skins.init = function () {
@@ -40,8 +40,8 @@ define('admin/appearance/skins', function () {
app.alert({
alert_id: 'admin:theme',
type: 'info',
- title: 'Skin Updated',
- message: themeId ? (themeId + ' skin was successfully applied') : 'Skin reverted to base colours',
+ title: '[[admin/appearance/skins:skin-updated]]',
+ message: themeId ? ('[[admin/appearance/skins:applied-success, ' + themeId + ']]') : '[[admin/appearance/skins:revert-success]]',
timeout: 5000
});
});
@@ -67,40 +67,48 @@ define('admin/appearance/skins', function () {
}),
showRevert: true
}, function (html) {
- themeContainer.html(html);
+ translator.translate(html, function (html) {
+ themeContainer.html(html);
- if (config['theme:src']) {
- var skin = config['theme:src']
+ if (config['theme:src']) {
+ var skin = config['theme:src']
.match(/latest\/(\S+)\/bootstrap.min.css/)[1]
.replace(/(^|\s)([a-z])/g , function (m,p1,p2) {return p1 + p2.toUpperCase();});
- highlightSelectedTheme(skin);
- }
+ highlightSelectedTheme(skin);
+ }
+ });
});
};
function highlightSelectedTheme(themeId) {
- $('[data-theme]')
- .removeClass('selected')
- .find('[data-action="use"]').each(function () {
- if ($(this).parents('[data-theme]').attr('data-theme')) {
- $(this)
- .html('Select Skin')
- .removeClass('btn-success')
- .addClass('btn-primary');
- }
- });
+ translator.translate('[[admin/appearance/skins:select-skin]] || [[admin/appearance/skins:current-skin]]', function (text) {
+ text = text.split(' || ');
+ var select = text[0];
+ var current = text[1];
- if (!themeId) {
- return;
- }
+ $('[data-theme]')
+ .removeClass('selected')
+ .find('[data-action="use"]').each(function () {
+ if ($(this).parents('[data-theme]').attr('data-theme')) {
+ $(this)
+ .html(select)
+ .removeClass('btn-success')
+ .addClass('btn-primary');
+ }
+ });
- $('[data-theme="' + themeId + '"]')
- .addClass('selected')
- .find('[data-action="use"]')
- .html('Current Skin')
- .removeClass('btn-primary')
- .addClass('btn-success');
+ if (!themeId) {
+ return;
+ }
+
+ $('[data-theme="' + themeId + '"]')
+ .addClass('selected')
+ .find('[data-action="use"]')
+ .html(current)
+ .removeClass('btn-primary')
+ .addClass('btn-success');
+ });
}
return Skins;
diff --git a/public/src/admin/appearance/themes.js b/public/src/admin/appearance/themes.js
index 835dcda654..0c71baa4c4 100644
--- a/public/src/admin/appearance/themes.js
+++ b/public/src/admin/appearance/themes.js
@@ -1,7 +1,7 @@
"use strict";
/* global define, app, socket, bootbox, templates, config */
-define('admin/appearance/themes', function () {
+define('admin/appearance/themes', ['translator'], function (translator) {
var Themes = {};
Themes.init = function () {
@@ -28,8 +28,8 @@ define('admin/appearance/themes', function () {
app.alert({
alert_id: 'admin:theme',
type: 'info',
- title: 'Theme Changed',
- message: 'Please restart your NodeBB to fully activate this theme',
+ title: '[[admin/appearance/themes:theme-changed]]',
+ message: '[[admin/appearance/themes:restart-to-activate]]',
timeout: 5000,
clickfn: function () {
socket.emit('admin.restart');
@@ -38,27 +38,29 @@ define('admin/appearance/themes', function () {
});
}
});
-
- $('#revert_theme').on('click', function () {
- bootbox.confirm('Are you sure you wish to restore the default NodeBB theme?', function (confirm) {
- if (confirm) {
- socket.emit('admin.themes.set', {
- type: 'local',
- id: 'nodebb-theme-persona'
- }, function (err) {
- if (err) {
- return app.alertError(err.message);
- }
- highlightSelectedTheme('nodebb-theme-persona');
- app.alert({
- alert_id: 'admin:theme',
- type: 'success',
- title: 'Theme Changed',
- message: 'You have successfully reverted your NodeBB back to it\'s default theme.',
- timeout: 3500
+
+ translator.translate('[[admin/appearance/themes:revert-confirm]]', function (revert) {
+ $('#revert_theme').on('click', function () {
+ bootbox.confirm(revert, function (confirm) {
+ if (confirm) {
+ socket.emit('admin.themes.set', {
+ type: 'local',
+ id: 'nodebb-theme-persona'
+ }, function (err) {
+ if (err) {
+ return app.alertError(err.message);
+ }
+ highlightSelectedTheme('nodebb-theme-persona');
+ app.alert({
+ alert_id: 'admin:theme',
+ type: 'success',
+ title: '[[admin/appearance/themes:theme-changed]]',
+ message: '[[admin/appearance/themes:revert-success]]',
+ timeout: 3500
+ });
});
- });
- }
+ }
+ });
});
});
@@ -70,17 +72,17 @@ define('admin/appearance/themes', function () {
var instListEl = $('#installed_themes');
if (!themes.length) {
- instListEl.append($('
').addClass('no-themes').html('No installed themes found'));
+ translator.translate('[[admin/appearance/themes:no-themes]]', function (text) {
+ instListEl.append($('').addClass('no-themes').html(text));
+ });
return;
} else {
templates.parse('admin/partials/theme_list', {
themes: themes
}, function (html) {
- require(['translator'], function (translator) {
- translator.translate(html, function (html) {
- instListEl.html(html);
- highlightSelectedTheme(config['theme:id']);
- });
+ translator.translate(html, function (html) {
+ instListEl.html(html);
+ highlightSelectedTheme(config['theme:id']);
});
});
}
@@ -88,19 +90,25 @@ define('admin/appearance/themes', function () {
};
function highlightSelectedTheme(themeId) {
- $('[data-theme]')
- .removeClass('selected')
- .find('[data-action="use"]')
- .html('Select Theme')
- .removeClass('btn-success')
- .addClass('btn-primary');
+ translator.translate('[[admin/appearance/themes:select-theme]] || [[admin/appearance/themes:current-theme]]', function (text) {
+ text = text.split(' || ');
+ var select = text[0];
+ var current = text[1];
- $('[data-theme="' + themeId + '"]')
- .addClass('selected')
- .find('[data-action="use"]')
- .html('Current Theme')
- .removeClass('btn-primary')
- .addClass('btn-success');
+ $('[data-theme]')
+ .removeClass('selected')
+ .find('[data-action="use"]')
+ .html(select)
+ .removeClass('btn-success')
+ .addClass('btn-primary');
+
+ $('[data-theme="' + themeId + '"]')
+ .addClass('selected')
+ .find('[data-action="use"]')
+ .html(current)
+ .removeClass('btn-primary')
+ .addClass('btn-success');
+ });
}
return Themes;
diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js
index f64a2490e7..4f46898b8e 100644
--- a/public/src/admin/modules/search.js
+++ b/public/src/admin/modules/search.js
@@ -1,114 +1,148 @@
"use strict";
-/*globals define, admin, ajaxify, RELATIVE_PATH*/
+/* globals socket, app, define, ajaxify, config */
-define(function () {
- var search = {},
- searchIndex;
+define('admin/modules/search', ['mousetrap'], function (mousetrap) {
+ var search = {};
+
+ function nsToTitle(namespace) {
+ return namespace.replace('admin/', '').split('/').map(function (str) {
+ return str[0].toUpperCase() + str.slice(1);
+ }).join(' > ');
+ }
+
+ function find(dict, term) {
+ var html = dict.filter(function (elem) {
+ return elem.translations.toLowerCase().includes(term);
+ }).map(function (params) {
+ var namespace = params.namespace;
+ var translations = params.translations;
+ var title = params.title == null ? nsToTitle(namespace) : params.title;
+
+ var results = translations
+ // remove all lines without a match
+ .replace(new RegExp('^(?:(?!' + term + ').)*$', 'gmi'), '')
+ // get up to 25 characaters of context on both sides of the match
+ // and wrap the match in a `.search-match` element
+ .replace(
+ new RegExp('^[\\s\\S]*?(.{0,25})(' + term + ')(.{0,25})[\\s\\S]*?$', 'gmi'),
+ '...$1$2$3...
'
+ )
+ // collapse whitespace
+ .replace(/(?:\n ?)+/g, '\n');
+
+ return '' +
+ '' +
+ title +
+ '
' +
+ '' +
+ results +
+ '' +
+ '' +
+ '';
+ }).join('');
+ return html;
+ }
search.init = function () {
- $.getJSON(RELATIVE_PATH + '/templates/indexed.json', function (data) {
- searchIndex = data;
- for (var file in searchIndex) {
- if (searchIndex.hasOwnProperty(file)) {
- searchIndex[file] = searchIndex[file].replace(/
' + searchIndex[file] + '');
- searchIndex[file].find('script').remove();
-
- searchIndex[file] = searchIndex[file].text().toLowerCase().replace(/[ |\r|\n]+/g, ' ');
- }
+ socket.emit('admin.getSearchDict', {}, function (err, dict) {
+ if (err) {
+ app.alertError(err);
+ throw err;
}
-
- delete searchIndex['/admin/header.tpl'];
- delete searchIndex['/admin/footer.tpl'];
-
- setupACPSearch();
+ setupACPSearch(dict);
});
};
- function setupACPSearch() {
- var menu = $('#acp-search .dropdown-menu'),
- routes = [],
- input = $('#acp-search input'),
- firstResult = null;
+ function setupACPSearch(dict) {
+ var dropdown = $('#acp-search .dropdown');
+ var menu = $('#acp-search .dropdown-menu');
+ var input = $('#acp-search input');
+
+ if (!config.searchEnabled) {
+ menu.addClass('search-disabled');
+ }
input.on('keyup', function () {
- $('#acp-search .dropdown').addClass('open');
+ dropdown.addClass('open');
});
$('#acp-search').parents('form').on('submit', function (ev) {
- var input = $(this).find('input'),
- href = firstResult ? firstResult : RELATIVE_PATH + '/search/' + input.val();
+ var selected = menu.find('li.result > a.focus').attr('href');
+ if (!selected.length) {
+ selected = menu.find('li.result > a').first().attr('href');
+ }
+ var href = selected ? selected : config.relative_path + '/search/' + input.val();
ajaxify.go(href.replace(/^\//, ''));
setTimeout(function () {
- $('#acp-search .dropdown').removeClass('open');
- $(input).blur();
+ dropdown.removeClass('open');
+ input.blur();
}, 150);
ev.preventDefault();
return false;
});
- $('#main-menu a').each(function (idx, link) {
- routes.push($(link).attr('href'));
+ mousetrap(input[0]).bind(['up', 'down'], function (ev, key) {
+ var next;
+ if (key === 'up') {
+ next = menu.find('li.result > a.focus').removeClass('focus').parent().prev('.result').children();
+ if (!next.length) {
+ next = menu.find('li.result > a').last();
+ }
+ next.addClass('focus');
+ if (menu[0].getBoundingClientRect().top > next[0].getBoundingClientRect().top) {
+ next[0].scrollIntoView(true);
+ }
+ } else if (key === 'down') {
+ next = menu.find('li.result > a.focus').removeClass('focus').parent().next('.result').children();
+ if (!next.length) {
+ next = menu.find('li.result > a').first();
+ }
+ next.addClass('focus');
+ if (menu[0].getBoundingClientRect().bottom < next[0].getBoundingClientRect().bottom) {
+ next[0].scrollIntoView(false);
+ }
+ }
+
+ ev.preventDefault();
});
+ var prevValue;
+
input.on('keyup focus', function () {
- var $input = $(this),
- value = $input.val().toLowerCase(),
- menuItems = $('#acp-search .dropdown-menu').html('');
+ var value = input.val().toLowerCase();
- function toUpperCase(txt) {
- return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
+ if (value === prevValue) {
+ return;
}
+ prevValue = value;
- firstResult = null;
+ menu.children('.result').remove();
- if (value.length >= 3) {
- for (var file in searchIndex) {
- if (searchIndex.hasOwnProperty(file)) {
- var position = searchIndex[file].indexOf(value);
+ var len = value.length;
+ var results;
- if (position !== -1) {
- var href = file.replace('.tpl', ''),
- title = href.replace(/^\/admin\//, '').split('/'),
- description = searchIndex[file].substring(Math.max(0, position - 25), Math.min(searchIndex[file].length - 1, position + 25))
- .replace(value, '' + value + '');
+ menu.toggleClass('state-start-typing', len === 0);
+ menu.toggleClass('state-keep-typing', len > 0 && len < 3);
+
+ if (len >= 3) {
+ menu.prepend(find(dict, value));
- for (var t in title) {
- if (title.hasOwnProperty(t)) {
- title[t] = title[t]
- .replace('-', ' ')
- .replace(/\w\S*/g, toUpperCase);
- }
- }
+ results = menu.children('.result').length;
- title = title.join(' > ');
- href = RELATIVE_PATH + href;
- firstResult = firstResult ? firstResult : href;
+ menu.toggleClass('state-no-results', !results);
+ menu.toggleClass('state-yes-results', !!results);
- if ($.inArray(href, routes) !== -1) {
- menuItems.append('' + title + '
...' + description + '...');
- }
- }
- }
- }
-
- if (menuItems.html() === '') {
- menuItems.append('No results...');
- }
- }
-
- if (value.length > 0) {
- if (config.searchEnabled) {
- menuItems.append('');
- menuItems.append('Search the forum for ' + value + '');
- } else if (value.length < 3) {
- menuItems.append('Type more to see results...');
- }
+ menu.find('.search-forum')
+ .not('.divider')
+ .find('a')
+ .attr('href', config.relative_path + '/search/' + value)
+ .find('strong')
+ .html(value);
} else {
- menuItems.append('Start typing to see results...');
+ menu.removeClass('state-no-results state-yes-results');
}
});
}
diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js
index 0771ab56a5..a876dba44c 100644
--- a/public/src/modules/translator.js
+++ b/public/src/modules/translator.js
@@ -3,7 +3,7 @@
(function (factory) {
'use strict';
function loadClient(language, namespace) {
- return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + namespace));
+ return Promise.resolve(jQuery.getJSON(config.relative_path + '/api/language/' + language + '/' + encodeURIComponent(namespace)));
}
if (typeof define === 'function' && define.amd) {
// AMD. Register as a named module
@@ -339,6 +339,68 @@
Translator.moduleFactories = {};
+ /**
+ * Remove the translator patterns from text
+ * @param {string} text
+ * @returns {string}
+ */
+ Translator.removePatterns = function removePatterns(text) {
+ var len = text.length;
+ var cursor = 0;
+ var lastBreak = 0;
+ var level = 0;
+ var out = '';
+ var sub;
+
+ while (cursor < len) {
+ sub = text.slice(cursor, cursor + 2);
+ if (sub === '[[') {
+ if (level === 0) {
+ out += text.slice(lastBreak, cursor);
+ }
+ level += 1;
+ cursor += 2;
+ } else if (sub === ']]') {
+ level -= 1;
+ cursor += 2;
+ if (level === 0) {
+ lastBreak = cursor;
+ }
+ } else {
+ cursor += 1;
+ }
+ }
+ out += text.slice(lastBreak, cursor);
+ return out;
+ };
+
+ /**
+ * Escape translator patterns in text
+ * @param {string} text
+ * @returns {string}
+ */
+ Translator.escape = function escape(text) {
+ return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text;
+ };
+
+ /**
+ * Unescape escaped translator patterns in text
+ * @param {string} text
+ * @returns {string}
+ */
+ Translator.unescape = function unescape(text) {
+ return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text;
+ };
+
+ /**
+ * Construct a translator pattern
+ */
+ Translator.compile = function compile() {
+ var args = Array.prototype.slice.call(arguments, 0);
+
+ return '[[' + args.join(', ') + ']]';
+ };
+
return Translator;
}());
@@ -348,12 +410,16 @@
*/
Translator: Translator,
+ compile: Translator.compile,
+ escape: Translator.escape,
+ unescape: Translator.unescape,
+ getLanguage: Translator.getLanguage,
+
/**
* Legacy translator function for backwards compatibility
*/
translate: function translate(text, language, callback) {
- // console.warn('[translator] `translator.translate(text, [lang, ]callback)` is deprecated. ' +
- // 'Use the `translator.Translator` class instead.');
+ // TODO: deprecate?
var cb = callback;
var lang = language;
@@ -373,31 +439,6 @@
});
},
- /**
- * Construct a translator pattern
- * @param {string} name - Translation name
- * @param {string[]} args - Optional arguments for the pattern
- */
- compile: function compile() {
- var args = Array.prototype.slice.call(arguments, 0);
-
- return '[[' + args.join(', ') + ']]';
- },
-
- /**
- * Escape translation patterns from text
- */
- escape: function escape(text) {
- return typeof text === 'string' ? text.replace(/\[\[([\S]*?)\]\]/g, '\\[\\[$1\\]\\]') : text;
- },
-
- /**
- * Unescape translation patterns from text
- */
- unescape: function unescape(text) {
- return typeof text === 'string' ? text.replace(/\\\[\\\[([\S]*?)\\\]\\\]/g, '[[$1]]') : text;
- },
-
/**
* Add translations to the cache
*/
@@ -422,11 +463,6 @@
adaptor.getTranslations(language, namespace, callback);
},
- /**
- * Get the language of the current environment, falling back to defaults
- */
- getLanguage: Translator.getLanguage,
-
toggleTimeagoShorthand: function toggleTimeagoShorthand() {
var tmp = assign({}, jQuery.timeago.settings.strings);
jQuery.timeago.settings.strings = assign({}, adaptor.timeagoShort);
diff --git a/src/admin/search.js b/src/admin/search.js
new file mode 100644
index 0000000000..8f567071bf
--- /dev/null
+++ b/src/admin/search.js
@@ -0,0 +1,183 @@
+'use strict';
+
+var fs = require('fs');
+var path = require('path');
+var async = require('async');
+var sanitizeHTML = require('sanitize-html');
+
+var languages = require('../languages');
+var utils = require('../../public/src/utils');
+var Translator = require('../../public/src/modules/translator').Translator;
+
+function filterDirectories(directories) {
+ return directories.map(function (dir) {
+ // get the relative path
+ return dir.replace(/^.*(admin.*?).tpl$/, '$1');
+ }).filter(function (dir) {
+ // exclude partials
+ // only include subpaths
+ // exclude category.tpl, group.tpl, category-analytics.tpl
+ return !dir.includes('/partials/') &&
+ /\/.*\//.test(dir) &&
+ !/category|group|category\-analytics$/.test(dir);
+ });
+}
+
+function getAdminNamespaces(callback) {
+ utils.walk(path.resolve(__dirname, '../../public/templates/admin'), function (err, directories) {
+ if (err) {
+ return callback(err);
+ }
+
+ callback(null, filterDirectories(directories));
+ });
+}
+
+function sanitize(html) {
+ // reduce the template to just meaningful text
+ // remove all tags and strip out scripts, etc completely
+ return sanitizeHTML(html, {
+ allowedTags: [],
+ allowedAttributes: [],
+ });
+}
+
+function simplify(translations) {
+ return translations
+ // remove all mustaches
+ .replace(/(?:\{{1,2}[^\}]*?\}{1,2})/g, '')
+ // collapse whitespace
+ .replace(/(?:[ \t]*[\n\r]+[ \t]*)+/g, '\n')
+ .replace(/[\t ]+/g, ' ');
+}
+
+function nsToTitle(namespace) {
+ return namespace.replace('admin/', '').split('/').map(function (str) {
+ return str[0].toUpperCase() + str.slice(1);
+ }).join(' > ');
+}
+
+var fallbackCacheInProgress = {};
+var fallbackCache = {};
+
+function initFallback(namespace, callback) {
+ fs.readFile(path.resolve(__dirname, '../../public/templates/', namespace + '.tpl'), function (err, file) {
+ if (err) {
+ return callback(err);
+ }
+
+ var template = file.toString();
+
+ var translations = sanitize(template);
+ translations = Translator.removePatterns(translations);
+ translations = simplify(translations);
+ translations += '\n' + nsToTitle(namespace);
+
+ callback(null, {
+ namespace: namespace,
+ translations: translations,
+ });
+ });
+}
+
+function fallback(namespace, callback) {
+ if (fallbackCache[namespace]) {
+ return callback(null, fallbackCache[namespace]);
+ }
+ if (fallbackCacheInProgress[namespace]) {
+ return fallbackCacheInProgress[namespace].push(callback);
+ }
+
+ fallbackCacheInProgress[namespace] = [function (err, params) {
+ if (err) {
+ return callback(err);
+ }
+
+ callback(null, params);
+ }];
+ initFallback(namespace, function (err, params) {
+ fallbackCacheInProgress[namespace].forEach(function (fn) {
+ fn(err, params);
+ });
+ fallbackCacheInProgress[namespace] = null;
+ fallbackCache[namespace] = params;
+ });
+}
+
+function initDict(language, callback) {
+ getAdminNamespaces(function (err, namespaces) {
+ if (err) {
+ return callback(err);
+ }
+
+ async.map(namespaces, function (namespace, cb) {
+ async.waterfall([
+ function (next) {
+ languages.get(language, namespace, next);
+ },
+ function (translations, next) {
+ if (!translations || !Object.keys(translations).length) {
+ return next(Error('No translations for ' + language + '/' + namespace));
+ }
+
+ // join all translations into one string separated by newlines
+ var str = Object.keys(translations).map(function (key) {
+ return translations[key];
+ }).join('\n');
+
+ next(null, {
+ namespace: namespace,
+ translations: str,
+ });
+ }
+ ], function (err, params) {
+ if (err) {
+ return fallback(namespace, function (err, params) {
+ if (err) {
+ return cb({
+ namespace: namespace,
+ translations: '',
+ });
+ }
+
+ cb(null, params);
+ });
+ }
+
+ cb(null, params);
+ });
+ }, callback);
+ });
+}
+
+var cacheInProgress = {};
+var cache = {};
+
+function getDictionary(language, callback) {
+ if (cache[language]) {
+ return callback(null, cache[language]);
+ }
+ if (cacheInProgress[language]) {
+ return cacheInProgress[language].push(callback);
+ }
+
+ cacheInProgress[language] = [function (err, params) {
+ if (err) {
+ return callback(err);
+ }
+
+ callback(null, params);
+ }];
+ initDict(language, function (err, params) {
+ cacheInProgress[language].forEach(function (fn) {
+ fn(err, params);
+ });
+ cacheInProgress[language] = null;
+ cache[language] = params;
+ });
+}
+
+module.exports.getDictionary = getDictionary;
+module.exports.filterDirectories = filterDirectories;
+module.exports.simplify = simplify;
+module.exports.sanitize = sanitize;
diff --git a/src/groups.js b/src/groups.js
index fbca035cba..7498ea4a2c 100644
--- a/src/groups.js
+++ b/src/groups.js
@@ -57,30 +57,24 @@ var utils = require('../public/src/utils');
};
Groups.getGroupsFromSet = function (set, uid, start, stop, callback) {
- var method;
- var args;
- if (set === 'groups:visible:name') {
- method = db.getSortedSetRangeByLex;
- args = [set, '-', '+', start, stop - start + 1, done];
- } else {
- method = db.getSortedSetRevRange;
- args = [set, start, stop, done];
- }
- method.apply(null, args);
+ async.waterfall([
+ function (next) {
+ if (set === 'groups:visible:name') {
+ db.getSortedSetRangeByLex(set, '-', '+', start, stop - start + 1, next);
+ } else {
+ db.getSortedSetRevRange(set, start, stop, next);
+ }
+ },
+ function (groupNames, next) {
+ if (set === 'groups:visible:name') {
+ groupNames = groupNames.map(function (name) {
+ return name.split(':')[1];
+ });
+ }
- function done(err, groupNames) {
- if (err) {
- return callback(err);
+ Groups.getGroupsAndMembers(groupNames, next);
}
-
- if (set === 'groups:visible:name') {
- groupNames = groupNames.map(function (name) {
- return name.split(':')[1];
- });
- }
-
- Groups.getGroupsAndMembers(groupNames, callback);
- }
+ ], callback);
};
Groups.getGroups = function (set, start, stop, callback) {
diff --git a/src/groups/create.js b/src/groups/create.js
index 1d16ea33cf..a59567afe6 100644
--- a/src/groups/create.js
+++ b/src/groups/create.js
@@ -92,6 +92,10 @@ module.exports = function (Groups) {
return callback(new Error('[[error:group-name-too-long]]'));
}
+ if (!Groups.isPrivilegeGroup(name) && name.indexOf(':') !== -1) {
+ return callback(new Error('[[error:invalid-group-name]]'));
+ }
+
if (name.indexOf('/') !== -1 || !utils.slugify(name)) {
return callback(new Error('[[error:invalid-group-name]]'));
}
diff --git a/src/languages.js b/src/languages.js
index 86563628b2..f3b9aa5743 100644
--- a/src/languages.js
+++ b/src/languages.js
@@ -75,7 +75,7 @@ Languages.list = function (callback) {
fs.readFile(configPath, function (err, stream) {
if (err) {
- next();
+ return next(err);
}
languages.push(JSON.parse(stream.toString()));
next();
diff --git a/src/meta/templates.js b/src/meta/templates.js
index 4e7f934624..fd5c1f71e1 100644
--- a/src/meta/templates.js
+++ b/src/meta/templates.js
@@ -12,7 +12,6 @@ var plugins = require('../plugins');
var utils = require('../../public/src/utils');
var Templates = {};
-var searchIndex = {};
Templates.compile = function (callback) {
callback = callback || function () {};
@@ -131,10 +130,6 @@ function compile(callback) {
}
}
- if (relativePath.match(/^\/admin\/[\s\S]*?/)) {
- addIndex(relativePath, file);
- }
-
mkdirp.sync(path.join(viewsPath, relativePath.split('/').slice(0, -1).join('/')));
fs.writeFile(path.join(viewsPath, relativePath), file, next);
}, function (err) {
@@ -143,25 +138,11 @@ function compile(callback) {
return callback(err);
}
- compileIndex(viewsPath, function (err) {
- if (err) {
- return callback(err);
- }
- winston.verbose('[meta/templates] Successfully compiled templates.');
+ winston.verbose('[meta/templates] Successfully compiled templates.');
- callback();
- });
+ callback();
});
});
}
-
-function addIndex(path, file) {
- searchIndex[path] = file;
-}
-
-function compileIndex(viewsPath, callback) {
- fs.writeFile(path.join(viewsPath, '/indexed.json'), JSON.stringify(searchIndex), callback);
-}
-
module.exports = Templates;
\ No newline at end of file
diff --git a/src/middleware/render.js b/src/middleware/render.js
index 399f81abbe..a15ee121e6 100644
--- a/src/middleware/render.js
+++ b/src/middleware/render.js
@@ -111,6 +111,7 @@ module.exports = function (middleware) {
var clean = req.path.replace(/^\/api/, '').replace(/^\/|\/$/g, '');
var parts = clean.split('/').slice(0, 3);
parts.forEach(function (p, index) {
+ p = decodeURIComponent(p);
parts[index] = index ? parts[0] + '-' + p : 'page-' + (p || 'home');
});
return parts.join(' ');
diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js
index c3220ed094..141d567f11 100644
--- a/src/socket.io/admin.js
+++ b/src/socket.io/admin.js
@@ -15,6 +15,7 @@ var emailer = require('../emailer');
var db = require('../database');
var analytics = require('../analytics');
var index = require('./index');
+var getAdminSearchDict = require('../admin/search').getDictionary;
var SocketAdmin = {
user: require('./admin/user'),
@@ -277,5 +278,15 @@ SocketAdmin.deleteAllEvents = function (socket, data, callback) {
events.deleteAll(callback);
};
+SocketAdmin.getSearchDict = function (socket, data, callback) {
+ user.getSettings(socket.uid, function (err, settings) {
+ if (err) {
+ return callback(err);
+ }
+ var lang = settings.userLang || meta.config.defaultLang || 'en-GB';
+ getAdminSearchDict(lang, callback);
+ });
+};
+
module.exports = SocketAdmin;
diff --git a/src/topics/create.js b/src/topics/create.js
index 852b987ffc..bf62266ecf 100644
--- a/src/topics/create.js
+++ b/src/topics/create.js
@@ -90,12 +90,12 @@ module.exports = function (Topics) {
Topics.post = function (data, callback) {
var uid = data.uid;
- var title = String(data.title).trim();
+ data.title = String(data.title).trim();
data.tags = data.tags || [];
async.waterfall([
function (next) {
- check(title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long', next);
+ check(data.title, meta.config.minimumTitleLength, meta.config.maximumTitleLength, 'title-too-short', 'title-too-long', next);
},
function (next) {
check(data.tags, meta.config.minimumTagsPerTopic, meta.config.maximumTagsPerTopic, 'not-enough-tags', 'too-many-tags', next);
diff --git a/src/views/admin/appearance/skins.tpl b/src/views/admin/appearance/skins.tpl
index 0c1b543a47..c5d1355f08 100644
--- a/src/views/admin/appearance/skins.tpl
+++ b/src/views/admin/appearance/skins.tpl
@@ -1,6 +1,6 @@
- Loading Skins
+ [[admin/appearance/skins:loading]]
diff --git a/src/views/admin/appearance/themes.tpl b/src/views/admin/appearance/themes.tpl
index e67c6a2755..8203c84e8b 100644
--- a/src/views/admin/appearance/themes.tpl
+++ b/src/views/admin/appearance/themes.tpl
@@ -1,6 +1,6 @@
- Checking for installed themes...
+ [[admin/appearance/themes:checking-for-installed]]
diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl
index 0fea719e46..9bc0c48d7e 100644
--- a/src/views/admin/partials/menu.tpl
+++ b/src/views/admin/partials/menu.tpl
@@ -149,7 +149,23 @@
diff --git a/src/views/admin/partials/theme_list.tpl b/src/views/admin/partials/theme_list.tpl
index 1517eb5c9e..26a226b803 100644
--- a/src/views/admin/partials/theme_list.tpl
+++ b/src/views/admin/partials/theme_list.tpl
@@ -10,13 +10,13 @@
- Homepage
+ [[admin/appearance/themes:homepage]]
diff --git a/test/groups.js b/test/groups.js
index 182910bc32..81d37ed833 100644
--- a/test/groups.js
+++ b/test/groups.js
@@ -255,6 +255,13 @@ describe('Groups', function () {
done();
});
});
+
+ it('should fail if group name is invalid', function (done) {
+ Groups.create({name: 'not:valid'}, function (err) {
+ assert.equal(err.message, '[[error:invalid-group-name]]');
+ done();
+ });
+ });
});
describe('.hide()', function () {
diff --git a/test/search-admin.js b/test/search-admin.js
new file mode 100644
index 0000000000..216d26d35f
--- /dev/null
+++ b/test/search-admin.js
@@ -0,0 +1,82 @@
+'use strict';
+/*global require*/
+
+var assert = require('assert');
+var search = require('../src/admin/search');
+
+describe('admin search', function () {
+ describe('filterDirectories', function () {
+ it('should resolve all paths to relative paths', function (done) {
+ assert.deepEqual(search.filterDirectories([
+ 'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
+ ]), [
+ 'admin/gdhgfsdg/sggag',
+ ]);
+ done();
+ });
+ it('should exclude partials', function (done) {
+ assert.deepEqual(search.filterDirectories([
+ 'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
+ 'dfahdfsgf/admin/partials/hgkfds/fdhsdfh.tpl',
+ ]), [
+ 'admin/gdhgfsdg/sggag',
+ ]);
+ done();
+ });
+ it('should exclude files in the admin directory', function (done) {
+ assert.deepEqual(search.filterDirectories([
+ 'hfjksfd/fdsgagag/admin/gdhgfsdg/sggag.tpl',
+ 'dfdasg/admin/hjkdfsk.tpl',
+ ]), [
+ 'admin/gdhgfsdg/sggag',
+ ]);
+ done();
+ });
+ });
+
+ describe('sanitize', function () {
+ it('should strip out scripts', function (done) {
+ assert.equal(
+ search.sanitize('Pellentesque tristique senectus' +
+ ' habitant morbi'),
+ 'Pellentesque tristique senectus' +
+ ' habitant morbi'
+ );
+ done();
+ });
+ it('should remove all tags', function (done) {
+ assert.equal(
+ search.sanitize('Pellentesque habitant morbi tristique senectus' +
+ 'Aenean vitae est.Mauris eleifend leo.
'),
+ 'Pellentesque habitant morbi tristique senectus' +
+ 'Aenean vitae est.Mauris eleifend leo.'
+ );
+ done();
+ });
+ });
+
+ describe('simplify', function () {
+ it('should remove all mustaches', function (done) {
+ assert.equal(
+ search.simplify(
+ 'Pellentesque tristique {{senectus}}habitant morbi' +
+ 'liquam tincidunt {mauris.eu}risus'
+ ),
+ 'Pellentesque tristique habitant morbi' +
+ 'liquam tincidunt risus'
+ );
+ done();
+ });
+ it('should collapse all whitespace', function (done) {
+ assert.equal(
+ search.simplify(
+ 'Pellentesque tristique habitant morbi' +
+ ' \n\n liquam tincidunt mauris eu risus.'
+ ),
+ 'Pellentesque tristique habitant morbi' +
+ '\nliquam tincidunt mauris eu risus.'
+ );
+ done();
+ });
+ });
+});
\ No newline at end of file
diff --git a/test/translator.js b/test/translator.js
index 9df035cfde..91fbf8f696 100644
--- a/test/translator.js
+++ b/test/translator.js
@@ -233,3 +233,15 @@ describe('Translator modules', function () {
done();
});
});
+
+describe('Translator static methods', function () {
+ describe('.removePatterns', function () {
+ it('should remove translator patterns from text', function (done) {
+ assert.strictEqual(
+ Translator.removePatterns('Lorem ipsum dolor [[sit:amet]], consectetur adipiscing elit. [[sed:vitae, [[semper:dolor]]]] lorem'),
+ 'Lorem ipsum dolor , consectetur adipiscing elit. lorem'
+ );
+ done();
+ });
+ });
+});