From a431dc030587f47760e005d30576176d0e1f1163 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Mon, 28 Nov 2016 12:52:26 -0700 Subject: [PATCH 01/15] ACP search updated to support translations --- package.json | 1 + .../en_GB/admin/appearance/themes.json | 6 + public/less/admin/admin.less | 1 + public/less/admin/modules/search.less | 36 ++++ public/src/admin/modules/search.js | 155 +++++++++--------- src/admin/search.js | 128 +++++++++++++++ src/socket.io/admin.js | 14 ++ src/views/admin/appearance/themes.tpl | 2 +- src/views/admin/partials/menu.tpl | 18 +- src/views/admin/partials/theme_list.tpl | 4 +- 10 files changed, 280 insertions(+), 85 deletions(-) create mode 100644 public/language/en_GB/admin/appearance/themes.json create mode 100644 public/less/admin/modules/search.less create mode 100644 src/admin/search.js diff --git a/package.json b/package.json index 7bcd0ce3da..78d0f582c9 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", 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..3f1b10cf7f --- /dev/null +++ b/public/language/en_GB/admin/appearance/themes.json @@ -0,0 +1,6 @@ +{ + "checking-for-installed": "Checking for installed themes...", + "homepage": "Homepage", + "select-skin": "Select Skin", + "select-theme": "Select 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..381b940646 --- /dev/null +++ b/public/less/admin/modules/search.less @@ -0,0 +1,36 @@ +#acp-search { + .dropdown-menu { + max-height: 75vh; + overflow-y: auto; + } + + .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/modules/search.js b/public/src/admin/modules/search.js index f64a2490e7..0a5d3dde56 100644 --- a/public/src/admin/modules/search.js +++ b/public/src/admin/modules/search.js @@ -2,113 +2,106 @@ /*globals define, admin, ajaxify, RELATIVE_PATH*/ define(function () { - var search = {}, - searchIndex; + var search = {}; - 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, ' '); - } - } + function nsToTitle(namespace) { + return namespace.replace('admin/', '').split('/').map(function (str) { + return str[0].toUpperCase() + str.slice(1); + }).join(' > '); + } - delete searchIndex['/admin/header.tpl']; - delete searchIndex['/admin/footer.tpl']; + 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 + .replace(new RegExp('^(?:(?!' + term + ').)*$', 'gmi'), '') + .replace( + new RegExp('^[\\s\\S]*?(.{0,25})(' + term + ')(.{0,25})[\\s\\S]*?$', 'gmi'), + '...$1$2$3...
' + ).replace(/(\n ?)+/g, '\n'); + + return ''; + }).join(''); + return html; + } - setupACPSearch(); + search.init = function () { + socket.emit('admin.getSearchDict', {}, function (err, dict) { + if (err) { + app.alertError(err); + throw err; + } + 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 firstResult = menu.find('li:first-child > a').attr('href'); + var href = firstResult ? firstResult : 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')); - }); - input.on('keyup focus', function () { - var $input = $(this), - value = $input.val().toLowerCase(), - menuItems = $('#acp-search .dropdown-menu').html(''); + var value = input.val().toLowerCase(); + menu.children('.result').remove(); - function toUpperCase(txt) { - return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); - } + var len = value.length; + var results; - firstResult = null; - - if (value.length >= 3) { - for (var file in searchIndex) { - if (searchIndex.hasOwnProperty(file)) { - var position = searchIndex[file].indexOf(value); - - 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 + ''); - - for (var t in title) { - if (title.hasOwnProperty(t)) { - title[t] = title[t] - .replace('-', ' ') - .replace(/\w\S*/g, toUpperCase); - } - } - - title = title.join(' > '); - href = RELATIVE_PATH + href; - firstResult = firstResult ? firstResult : href; - - if ($.inArray(href, routes) !== -1) { - menuItems.append('
  • ' + title + '
    ...' + description + '...
  • '); - } - } - } - } - - if (menuItems.html() === '') { - menuItems.append('
  • No results...
  • '); - } - } + menu.toggleClass('state-start-typing', len === 0); + menu.toggleClass('state-keep-typing', len > 0 && len < 3); + + if (len >= 3) { + menu.prepend(find(dict, value)); + + results = menu.children('.result').length; + + menu.toggleClass('state-no-results', !results); + menu.toggleClass('state-yes-results', !!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', RELATIVE_PATH + '/search/' + value) + .find('strong') + .html(value); } else { - menuItems.append('
  • Start typing to see results...
  • '); + menu.removeClass('state-no-results'); } }); } diff --git a/src/admin/search.js b/src/admin/search.js new file mode 100644 index 0000000000..985ea51a05 --- /dev/null +++ b/src/admin/search.js @@ -0,0 +1,128 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var nconf = require('nconf'); +var sanitize = require('sanitize-html'); + +var languages = require('../languages'); +var meta = require('../meta'); +var utils = require('../../public/src/utils'); + +function walk(directory) { + return new Promise(function (resolve, reject) { + utils.walk(directory, function (err, data) { + if (err) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +function readFile(path) { + return new Promise(function (resolve, reject) { + fs.readFile(path, function (err, data) { + if (err) { + reject(err); + } else { + resolve(data.toString()); + } + }); + }); +} + +function loadLanguage(language, filename) { + return new Promise(function (resolve, reject) { + languages.get(language, filename + '.json', function (err, data) { + if (err || !data || !Object.keys(data).length) { + reject(err); + } else { + resolve(data); + } + }); + }); +} + +function getAdminNamespaces() { + return walk(path.resolve('./public/templates/admin')) + .then(function (directories) { + return directories.map(function (dir) { + return dir.replace(/^.*(admin.*?).tpl$/, '$1'); + }).filter(function (dir) { + return !dir.includes('/partials/'); + }).filter(function (dir) { + return dir.match(/\/.*\//); + }); + }); +} + +var fallbackCache = {}; + +function fallback(namespace) { + fallbackCache[namespace] = fallbackCache[namespace] || + readFile(path.resolve('./public/templates/', namespace + '.tpl')) + .then(function (template) { + var translations = sanitize(template, { + transformTags: { + '*': function () { + return { + tagName: 'div' + }; + } + } + }) + .replace(/(
    )|(<\/div>)/g, '') + .replace(/([\n\r]+ ?)+/g, '\n') + .replace(/[\t ]+/g, ' '); + + return { + namespace: namespace, + translations: translations, + }; + }); + return fallbackCache[namespace]; +} + +function initDict(language) { + return getAdminNamespaces().then(function (namespaces) { + return Promise.all(namespaces.map(function (namespace) { + return loadLanguage(language, namespace).then(function (translations) { + return { namespace: namespace, translations: translations }; + }).then(function (params) { + var namespace = params.namespace; + var translations = params.translations; + + var str = Object.keys(translations).map(function (key) { + return translations[key]; + }).join('\n'); + + return { + namespace: namespace, + translations: str + }; + }) + // TODO: Use translator to get title for admin route? + .catch(function () { + return fallback(namespace); + }) + .catch(function () { + return { namespace: namespace, translations: '' }; + }) + .then(function (params) { + params.translations = params.translations.replace(/\{[^\{\}]*\}/g, ''); + return params; + }); + })); + }); +} + +var cache = {}; + +function getDict(language, term) { + cache[language] = cache[language] || initDict(language); + return cache[language]; +} + +module.exports.getDict = getDict; diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index c3220ed094..9817099ef5 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').getDict; var SocketAdmin = { user: require('./admin/user'), @@ -277,5 +278,18 @@ 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) + .then(function (results) { + callback(null, results); + }, callback); + }); +}; + module.exports = SocketAdmin; 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..066f43cdd8 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]]

    - Select SkinSelect Theme + [[admin/appearance/themes:select-skin]][[admin/appearance/themes:select-theme]]
    From e3dd68e19cd06ddea7c7eb127c836d8462536bac Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Mon, 28 Nov 2016 12:53:47 -0700 Subject: [PATCH 02/15] Explanations and simplifications --- public/src/admin/modules/search.js | 7 ++++- src/admin/search.js | 43 ++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js index 0a5d3dde56..c7f660149a 100644 --- a/public/src/admin/modules/search.js +++ b/public/src/admin/modules/search.js @@ -19,11 +19,16 @@ define(function () { 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...
    ' - ).replace(/(\n ?)+/g, '\n'); + ) + // collapse whitespace + .replace(/(?:\n ?)+/g, '\n'); return '
  • ').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/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 @@ From 8ca98625b9eb4d993fbdfb5d0b7c6c935a4f8727 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Mon, 5 Dec 2016 18:32:17 -0700 Subject: [PATCH 10/15] Key through search results --- public/less/admin/modules/search.less | 9 ++++++ public/src/admin/modules/search.js | 44 ++++++++++++++++++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/public/less/admin/modules/search.less b/public/less/admin/modules/search.less index 381b940646..d2286005bf 100644 --- a/public/less/admin/modules/search.less +++ b/public/less/admin/modules/search.less @@ -2,6 +2,15 @@ .dropdown-menu { max-height: 75vh; overflow-y: auto; + + > li > a { + &.focus { + &:extend(.dropdown-menu>li>a:focus); + } + &:focus { + outline: none; + } + } } .state-start-typing { diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js index 5eb2f336a2..d4ac16f1d8 100644 --- a/public/src/admin/modules/search.js +++ b/public/src/admin/modules/search.js @@ -1,7 +1,7 @@ "use strict"; /* globals socket, app, define, ajaxify, config */ -define(function () { +define(['mousetrap'], function (mousetrap) { var search = {}; function nsToTitle(namespace) { @@ -67,8 +67,11 @@ define(function () { }); $('#acp-search').parents('form').on('submit', function (ev) { - var firstResult = menu.find('li:first-child > a').attr('href'); - var href = firstResult ? firstResult : config.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(/^\//, '')); @@ -81,8 +84,41 @@ define(function () { return false; }); + 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 value = input.val().toLowerCase(); + + if (value === prevValue) { + return; + } + prevValue = value; + menu.children('.result').remove(); var len = value.length; @@ -106,7 +142,7 @@ define(function () { .find('strong') .html(value); } else { - menu.removeClass('state-no-results'); + menu.removeClass('state-no-results state-yes-results'); } }); } From e2ea3cb21e6635bb2435371ed8d41868946eb49e Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 6 Dec 2016 15:13:34 -0700 Subject: [PATCH 11/15] Fix linting error --- public/src/admin/modules/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/admin/modules/search.js b/public/src/admin/modules/search.js index d4ac16f1d8..4f46898b8e 100644 --- a/public/src/admin/modules/search.js +++ b/public/src/admin/modules/search.js @@ -1,7 +1,7 @@ "use strict"; /* globals socket, app, define, ajaxify, config */ -define(['mousetrap'], function (mousetrap) { +define('admin/modules/search', ['mousetrap'], function (mousetrap) { var search = {}; function nsToTitle(namespace) { From 74b9f1a0166ec91186515e53d7885095d9b13429 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 7 Dec 2016 11:57:19 +0300 Subject: [PATCH 12/15] closes #5262 --- src/groups.js | 38 ++++++++++++++++---------------------- src/groups/create.js | 4 ++++ test/groups.js | 7 +++++++ 3 files changed, 27 insertions(+), 22 deletions(-) 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); - - function done(err, groupNames) { - if (err) { - return callback(err); - } + 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]; + }); + } - if (set === 'groups:visible:name') { - groupNames = groupNames.map(function (name) { - return name.split(':')[1]; - }); + Groups.getGroupsAndMembers(groupNames, next); } - - 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/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 () { From 78b7d5896c0d93abc78ef32165003e04207e3b65 Mon Sep 17 00:00:00 2001 From: barisusakli Date: Wed, 7 Dec 2016 16:25:46 +0300 Subject: [PATCH 13/15] convert topic title to string --- src/topics/create.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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); From 56fb9419796270a94676929f3e7eb338822fd17c Mon Sep 17 00:00:00 2001 From: psychobunny Date: Wed, 7 Dec 2016 15:53:47 -0500 Subject: [PATCH 14/15] up'd tjs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c0114b1978..a897b57966 100644 --- a/package.json +++ b/package.json @@ -87,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", From acf2e4078b991683118a1ef128cc9d4ee0fee1a2 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 8 Dec 2016 11:28:17 -0500 Subject: [PATCH 15/15] fixes #5263 --- src/middleware/render.js | 1 + 1 file changed, 1 insertion(+) 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(' ');