From 3350a89791be91a05a6fc373bcf4485a2e2baf3b Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 25 Apr 2017 15:27:12 -0600 Subject: [PATCH 001/263] Allow aborting notification push --- src/notifications.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/notifications.js b/src/notifications.js index 147ae7fea4..b7000dc124 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -191,6 +191,10 @@ var utils = require('./utils'); plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next); }, function (data, next) { + if (!data || !data.notification) { + return callback(); + } + uids = data.uids; notification = data.notification; From 42e21d5aa985f50e7d2749f93fc8cd7577494ef3 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 26 Apr 2017 10:45:40 -0600 Subject: [PATCH 002/263] Add more messaging hooks --- src/messaging.js | 19 ++++++++++++++++--- src/messaging/data.js | 12 ++++++++++++ src/messaging/notifications.js | 10 ++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/messaging.js b/src/messaging.js index aa16cd0b36..c7e13e72bb 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -172,6 +172,14 @@ Messaging.getRecentChats = function (callerUid, uid, start, stop, callback) { next(null, { rooms: results.roomData, nextStart: stop + 1 }); }, + function (ref, next) { + plugins.fireHook('filter:messaging.getRecentChats', { + rooms: ref.rooms, + nextStart: ref.nextStart, + uid: uid, + callerUid: callerUid, + }, next); + }, ], callback); }; @@ -252,11 +260,16 @@ Messaging.canMessageUser = function (uid, toUid, callback) { }, next); }, function (results, next) { - if (!results.settings.restrictChat || results.isAdmin || results.isFollowing) { - return next(); + if (results.settings.restrictChat && !results.isAdmin && !results.isFollowing) { + return next(new Error('[[error:chat-restricted]]')); } - next(new Error('[[error:chat-restricted]]')); + plugins.fireHook('filter:messaging.canMessageUser', { + uid: uid, + toUid: toUid, + }, function (err) { + next(err); + }); }, ], callback); }; diff --git a/src/messaging/data.js b/src/messaging/data.js index 3805b1c6f1..6a3a7b03a4 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -6,6 +6,7 @@ var S = require('string'); var db = require('../database'); var user = require('../user'); var utils = require('../utils'); +var plugins = require('../plugins'); module.exports = function (Messaging) { Messaging.getMessageField = function (mid, field, callback) { @@ -128,6 +129,17 @@ module.exports = function (Messaging) { next(null, []); } }, + function (messages, next) { + plugins.fireHook('filter:messaging.getMessagesData', { + messages: messages, + uid: uid, + roomId: roomId, + isNew: isNew, + mids: mids, + }, function (err, data) { + next(err, data && data.messages); + }); + }, ], callback); }; }; diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index eb7a1a1a74..564ea01fee 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -9,6 +9,7 @@ var emailer = require('../emailer'); var notifications = require('../notifications'); var meta = require('../meta'); var sockets = require('../socket.io'); +var plugins = require('../plugins'); module.exports = function (Messaging) { Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser @@ -27,6 +28,15 @@ module.exports = function (Messaging) { message: messageObj, }; + plugins.fireHook('filter:messaging.notifyUsersInRoom', data, next); + }, + function (data, next) { + if (!data || !data.uids || !data.uids.length) { + return next(); + } + + var uids = data.uids; + uids.forEach(function (uid) { data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0; Messaging.pushUnreadCount(uid); From 19d63862379fdeface4db0ed6c9ec4edf6d6f31c Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 26 Apr 2017 10:48:08 -0600 Subject: [PATCH 003/263] Also abort if no uids to push to --- src/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications.js b/src/notifications.js index b7000dc124..057a86e674 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -191,7 +191,7 @@ var utils = require('./utils'); plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next); }, function (data, next) { - if (!data || !data.notification) { + if (!data || !data.notification || !data.uids || !data.uids.length) { return callback(); } From 033ce02e7bada2158b004f3e2620d3ec957f3e00 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 26 Apr 2017 15:22:17 -0600 Subject: [PATCH 004/263] Rename hooks Add `static:messaging.canMessageRoom` as well --- src/messaging.js | 9 +++++++-- src/messaging/data.js | 2 +- src/messaging/notifications.js | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/messaging.js b/src/messaging.js index c7e13e72bb..8864368c11 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -264,7 +264,7 @@ Messaging.canMessageUser = function (uid, toUid, callback) { return next(new Error('[[error:chat-restricted]]')); } - plugins.fireHook('filter:messaging.canMessageUser', { + plugins.fireHook('static:messaging.canMessageUser', { uid: uid, toUid: toUid, }, function (err) { @@ -306,7 +306,12 @@ Messaging.canMessageRoom = function (uid, roomId, callback) { return next(new Error('[[error:email-not-confirmed-chat]]')); } - next(); + plugins.fireHook('static:messaging.canMessageRoom', { + uid: uid, + roomId: roomId, + }, function (err) { + next(err); + }); }, ], callback); }; diff --git a/src/messaging/data.js b/src/messaging/data.js index 6a3a7b03a4..b565294902 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -130,7 +130,7 @@ module.exports = function (Messaging) { } }, function (messages, next) { - plugins.fireHook('filter:messaging.getMessagesData', { + plugins.fireHook('filter:messaging.getMessages', { messages: messages, uid: uid, roomId: roomId, diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 564ea01fee..9e223a8158 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -28,7 +28,7 @@ module.exports = function (Messaging) { message: messageObj, }; - plugins.fireHook('filter:messaging.notifyUsersInRoom', data, next); + plugins.fireHook('filter:messaging.notify', data, next); }, function (data, next) { if (!data || !data.uids || !data.uids.length) { From 4b873242348ce980ca645693ec4669140639c689 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 26 Apr 2017 16:02:15 -0600 Subject: [PATCH 005/263] Quick fix --- src/messaging/notifications.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 9e223a8158..37318121c3 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -26,6 +26,7 @@ module.exports = function (Messaging) { roomId: roomId, fromUid: fromUid, message: messageObj, + uids: uids, }; plugins.fireHook('filter:messaging.notify', data, next); From a76a8b57cde607228c1f354a9b0e0b14d8e2a2b1 Mon Sep 17 00:00:00 2001 From: Yudu Ban Date: Mon, 8 May 2017 09:30:16 +0800 Subject: [PATCH 006/263] fix indent Use space instead of Tab --- test/controllers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/controllers.js b/test/controllers.js index e1568f6802..4eadfcb145 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -1,7 +1,7 @@ 'use strict'; var async = require('async'); -var assert = require('assert'); +var assert = require('assert'); var nconf = require('nconf'); var request = require('request'); From 972e9a0303e49d17076fcaa3a358a904297981f6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 8 May 2017 13:56:49 -0400 Subject: [PATCH 007/263] bump version for manual minor release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e668c42ec..1564dbb3a8 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "1.4.6", + "version": "1.5.0", "homepage": "http://www.nodebb.org", "repository": { "type": "git", From aa9b795b3b92e0ee75e4845253e336a6f78c2d05 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 8 May 2017 14:08:17 -0400 Subject: [PATCH 008/263] update theme versions for v1.5.0 release --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 1564dbb3a8..616ad63639 100644 --- a/package.json +++ b/package.json @@ -64,8 +64,8 @@ "nodebb-plugin-spam-be-gone": "0.4.13", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "4.0.0", - "nodebb-theme-persona": "4.2.10", - "nodebb-theme-vanilla": "5.2.1", + "nodebb-theme-persona": "5.0.0", + "nodebb-theme-vanilla": "6.0.0", "nodebb-widget-essentials": "3.0.0", "nodemailer": "2.6.4", "nodemailer-sendmail-transport": "1.0.0", From b58eb99571a4e5f8a2e9a40c9366db84a4c172d0 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 8 May 2017 14:26:48 -0400 Subject: [PATCH 009/263] re-adding public/uploads to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index dcda0224fc..469b990863 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ pidfile /public/templates /public/sounds +/public/uploads # compiled files /public/stylesheet.css From aca07a0f202dbb96b7045bb32824c8510b3df805 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 14:41:49 -0400 Subject: [PATCH 010/263] fix typo --- src/views/admin/development/info.tpl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/admin/development/info.tpl b/src/views/admin/development/info.tpl index 4a6d9b90b3..f594e7a499 100644 --- a/src/views/admin/development/info.tpl +++ b/src/views/admin/development/info.tpl @@ -5,8 +5,8 @@
- [[admin/development/info:nodes-responded, {nodeCount}, {timeOut}]] - + [[admin/development/info:nodes-responded, {nodeCount}, {timeout}]] + @@ -26,8 +26,8 @@ From 7ac031775a632e0ca89a6ceff20702b9650bb7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 14:55:29 -0400 Subject: [PATCH 011/263] locale test change --- test/locale-detect.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/locale-detect.js b/test/locale-detect.js index 84ea5af59c..1bace9fd75 100644 --- a/test/locale-detect.js +++ b/test/locale-detect.js @@ -4,10 +4,12 @@ var assert = require('assert'); var nconf = require('nconf'); var request = require('request'); +var db = require('./mocks/databasemock'); var meta = require('../src/meta'); describe('Language detection', function () { it('should detect the language for a guest', function (done) { + meta.config.autoDetectLang = 1; request(nconf.get('url') + '/api/config', { headers: { 'Accept-Language': 'de-DE,de;q=0.5', @@ -24,7 +26,6 @@ describe('Language detection', function () { it('should do nothing when disabled', function (done) { meta.configs.set('autoDetectLang', 0, function (err) { assert.ifError(err); - request(nconf.get('url') + '/api/config', { headers: { 'Accept-Language': 'de-DE,de;q=0.5', From d333e87b514262c62bf1890c228691663b50d99b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 15:37:36 -0400 Subject: [PATCH 012/263] update some deps --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 616ad63639..9633474e34 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, "dependencies": { - "async": "^2.1.4", + "async": "2.4.0", "ace-builds": "^1.2.6", "autoprefixer": "^6.2.3", "bcryptjs": "~2.3.0", @@ -39,8 +39,8 @@ "express": "^4.14.0", "express-session": "^1.8.2", "express-useragent": "1.0.7", - "html-to-text": "2.1.3", - "ip": "1.1.3", + "html-to-text": "3.2.0", + "ip": "1.1.5", "jimp": "0.2.27", "jquery": "^3.1.0", "json-2-csv": "^2.0.22", @@ -50,7 +50,7 @@ "mime": "^1.3.4", "minimist": "^1.1.1", "mkdirp": "~0.5.0", - "mongodb": "2.2.25", + "mongodb": "2.2.26", "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", @@ -84,10 +84,10 @@ "semver": "^5.1.0", "serve-favicon": "^2.1.5", "sitemap": "^1.4.0", - "socket.io": "1.7.2", - "socket.io-client": "1.7.2", - "socket.io-redis": "3.1.0", - "socketio-wildcard": "~0.3.0", + "socket.io": "1.7.4", + "socket.io-client": "1.7.4", + "socket.io-redis": "4.0.0", + "socketio-wildcard": "0.4.0", "string": "^3.0.0", "templates.js": "0.3.10", "toobusy-js": "^0.5.1", From 84ff8bb24af59fa744a3aec4317eb7b5b6006887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 16:03:32 -0400 Subject: [PATCH 013/263] up autoprefixer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9633474e34..06650cb598 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dependencies": { "async": "2.4.0", "ace-builds": "^1.2.6", - "autoprefixer": "^6.2.3", + "autoprefixer": "7.0.1", "bcryptjs": "~2.3.0", "body-parser": "^1.9.0", "bootstrap": "^3.3.7", From 1f3840414cdc681bbee14c07d98bf82c7a05c035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 16:12:21 -0400 Subject: [PATCH 014/263] up bcryptjs --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06650cb598..04fb86f80e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "async": "2.4.0", "ace-builds": "^1.2.6", "autoprefixer": "7.0.1", - "bcryptjs": "~2.3.0", + "bcryptjs": "2.4.3", "body-parser": "^1.9.0", "bootstrap": "^3.3.7", "chart.js": "^2.4.0", From ea5927047fdf065cd05ff63572f899b5fd76afa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 16:18:36 -0400 Subject: [PATCH 015/263] up connect-redis --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 04fb86f80e..bb20a5291b 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "connect-flash": "^0.1.1", "connect-mongo": "1.3.2", "connect-multiparty": "^2.0.0", - "connect-redis": "~3.1.0", + "connect-redis": "3.3.0", "cookie-parser": "^1.3.3", "cron": "^1.0.5", "cropperjs": "^0.8.1", From dfbb488a0fec86dd8f13f102931df2ff1a82c10f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 16:30:12 -0400 Subject: [PATCH 016/263] up postcss --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bb20a5291b..671102982e 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,8 @@ "nodemailer-smtp-transport": "^2.4.1", "passport": "^0.3.0", "passport-local": "1.0.0", - "postcss": "^5.0.13", - "postcss-clean": "^1.0.0", + "postcss": "6.0.1", + "postcss-clean": "1.0.2", "promise-polyfill": "^6.0.2", "prompt": "^1.0.0", "redis": "~2.6.2", From a43d6e69667b6a47d61c3bb0c595ccefe037356d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 16:50:40 -0400 Subject: [PATCH 017/263] up redis --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 671102982e..2b980a0567 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "postcss-clean": "1.0.2", "promise-polyfill": "^6.0.2", "prompt": "^1.0.0", - "redis": "~2.6.2", + "redis": "2.7.1", "request": "2.81.0", "rimraf": "~2.5.0", "rss": "^1.0.0", From 5822aaa4338be7fce1c9bec803f0ec7ed29c83d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 16:57:36 -0400 Subject: [PATCH 018/263] up rimraf --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2b980a0567..5ed6ce010a 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,7 @@ "prompt": "^1.0.0", "redis": "2.7.1", "request": "2.81.0", - "rimraf": "~2.5.0", + "rimraf": "2.6.1", "rss": "^1.0.0", "sanitize-html": "^1.13.0", "semver": "^5.1.0", From df7f357e535fe2dc2b65deba10b8920496ee832a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 17:36:59 -0400 Subject: [PATCH 019/263] up validator --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5ed6ce010a..4b5cdb2640 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "uglify-js": "^2.6.0", "underscore": "^1.8.3", "underscore.deep": "^0.5.1", - "validator": "^6.1.0", + "validator": "7.0.0", "winston": "^2.1.0", "xml": "^1.0.1", "xregexp": "~3.1.0", From d49da8c85d84e7e7d2e60908abe79a9450237e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 17:46:26 -0400 Subject: [PATCH 020/263] up xregexp --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4b5cdb2640..36b7d2032f 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "validator": "7.0.0", "winston": "^2.1.0", "xml": "^1.0.1", - "xregexp": "~3.1.0", + "xregexp": "3.2.0", "zxcvbn": "^4.4.2" }, "devDependencies": { From f3deef4931a30d49b753e84d6fa2cc5c705188d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 19:54:53 -0400 Subject: [PATCH 021/263] closes #5656 --- public/src/client/footer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/footer.js b/public/src/client/footer.js index dc10e84d1f..25444f0912 100644 --- a/public/src/client/footer.js +++ b/public/src/client/footer.js @@ -7,7 +7,7 @@ define('forum/footer', ['notifications', 'chat', 'components', 'translator'], fu translator.prepareDOM(); function updateUnreadTopicCount(url, count) { - $('#main-nav a[href="' + config.relative_path + url + '"] i') + $('a[href="' + config.relative_path + url + '"] i') .toggleClass('unread-count', count > 0) .attr('data-content', count > 99 ? '99+' : count); } From b18b0db5befc479cee067565d05cfea696feacc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 20:36:43 -0400 Subject: [PATCH 022/263] notifications.js style --- src/notifications.js | 817 +++++++++++++++++++++---------------------- src/start.js | 2 +- 2 files changed, 409 insertions(+), 410 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index 9034fc54c8..bb002238e6 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -15,500 +15,499 @@ var batch = require('./batch'); var plugins = require('./plugins'); var utils = require('./utils'); -(function (Notifications) { - Notifications.init = function () { - winston.verbose('[notifications.init] Registering jobs.'); - new cron('*/30 * * * *', Notifications.prune, null, true); - }; - - Notifications.get = function (nid, callback) { - Notifications.getMultiple([nid], function (err, notifications) { - callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null); - }); - }; +var Notifications = module.exports; + +Notifications.startJobs = function () { + winston.verbose('[notifications.init] Registering jobs.'); + new cron('*/30 * * * *', Notifications.prune, null, true); +}; + +Notifications.get = function (nid, callback) { + Notifications.getMultiple([nid], function (err, notifications) { + callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null); + }); +}; + +Notifications.getMultiple = function (nids, callback) { + if (!nids.length) { + return setImmediate(callback, null, []); + } + var keys = nids.map(function (nid) { + return 'notifications:' + nid; + }); + + var notifications; + + async.waterfall([ + function (next) { + db.getObjects(keys, next); + }, + function (_notifications, next) { + notifications = _notifications; + var userKeys = notifications.map(function (notification) { + return notification && notification.from; + }); - Notifications.getMultiple = function (nids, callback) { - if (!nids.length) { - return setImmediate(callback, null, []); - } - var keys = nids.map(function (nid) { - return 'notifications:' + nid; - }); + User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], next); + }, + function (usersData, next) { + notifications.forEach(function (notification, index) { + if (notification) { + notification.datetimeISO = utils.toISOString(notification.datetime); - var notifications; + if (notification.bodyLong) { + notification.bodyLong = S(notification.bodyLong).escapeHTML().s; + } - async.waterfall([ - function (next) { - db.getObjects(keys, next); - }, - function (_notifications, next) { - notifications = _notifications; - var userKeys = notifications.map(function (notification) { - return notification && notification.from; - }); + notification.user = usersData[index]; + if (notification.user) { + notification.image = notification.user.picture || null; + if (notification.user.username === '[[global:guest]]') { + notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2'); + } + } else if (notification.image === 'brand:logo' || !notification.image) { + notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; + } + } + }); - User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], next); - }, - function (usersData, next) { - notifications.forEach(function (notification, index) { - if (notification) { - notification.datetimeISO = utils.toISOString(notification.datetime); + next(null, notifications); + }, + ], callback); +}; + +Notifications.filterExists = function (nids, callback) { + async.waterfall([ + function (next) { + db.isSortedSetMembers('notifications', nids, next); + }, + function (exists, next) { + nids = nids.filter(function (notifId, idx) { + return exists[idx]; + }); - if (notification.bodyLong) { - notification.bodyLong = S(notification.bodyLong).escapeHTML().s; - } + next(null, nids); + }, + ], callback); +}; - notification.user = usersData[index]; - if (notification.user) { - notification.image = notification.user.picture || null; - if (notification.user.username === '[[global:guest]]') { - notification.bodyShort = notification.bodyShort.replace(/([\s\S]*?),[\s\S]*?,([\s\S]*?)/, '$1, [[global:guest]], $2'); - } - } else if (notification.image === 'brand:logo' || !notification.image) { - notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png'; - } - } - }); +Notifications.findRelated = function (mergeIds, set, callback) { + // A related notification is one in a zset that has the same mergeId + var _nids; - next(null, notifications); - }, - ], callback); - }; + async.waterfall([ + async.apply(db.getSortedSetRevRange, set, 0, -1), + function (nids, next) { + _nids = nids; - Notifications.filterExists = function (nids, callback) { - async.waterfall([ - function (next) { - db.isSortedSetMembers('notifications', nids, next); - }, - function (exists, next) { - nids = nids.filter(function (notifId, idx) { - return exists[idx]; - }); + var keys = nids.map(function (nid) { + return 'notifications:' + nid; + }); - next(null, nids); - }, - ], callback); - }; + db.getObjectsFields(keys, ['mergeId'], next); + }, + ], function (err, sets) { + if (err) { + return callback(err); + } - Notifications.findRelated = function (mergeIds, set, callback) { - // A related notification is one in a zset that has the same mergeId - var _nids; + sets = sets.map(function (set) { + return set.mergeId; + }); - async.waterfall([ - async.apply(db.getSortedSetRevRange, set, 0, -1), - function (nids, next) { - _nids = nids; + callback(null, _nids.filter(function (nid, idx) { + return mergeIds.indexOf(sets[idx]) !== -1; + })); + }); +}; - var keys = nids.map(function (nid) { - return 'notifications:' + nid; - }); +Notifications.create = function (data, callback) { + if (!data.nid) { + return callback(new Error('no-notification-id')); + } + data.importance = data.importance || 5; + db.getObject('notifications:' + data.nid, function (err, oldNotification) { + if (err) { + return callback(err); + } - db.getObjectsFields(keys, ['mergeId'], next); - }, - ], function (err, sets) { - if (err) { - return callback(err); + if (oldNotification) { + if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) { + return callback(null, null); } + } - sets = sets.map(function (set) { - return set.mergeId; - }); - - callback(null, _nids.filter(function (nid, idx) { - return mergeIds.indexOf(sets[idx]) !== -1; - })); + var now = Date.now(); + data.datetime = now; + async.parallel([ + function (next) { + db.sortedSetAdd('notifications', now, data.nid, next); + }, + function (next) { + db.setObject('notifications:' + data.nid, data, next); + }, + ], function (err) { + callback(err, data); }); - }; + }); +}; - Notifications.create = function (data, callback) { - if (!data.nid) { - return callback(new Error('no-notification-id')); - } - data.importance = data.importance || 5; - db.getObject('notifications:' + data.nid, function (err, oldNotification) { - if (err) { - return callback(err); - } +Notifications.push = function (notification, uids, callback) { + callback = callback || function () {}; - if (oldNotification) { - if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) { - return callback(null, null); - } - } + if (!notification || !notification.nid) { + return callback(); + } - var now = Date.now(); - data.datetime = now; - async.parallel([ - function (next) { - db.sortedSetAdd('notifications', now, data.nid, next); - }, - function (next) { - db.setObject('notifications:' + data.nid, data, next); - }, - ], function (err) { - callback(err, data); - }); + if (!Array.isArray(uids)) { + uids = [uids]; + } + + uids = uids.filter(function (uid, index, array) { + return parseInt(uid, 10) && array.indexOf(uid) === index; + }); + + if (!uids.length) { + return callback(); + } + + setTimeout(function () { + batch.processArray(uids, function (uids, next) { + pushToUids(uids, notification, next); + }, { interval: 1000 }, function (err) { + if (err) { + winston.error(err.stack); + } }); - }; + }, 1000); + + callback(); +}; + +function pushToUids(uids, notification, callback) { + var oneWeekAgo = Date.now() - 604800000; + var unreadKeys = []; + var readKeys = []; + + async.waterfall([ + function (next) { + plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next); + }, + function (data, next) { + uids = data.uids; + notification = data.notification; + + uids.forEach(function (uid) { + unreadKeys.push('uid:' + uid + ':notifications:unread'); + readKeys.push('uid:' + uid + ':notifications:read'); + }); - Notifications.push = function (notification, uids, callback) { - callback = callback || function () {}; + db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next); + }, + function (next) { + db.sortedSetsRemove(readKeys, notification.nid, next); + }, + function (next) { + db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next); + }, + function (next) { + db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next); + }, + function (next) { + var websockets = require('./socket.io'); + if (websockets.server) { + uids.forEach(function (uid) { + websockets.in('uid_' + uid).emit('event:new_notification', notification); + }); + } - if (!notification || !notification.nid) { - return callback(); + plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids }); + next(); + }, + ], callback); +} + +Notifications.pushGroup = function (notification, groupName, callback) { + callback = callback || function () {}; + groups.getMembers(groupName, 0, -1, function (err, members) { + if (err || !Array.isArray(members) || !members.length) { + return callback(err); } - if (!Array.isArray(uids)) { - uids = [uids]; + Notifications.push(notification, members, callback); + }); +}; + +Notifications.pushGroups = function (notification, groupNames, callback) { + callback = callback || function () {}; + groups.getMembersOfGroups(groupNames, function (err, groupMembers) { + if (err) { + return callback(err); } - uids = uids.filter(function (uid, index, array) { - return parseInt(uid, 10) && array.indexOf(uid) === index; - }); + var members = _.unique(_.flatten(groupMembers)); - if (!uids.length) { - return callback(); - } + Notifications.push(notification, members, callback); + }); +}; - setTimeout(function () { - batch.processArray(uids, function (uids, next) { - pushToUids(uids, notification, next); - }, { interval: 1000 }, function (err) { - if (err) { - winston.error(err.stack); - } - }); - }, 1000); +Notifications.rescind = function (nid, callback) { + callback = callback || function () {}; - callback(); - }; + async.parallel([ + async.apply(db.sortedSetRemove, 'notifications', nid), + async.apply(db.delete, 'notifications:' + nid), + ], function (err) { + if (err) { + winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message); + } else { + winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"'); + } - function pushToUids(uids, notification, callback) { - var oneWeekAgo = Date.now() - 604800000; - var unreadKeys = []; - var readKeys = []; + callback(err, nid); + }); +}; - async.waterfall([ - function (next) { - plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next); - }, - function (data, next) { - uids = data.uids; - notification = data.notification; +Notifications.markRead = function (nid, uid, callback) { + callback = callback || function () {}; + if (!parseInt(uid, 10) || !nid) { + return callback(); + } + Notifications.markReadMultiple([nid], uid, callback); +}; - uids.forEach(function (uid) { - unreadKeys.push('uid:' + uid + ':notifications:unread'); - readKeys.push('uid:' + uid + ':notifications:read'); - }); +Notifications.markUnread = function (nid, uid, callback) { + callback = callback || function () {}; + if (!parseInt(uid, 10) || !nid) { + return callback(); + } - db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next); - }, - function (next) { - db.sortedSetsRemove(readKeys, notification.nid, next); - }, - function (next) { - db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next); - }, - function (next) { - db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next); - }, - function (next) { - var websockets = require('./socket.io'); - if (websockets.server) { - uids.forEach(function (uid) { - websockets.in('uid_' + uid).emit('event:new_notification', notification); - }); - } + db.getObject('notifications:' + nid, function (err, notification) { + if (err || !notification) { + return callback(err || new Error('[[error:no-notification]]')); + } + notification.datetime = notification.datetime || Date.now(); - plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids }); - next(); - }, + async.parallel([ + async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid), + async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid), ], callback); + }); +}; + +Notifications.markReadMultiple = function (nids, uid, callback) { + callback = callback || function () {}; + nids = nids.filter(Boolean); + if (!Array.isArray(nids) || !nids.length) { + return callback(); } - Notifications.pushGroup = function (notification, groupName, callback) { - callback = callback || function () {}; - groups.getMembers(groupName, 0, -1, function (err, members) { - if (err || !Array.isArray(members) || !members.length) { - return callback(err); - } + var notificationKeys = nids.map(function (nid) { + return 'notifications:' + nid; + }); - Notifications.push(notification, members, callback); - }); - }; + async.waterfall([ + async.apply(db.getObjectsFields, notificationKeys, ['mergeId']), + function (mergeIds, next) { + // Isolate mergeIds and find related notifications + mergeIds = mergeIds.map(function (set) { + return set.mergeId; + }).reduce(function (memo, mergeId, idx, arr) { + if (mergeId && idx === arr.indexOf(mergeId)) { + memo.push(mergeId); + } + return memo; + }, []); - Notifications.pushGroups = function (notification, groupNames, callback) { - callback = callback || function () {}; - groups.getMembersOfGroups(groupNames, function (err, groupMembers) { - if (err) { - return callback(err); - } + Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next); + }, + function (relatedNids, next) { + notificationKeys = _.union(nids, relatedNids).map(function (nid) { + return 'notifications:' + nid; + }); + + db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next); + }, + ], function (err, notificationData) { + if (err) { + return callback(err); + } - var members = _.unique(_.flatten(groupMembers)); + // Filter out notifications that didn't exist + notificationData = notificationData.filter(function (notification) { + return notification && notification.nid; + }); - Notifications.push(notification, members, callback); + // Extract nid + nids = notificationData.map(function (notification) { + return notification.nid; }); - }; - Notifications.rescind = function (nid, callback) { - callback = callback || function () {}; + var datetimes = notificationData.map(function (notification) { + return (notification && notification.datetime) || Date.now(); + }); async.parallel([ - async.apply(db.sortedSetRemove, 'notifications', nid), - async.apply(db.delete, 'notifications:' + nid), + function (next) { + db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); + }, + function (next) { + db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next); + }, ], function (err) { - if (err) { - winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message); - } else { - winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"'); - } - - callback(err, nid); + callback(err); }); - }; + }); +}; - Notifications.markRead = function (nid, uid, callback) { - callback = callback || function () {}; - if (!parseInt(uid, 10) || !nid) { - return callback(); +Notifications.markAllRead = function (uid, callback) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) { + if (err) { + return callback(err); } - Notifications.markReadMultiple([nid], uid, callback); - }; - Notifications.markUnread = function (nid, uid, callback) { - callback = callback || function () {}; - if (!parseInt(uid, 10) || !nid) { + if (!Array.isArray(nids) || !nids.length) { return callback(); } - db.getObject('notifications:' + nid, function (err, notification) { - if (err || !notification) { - return callback(err || new Error('[[error:no-notification]]')); - } - notification.datetime = notification.datetime || Date.now(); + Notifications.markReadMultiple(nids, uid, callback); + }); +}; - async.parallel([ - async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid), - async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid), - ], callback); - }); - }; +Notifications.prune = function () { + var week = 604800000; + + var cutoffTime = Date.now() - week; + + db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) { + if (err) { + return winston.error(err.message); + } - Notifications.markReadMultiple = function (nids, uid, callback) { - callback = callback || function () {}; - nids = nids.filter(Boolean); if (!Array.isArray(nids) || !nids.length) { - return callback(); + return; } - var notificationKeys = nids.map(function (nid) { + var keys = nids.map(function (nid) { return 'notifications:' + nid; }); - async.waterfall([ - async.apply(db.getObjectsFields, notificationKeys, ['mergeId']), - function (mergeIds, next) { - // Isolate mergeIds and find related notifications - mergeIds = mergeIds.map(function (set) { - return set.mergeId; - }).reduce(function (memo, mergeId, idx, arr) { - if (mergeId && idx === arr.indexOf(mergeId)) { - memo.push(mergeId); - } - return memo; - }, []); - - Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next); + async.parallel([ + function (next) { + db.sortedSetRemove('notifications', nids, next); }, - function (relatedNids, next) { - notificationKeys = _.union(nids, relatedNids).map(function (nid) { - return 'notifications:' + nid; - }); - - db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next); + function (next) { + db.deleteAll(keys, next); }, - ], function (err, notificationData) { + ], function (err) { if (err) { - return callback(err); + return winston.error('Encountered error pruning notifications: ' + err.message); } - - // Filter out notifications that didn't exist - notificationData = notificationData.filter(function (notification) { - return notification && notification.nid; - }); - - // Extract nid - nids = notificationData.map(function (notification) { - return notification.nid; - }); - - var datetimes = notificationData.map(function (notification) { - return (notification && notification.datetime) || Date.now(); - }); - - async.parallel([ - function (next) { - db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); - }, - function (next) { - db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next); - }, - ], function (err) { - callback(err); - }); }); - }; - - Notifications.markAllRead = function (uid, callback) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) { - if (err) { - return callback(err); + }); +}; + +Notifications.merge = function (notifications, callback) { + // When passed a set of notification objects, merge any that can be merged + var mergeIds = [ + 'notifications:upvoted_your_post_in', + 'notifications:user_started_following_you', + 'notifications:user_posted_to', + 'notifications:user_flagged_post_in', + 'notifications:user_flagged_user', + 'new_register', + ]; + var isolated; + var differentiators; + var differentiator; + var modifyIndex; + var set; + + notifications = mergeIds.reduce(function (notifications, mergeId) { + isolated = notifications.filter(function (notifObj) { + if (!notifObj || !notifObj.hasOwnProperty('mergeId')) { + return false; } - if (!Array.isArray(nids) || !nids.length) { - return callback(); - } - - Notifications.markReadMultiple(nids, uid, callback); + return notifObj.mergeId.split('|')[0] === mergeId; }); - }; - - Notifications.prune = function () { - var week = 604800000; - var cutoffTime = Date.now() - week; - - db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) { - if (err) { - return winston.error(err.message); - } + if (isolated.length <= 1) { + return notifications; // Nothing to merge + } - if (!Array.isArray(nids) || !nids.length) { - return; + // Each isolated mergeId may have multiple differentiators, so process each separately + differentiators = isolated.reduce(function (cur, next) { + differentiator = next.mergeId.split('|')[1] || 0; + if (cur.indexOf(differentiator) === -1) { + cur.push(differentiator); } - var keys = nids.map(function (nid) { - return 'notifications:' + nid; - }); - - async.parallel([ - function (next) { - db.sortedSetRemove('notifications', nids, next); - }, - function (next) { - db.deleteAll(keys, next); - }, - ], function (err) { - if (err) { - return winston.error('Encountered error pruning notifications: ' + err.message); - } - }); - }); - }; - - Notifications.merge = function (notifications, callback) { - // When passed a set of notification objects, merge any that can be merged - var mergeIds = [ - 'notifications:upvoted_your_post_in', - 'notifications:user_started_following_you', - 'notifications:user_posted_to', - 'notifications:user_flagged_post_in', - 'notifications:user_flagged_user', - 'new_register', - ]; - var isolated; - var differentiators; - var differentiator; - var modifyIndex; - var set; - - notifications = mergeIds.reduce(function (notifications, mergeId) { - isolated = notifications.filter(function (notifObj) { - if (!notifObj || !notifObj.hasOwnProperty('mergeId')) { - return false; - } - - return notifObj.mergeId.split('|')[0] === mergeId; - }); + return cur; + }, []); - if (isolated.length <= 1) { - return notifications; // Nothing to merge + differentiators.forEach(function (differentiator) { + if (differentiator === 0 && differentiators.length === 1) { + set = isolated; + } else { + set = isolated.filter(function (notifObj) { + return notifObj.mergeId === (mergeId + '|' + differentiator); + }); } - // Each isolated mergeId may have multiple differentiators, so process each separately - differentiators = isolated.reduce(function (cur, next) { - differentiator = next.mergeId.split('|')[1] || 0; - if (cur.indexOf(differentiator) === -1) { - cur.push(differentiator); - } + modifyIndex = notifications.indexOf(set[0]); + if (modifyIndex === -1 || set.length === 1) { + return notifications; + } - return cur; - }, []); + switch (mergeId) { + // intentional fall-through + case 'notifications:upvoted_your_post_in': + case 'notifications:user_started_following_you': + case 'notifications:user_posted_to': + case 'notifications:user_flagged_post_in': + case 'notifications:user_flagged_user': + var usernames = set.map(function (notifObj) { + return notifObj && notifObj.user && notifObj.user.username; + }).filter(function (username, idx, array) { + return array.indexOf(username) === idx; + }); + var numUsers = usernames.length; - differentiators.forEach(function (differentiator) { - if (differentiator === 0 && differentiators.length === 1) { - set = isolated; - } else { - set = isolated.filter(function (notifObj) { - return notifObj.mergeId === (mergeId + '|' + differentiator); - }); - } + var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s; + var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + titleEscaped = titleEscaped ? (', ' + titleEscaped) : ''; - modifyIndex = notifications.indexOf(set[0]); - if (modifyIndex === -1 || set.length === 1) { - return notifications; + if (numUsers === 2) { + notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]'; + } else if (numUsers > 2) { + notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]'; } - switch (mergeId) { - // intentional fall-through - case 'notifications:upvoted_your_post_in': - case 'notifications:user_started_following_you': - case 'notifications:user_posted_to': - case 'notifications:user_flagged_post_in': - case 'notifications:user_flagged_user': - var usernames = set.map(function (notifObj) { - return notifObj && notifObj.user && notifObj.user.username; - }).filter(function (username, idx, array) { - return array.indexOf(username) === idx; - }); - var numUsers = usernames.length; - - var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s; - var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); - titleEscaped = titleEscaped ? (', ' + titleEscaped) : ''; - - if (numUsers === 2) { - notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]'; - } else if (numUsers > 2) { - notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]'; - } + notifications[modifyIndex].path = set[set.length - 1].path; + break; - notifications[modifyIndex].path = set[set.length - 1].path; - break; + case 'new_register': + notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]'; + break; + } - case 'new_register': - notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]'; - break; + // Filter out duplicates + notifications = notifications.filter(function (notifObj, idx) { + if (!notifObj || !notifObj.mergeId) { + return true; } - // Filter out duplicates - notifications = notifications.filter(function (notifObj, idx) { - if (!notifObj || !notifObj.mergeId) { - return true; - } - - return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex); - }); + return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex); }); - - return notifications; - }, notifications); - - plugins.fireHook('filter:notifications.merge', { - notifications: notifications, - }, function (err, data) { - callback(err, data.notifications); }); - }; -}(exports)); + return notifications; + }, notifications); + + plugins.fireHook('filter:notifications.merge', { + notifications: notifications, + }, function (err, data) { + callback(err, data.notifications); + }); +}; diff --git a/src/start.js b/src/start.js index 2a6e53f507..8d4e465a55 100644 --- a/src/start.js +++ b/src/start.js @@ -48,7 +48,7 @@ start.start = function () { require('./socket.io').init(webserver.server); if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) { - require('./notifications').init(); + require('./notifications').startJobs(); require('./user').startJobs(); } From 6fcd12eafe7cf5839d74835afb165720df05c027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 20:55:47 -0400 Subject: [PATCH 023/263] fix tests --- test/mocks/databasemock.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 412a255dc6..4432f944f1 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -146,7 +146,7 @@ var sockets = require('../../src/socket.io'); sockets.init(webserver.server); - require('../../src/notifications').init(); + require('../../src/notifications').startJobs(); require('../../src/user').startJobs(); webserver.listen(next); From db532fbb0d8cd237cec461f6498c68a34032a46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 21:23:30 -0400 Subject: [PATCH 024/263] fix user settings cache --- src/user/settings.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/user/settings.js b/src/user/settings.js index 9aa247bd79..de0da0641d 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -2,6 +2,7 @@ 'use strict'; var async = require('async'); +var _ = require('underscore'); var meta = require('../meta'); var db = require('../database'); @@ -25,7 +26,7 @@ module.exports = function (User) { var cached = cache.get('user:' + uid + ':settings'); if (cached) { - return onSettingsLoaded(uid, cached || {}, callback); + return onSettingsLoaded(uid, _.clone(cached || {}), callback); } async.waterfall([ @@ -36,7 +37,7 @@ module.exports = function (User) { settings = settings || {}; settings.uid = uid; cache.set('user:' + uid + ':settings', settings); - onSettingsLoaded(uid, settings || {}, next); + onSettingsLoaded(uid, _.clone(settings || {}), next); }, ], callback); }; @@ -47,7 +48,7 @@ module.exports = function (User) { return cache.get('user:' + uid + ':settings') || {}; }); async.map(settings, function (setting, next) { - onSettingsLoaded(setting.uid, setting, next); + onSettingsLoaded(setting.uid, _.clone(setting), next); }, next); } From 36a2ac0ebde280caacee761a6871b30877bab4f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 8 May 2017 21:28:06 -0400 Subject: [PATCH 025/263] add test for user settings cache issue --- test/user.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/user.js b/test/user.js index 662b82f326..943c348872 100644 --- a/test/user.js +++ b/test/user.js @@ -945,7 +945,11 @@ describe('User', function () { }; socketUser.saveSettings({ uid: testUid }, data, function (err) { assert.ifError(err); - done(); + User.getSettings(testUid, function (err, data) { + assert.ifError(err); + assert.equal(data.usePagination, true); + done(); + }); }); }); From cb339838c03666a45c8bf0a4de68c60785d920f6 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Tue, 9 May 2017 09:23:07 +0000 Subject: [PATCH 026/263] Latest translations and fallbacks --- public/language/cs/flags.json | 2 +- public/language/de/admin/manage/groups.json | 2 +- .../language/de/admin/settings/advanced.json | 2 +- public/language/de/email.json | 4 +- public/language/de/error.json | 4 +- public/language/de/flags.json | 100 ++++++++--------- public/language/de/modules.json | 2 +- public/language/de/user.json | 2 +- public/language/es/flags.json | 98 ++++++++--------- public/language/fr/flags.json | 98 ++++++++--------- public/language/gl/flags.json | 100 ++++++++--------- public/language/ja/flags.json | 102 +++++++++--------- public/language/pt-BR/flags.json | 98 ++++++++--------- public/language/pt-PT/flags.json | 72 ++++++------- public/language/ru/flags.json | 38 +++---- public/language/sk/flags.json | 4 +- public/language/sr/flags.json | 102 +++++++++--------- public/language/tr/flags.json | 102 +++++++++--------- public/language/zh-CN/flags.json | 102 +++++++++--------- public/language/zh-TW/flags.json | 4 +- 20 files changed, 519 insertions(+), 519 deletions(-) diff --git a/public/language/cs/flags.json b/public/language/cs/flags.json index c4bfed6f23..aea0e868e4 100644 --- a/public/language/cs/flags.json +++ b/public/language/cs/flags.json @@ -54,7 +54,7 @@ "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", "modal-reason-spam": "Spam", "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", + "modal-reason-custom": "Důvod nahlášení tohoto obsahu…", "modal-submit": "Submit Report", "modal-submit-success": "Content has been flagged for moderation." } \ No newline at end of file diff --git a/public/language/de/admin/manage/groups.json b/public/language/de/admin/manage/groups.json index f6df5ca99e..fa7244a3e5 100644 --- a/public/language/de/admin/manage/groups.json +++ b/public/language/de/admin/manage/groups.json @@ -1,7 +1,7 @@ { "name": "Gruppenname", "description": "Gruppenbeschreibung", - "member-count": "Member Count", + "member-count": "Mitglieder Anzahl", "system": "System-Gruppe", "edit": "Ändern", "search-placeholder": "Suchen", diff --git a/public/language/de/admin/settings/advanced.json b/public/language/de/admin/settings/advanced.json index cca15102cc..87c5b2ca21 100644 --- a/public/language/de/admin/settings/advanced.json +++ b/public/language/de/admin/settings/advanced.json @@ -6,7 +6,7 @@ "headers.allow-from": "ALLOW-FROM setzen um NodeBB in einem iFrame zu platzieren", "headers.powered-by": "Anpassen des \"Powered By\" Headers von NodeBB", "headers.acao": "Access-Control-Allow-Origin", - "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-help": "Um den Zugriff zu allen Seiten zu verbieten, leer lassen.", "headers.acam": "Access-Control-Allow-Methods", "headers.acah": "Access-Control-Allow-Headers", "traffic-management": "Traffic Management", diff --git a/public/language/de/email.json b/public/language/de/email.json index 490001d405..1ade727750 100644 --- a/public/language/de/email.json +++ b/public/language/de/email.json @@ -32,8 +32,8 @@ "notif.post.unsub.info": "Diese Mitteilung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.", "test.text1": "Dies ist eine Test-E-Mail, um zu überprüfen, ob der E-Mailer deines NodeBB korrekt eingestellt wurde.", "unsub.cta": "Klicke hier, um diese Einstellungen zu ändern.", - "banned.subject": "You have been banned from %1", - "banned.text1": "The user %1 has been banned from %2.", + "banned.subject": "Du wurdest von %1 gebannt.", + "banned.text1": "Der Benutzer %1 wurde von %2 gebannt.", "banned.text2": "This ban will last until %1.", "banned.text3": "This is the reason why you have been banned:", "closing": "Danke!" diff --git a/public/language/de/error.json b/public/language/de/error.json index ea969f8284..51526a9041 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -30,7 +30,7 @@ "password-too-long": "Passwort ist zu lang", "user-banned": "Benutzer ist gesperrt", "user-banned-reason": "Entschuldige, dieses Konto wurde gebannt (Grund: %1)", - "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-banned-reason-until": "Entschuldigung, dieser Account wurde bis %1 (Reason: %2) gebannt.", "user-too-new": "Entschuldigung, du musst %1 Sekunde(n) warten, bevor du deinen ersten Beitrag schreiben kannst.", "blacklisted-ip": "Deine IP-Adresse ist für diese Plattform gesperrt. Sollte dies ein Irrtum sein, dann kontaktiere bitte einen Administrator.", "ban-expiry-missing": "Bitte gebe ein Enddatum für diesen Ban an", @@ -105,7 +105,7 @@ "chat-disabled": "Das Chatsystem deaktiviert", "too-many-messages": "Du hast zu viele Nachrichten versandt, bitte warte eine Weile.", "invalid-chat-message": "Ungültige Nachricht", - "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "chat-message-too-long": "Chat Nachricht darf nicht länger als %1 Zeichen sein.", "cant-edit-chat-message": "Du darfst diese Nachricht nicht ändern", "cant-remove-last-user": "Du kannst den letzten Benutzer nicht entfernen", "cant-delete-chat-message": "Du darfst diese Nachricht nicht löschen", diff --git a/public/language/de/flags.json b/public/language/de/flags.json index c4bfed6f23..974ba725ff 100644 --- a/public/language/de/flags.json +++ b/public/language/de/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "Zustand", + "reporter": "Meldender", + "reported-at": "Gemeldet am", + "description": "Beschreibung", + "no-flags": "Hurra! Keine Meldungen gefunden.", + "assignee": "Zugeordneter Benutzer", + "update": "Aktualisieren", + "updated": "Aktualisiert", + "target-purged": "Der Inhalt auf den diese Meldung hingewiesen hat, wurde gelöscht und ist nicht mehr verfügbar.", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "quick-filters": "Schnell-Filter", + "filter-active": "Ein oder mehrere Filter sind in dieser Meldungs-Liste aktiv", + "filter-reset": "Filter Entfernen", + "filters": "Filter Optionen", + "filter-reporterId": "Melder UID", + "filter-targetUid": "Gemeldete UID", + "filter-type": "Meldungstyp", + "filter-type-all": "Gesamter Inhalt", + "filter-type-post": "Beitrag", + "filter-state": "Status", + "filter-assignee": "UID des Zugewiesenen", + "filter-cid": "Kategorie", + "filter-quick-mine": "Mir zugewiesen", + "filter-cid-all": "Alle Kategorien", + "apply-filters": "Filter anwenden", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Schnellnavigation", + "flagged-user": "Gemeldeter Benutzer", + "view-profile": "Profil ansehen", + "start-new-chat": "Neuen Chat beginnen", + "go-to-target": "Meldungsziel ansehen", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Profil ansehen", + "user-edit": "Profil bearbeiten", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Meldungsnotizen", + "add-note": "Notiz hinzufügen", + "no-notes": "Keine geteilten Notizen", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Meldungsverlauf", + "back": "Zurück zur Meldungsliste", + "no-history": "Kein Meldungsverlauf", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Alle Status", + "state-open": "Neu/Öffnen", + "state-wip": "In Arbeit", + "state-resolved": "Gelöst", + "state-rejected": "Abgelehnt", + "no-assignee": "Nicht zugewiesen", + "note-added": "Notiz hinzugefügt", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-title": "Anstößige Inhalte Melden", + "modal-body": "Bitte geben Sie den Grund an, weshalb Sie %1 %2 melden wollen. Alternativ können Sie einen der Schnell-Meldungs-Knöpfe verwenden, wenn anwendbar.", "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-reason-offensive": "Beleidigend", + "modal-reason-custom": "Grund für die Meldung dieses Inhalts...", + "modal-submit": "Meldung abschicken", + "modal-submit-success": "Der Inhalt wurde gemeldet." } \ No newline at end of file diff --git a/public/language/de/modules.json b/public/language/de/modules.json index a1cf19cea0..bb53b0fb87 100644 --- a/public/language/de/modules.json +++ b/public/language/de/modules.json @@ -20,7 +20,7 @@ "chat.three_months": "3 Monate", "chat.delete_message_confirm": "Bist du sicher, dass du diese Nachricht löschen möchtest?", "chat.add-users-to-room": "Benutzer zum Raum hinzufügen", - "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.confirm-chat-with-dnd-user": "Dieser Benutzer hat seinen Status auf DnD(Bitte nicht stören) gesetzt. Möchtest du noch immer mit ihm chatten?", "composer.compose": "Verfassen", "composer.show_preview": "Vorschau zeigen", "composer.hide_preview": "Vorschau ausblenden", diff --git a/public/language/de/user.json b/public/language/de/user.json index afcbb48c96..22dd586842 100644 --- a/public/language/de/user.json +++ b/public/language/de/user.json @@ -60,7 +60,7 @@ "username_taken_workaround": "Der gewünschte Benutzername ist bereits vergeben, deshalb haben wir ihn ein wenig verändert. Du bist jetzt unter dem Namen %1 bekannt.", "password_same_as_username": "Dein Passwort entspricht deinem Benutzernamen, bitte wähle ein anderes Passwort.", "password_same_as_email": "Dein Passwort entspricht deiner E-Mail-Adresse, bitte wähle ein anderes Passwort.", - "weak_password": "Weak password.", + "weak_password": "Schwaches Password.", "upload_picture": "Bild hochladen", "upload_a_picture": "Ein Bild hochladen", "remove_uploaded_picture": "Hochgeladenes Bild entfernen", diff --git a/public/language/es/flags.json b/public/language/es/flags.json index c4bfed6f23..012d3181b0 100644 --- a/public/language/es/flags.json +++ b/public/language/es/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "Estado", + "reporter": "Reportador", + "reported-at": "Reportado en", + "description": "Descripción", + "no-flags": "Yeah! No se encontraron indicadores", + "assignee": "Asignado", + "update": "Actualizar", + "updated": "Actualizado", + "target-purged": "El contenido al que se refiere este indicador ha sido purgado y ya no está disponible.", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", + "quick-filters": "Filtros rapidos", + "filter-active": "Hay uno o más filtros activos en esta lista de indicadores.", + "filter-reset": "Quitar filtros", + "filters": "Opciones de filtros", + "filter-reporterId": "UID del reportador", + "filter-targetUid": "Indicador UID", + "filter-type": "Tipo de indicador", + "filter-type-all": "Todo el contenido", "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "filter-state": "estado", + "filter-assignee": "UID asignado", + "filter-cid": "Categoria", + "filter-quick-mine": "Asignado a mí", + "filter-cid-all": "Todas las categorias", + "apply-filters": "Aplicar filtros", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Links rapidos", + "flagged-user": "Usuario marcado", + "view-profile": "Ver perfil", + "start-new-chat": "Empezar nuevo chat", + "go-to-target": "Ver objetivo marcado", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Ver perfil", + "user-edit": "Editar perfil", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Marcar notas", + "add-note": "Añadir nota", + "no-notes": "No hay notas compartidas", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Historico de marcadores", + "back": "Volver a la lista de marcadores", + "no-history": "No hay registro de marcadores", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Todos los estados", + "state-open": "Nuevo/Abrir", + "state-wip": "Trabajo en proceso", + "state-resolved": "Resuelto", + "state-rejected": "Rechazado", + "no-assignee": "Sin asignar", + "note-added": "Nota añadida", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", - "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", + "modal-title": "Reportar contenido inapropiado", + "modal-body": "Por favor especifica tu razón para marcar %1 %2 para revisar. Alternativamente, usa una de los botones de reporte rápido si corresponde.", + "modal-reason-spam": "Correo no deseado", + "modal-reason-offensive": "Ofensivo", + "modal-reason-custom": "Razón para reportar este contenido...", + "modal-submit": "Enviar reporte", "modal-submit-success": "Content has been flagged for moderation." } \ No newline at end of file diff --git a/public/language/fr/flags.json b/public/language/fr/flags.json index c4bfed6f23..c4d031cca4 100644 --- a/public/language/fr/flags.json +++ b/public/language/fr/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", + "state": "Etat", + "reporter": "Rapporteur", + "reported-at": "Reporté à", "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "no-flags": "Hourra ! Aucun signalement trouvé.", + "assignee": "Assigné", + "update": "Mise à jour", + "updated": "Mis à jour", + "target-purged": "Le contenu référencé par ce signalement a été supprimé et n'est plus accessible", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "quick-filters": "Filtres rapides", + "filter-active": "Il y a un ou plusieurs filtres actifs dans cette liste de signalements", + "filter-reset": "Supprimer les filtres", + "filters": "Options de filtre", + "filter-reporterId": "UID du reporteur", + "filter-targetUid": "UID signalé", + "filter-type": "Type de signalement", + "filter-type-all": "Tout le contenu", + "filter-type-post": "Message", + "filter-state": "Etat", + "filter-assignee": "UID assigné", + "filter-cid": "Catégorie", + "filter-quick-mine": "Assigné à moi", + "filter-cid-all": "Toutes les catégories", + "apply-filters": "Appliquer les filtres", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Permaliens", + "flagged-user": "Utilisateurs signalés", + "view-profile": "Voir le profil", + "start-new-chat": "Démarrer un nouveau Chat", + "go-to-target": "Voir le signalement cible", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Voir le profil", + "user-edit": "Éditer le profil", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Notes de signalement", + "add-note": "Ajouter une note", + "no-notes": "aucune note partagée", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Historiques des signalements", + "back": "Revenir à la liste des signalements", + "no-history": "aucun historique de signalements", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Tous les états", + "state-open": "Nouveau/Ouvert", + "state-wip": "En cours", + "state-resolved": "Résolu", + "state-rejected": "Rejeté", + "no-assignee": "Non assigné", + "note-added": "Note ajoutée", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-title": "Signaler un contenu inapproprié", + "modal-body": "Veuillez spécifier votre raison de signaler %1 %2 pour une révision. Vous pouvez utiliser un des boutons de report rapide si c'est plus approprié", "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-reason-offensive": "Choquant", + "modal-reason-custom": "Motif du signalement...", + "modal-submit": "Soumettre le signalement", + "modal-submit-success": "Le contenu a été soumis pour examen." } \ No newline at end of file diff --git a/public/language/gl/flags.json b/public/language/gl/flags.json index c4bfed6f23..0fda38ff5a 100644 --- a/public/language/gl/flags.json +++ b/public/language/gl/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "Estado", + "reporter": "Reportador", + "reported-at": "Reportado en", + "description": "Descripción", + "no-flags": "Un licorca, que non hai nada marcado para revisión.", + "assignee": "Encargado", + "update": "Actualizar", + "updated": "Actualizado", + "target-purged": "O contido marcado foi purgado e xa non está dispoñible", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "quick-filters": "Filtros rápidos", + "filter-active": "Hai un ou máis filtros na lista de avisos", + "filter-reset": "Eliminar filtros", + "filters": "Filtrar opcións", + "filter-reporterId": "UID do reportador", + "filter-targetUid": "UID marcada", + "filter-type": "Tipo de aviso", + "filter-type-all": "Todo o contido", + "filter-type-post": "Publicar", + "filter-state": "Estado", + "filter-assignee": "UID do encargado", + "filter-cid": "Categoría", + "filter-quick-mine": "Asignado a min", + "filter-cid-all": "Tódalas categorías", + "apply-filters": "Aplicar filtros", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Ligazóns rápidas", + "flagged-user": "Usuario marcado", + "view-profile": "Ver perfil", + "start-new-chat": "Comezar novo chat", + "go-to-target": "Ver contido marcado", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Ver perfil", + "user-edit": "Editar perfil", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Notas do aviso", + "add-note": "Engadir nota", + "no-notes": "Ningunha nota foi compartida", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Historial de avisos", + "back": "Voltar á lista de avisos", + "no-history": "Non hai historial de avisos", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Tódolos estados", + "state-open": "Novo/Abrir", + "state-wip": "Traballo en progreso", + "state-resolved": "Resolto", + "state-rejected": "Rexeitado", + "no-assignee": "Non asignado", + "note-added": "Nota engadida", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-title": "Reportar Contido Inapropiado", + "modal-body": "Por favor, especifique o seu motivo para marcar %1 %2 para revisión. Alternativamente, empregue un dos botóns de reporte rápido se fose pertinente.", "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-reason-offensive": "Ofensivo", + "modal-reason-custom": "Motivo para reportar este contido...", + "modal-submit": "Enviar Reporte", + "modal-submit-success": "Contido marcado para moderación" } \ No newline at end of file diff --git a/public/language/ja/flags.json b/public/language/ja/flags.json index c4bfed6f23..90a2d96569 100644 --- a/public/language/ja/flags.json +++ b/public/language/ja/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "状態", + "reporter": "報告者", + "reported-at": "報告された", + "description": "説明", + "no-flags": "おめでとう!フラグは見つかりませんでした。", + "assignee": "譲受人", + "update": "更新", + "updated": "更新されました", + "target-purged": "このフラグが参照しているコンテンツは切り離されており、利用できません。", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "quick-filters": "クイックフィルター", + "filter-active": "このフラグのリストには1つまたは複数のフィルタが有効です。", + "filter-reset": "フィルターを削除", + "filters": "フィルターオプション", + "filter-reporterId": "報告者のユーザーID", + "filter-targetUid": "フラグを立てたユーザーID", + "filter-type": "フラグの種類", + "filter-type-all": "すべてのコンテンツ", + "filter-type-post": "投稿", + "filter-state": "状態", + "filter-assignee": "譲受人のユーザーID", + "filter-cid": "カテゴリ", + "filter-quick-mine": "私に割り当てられました", + "filter-cid-all": "全てのカテゴリ", + "apply-filters": "フィルターを追加", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "クイックリンク", + "flagged-user": "フラグを立てたユーザー", + "view-profile": "プロフィールを見る", + "start-new-chat": "新しいチャットを開始", + "go-to-target": "フラグのターゲットを表示", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "プロフィールを見る", + "user-edit": "プロフィールを編集", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "ノートにフラグをつける", + "add-note": "ノートを追加", + "no-notes": "共有ノートはありません。", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "フラグ履歴", + "back": "フラグリストに戻る", + "no-history": "フラグ履歴がありません", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "全ての状態", + "state-open": "新規/開く", + "state-wip": "進行中の作業", + "state-resolved": "解決済み", + "state-rejected": "拒否済", + "no-assignee": "割り当てられていない", + "note-added": "ノートが追加されました", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", - "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-title": "不適切なコンテンツを報告する", + "modal-body": "レビューのために%1 %2 にフラグを付ける理由を指定してください。または必要に応じてクイックレポートボタンの1つを使用します。", + "modal-reason-spam": "スパム", + "modal-reason-offensive": "攻撃", + "modal-reason-custom": "このコンテンツを報告する理由...", + "modal-submit": "レポートを提出", + "modal-submit-success": "コンテンツはモデレーションにフラグ付けされています。" } \ No newline at end of file diff --git a/public/language/pt-BR/flags.json b/public/language/pt-BR/flags.json index c4bfed6f23..802b042fa3 100644 --- a/public/language/pt-BR/flags.json +++ b/public/language/pt-BR/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "Estado", + "reporter": "Reportado por", + "reported-at": "Reportado Em", + "description": "Descrição", + "no-flags": "Ihuul! Nenhuma sinalização encontrada.", + "assignee": "Cessionário", + "update": "Atualizar", + "updated": "Atualizado", + "target-purged": "O conteúdo ao qual essa sinalização se referia foi removido e não está mais disponível.", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", + "quick-filters": "Filtros Rápidos", + "filter-active": "Há um ou mais filtros ativos nesta lista de sinalizações", + "filter-reset": "Remover Filtros", + "filters": "Opções de Filtro", + "filter-reporterId": "UID do Reportador", + "filter-targetUid": "UID Sinalizado", + "filter-type": "Tipo de Sinalização", + "filter-type-all": "Todo o Conteúdo", "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "filter-state": "Estado", + "filter-assignee": "UID do Cessionário", + "filter-cid": "Categoria", + "filter-quick-mine": "Procurado à mim", + "filter-cid-all": "Todas as categorias", + "apply-filters": "Aplicar Filtros", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Links Rápidos", + "flagged-user": "Usuário Sinalizado", + "view-profile": "Ver Perfil", + "start-new-chat": "Iniciar Novo Chat", + "go-to-target": "Ver Sinalizado", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Ver Perfil", + "user-edit": "Editar Perfil", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Notas da Sinalização", + "add-note": "Adicionar Nota", + "no-notes": "Nenhuma nota compartilhada.", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Histórico de Sinalizações", + "back": "Voltar à Lista de Sinaliações", + "no-history": "Sem histórico de sinalizações.", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Todos os estados", + "state-open": "Novo/Aberto", + "state-wip": "Trabalho em Progresso", + "state-resolved": "Resolvido", + "state-rejected": "Rejeitado", + "no-assignee": "Não Procurado", + "note-added": "Nota Adicionada", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", + "modal-title": "Reportar Conteúdo Inadequado", + "modal-body": "Por favor especifique sua razão para sinalizar %1 %2 para revisão. Alternativamente, use um dos botões de reporte rápido se for aplicável.", "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-reason-offensive": "Ofensivo", + "modal-reason-custom": "Motivo para reportar este conteúdo...", + "modal-submit": "Enviar Reportagem", + "modal-submit-success": "O conteúdo foi sinalizado para moderação." } \ No newline at end of file diff --git a/public/language/pt-PT/flags.json b/public/language/pt-PT/flags.json index c4bfed6f23..8d1d58cbf4 100644 --- a/public/language/pt-PT/flags.json +++ b/public/language/pt-PT/flags.json @@ -2,59 +2,59 @@ "state": "State", "reporter": "Reporter", "reported-at": "Reported At", - "description": "Description", + "description": "Descrição", "no-flags": "Hooray! No flags found.", "assignee": "Assignee", - "update": "Update", - "updated": "Updated", + "update": "Atualizar", + "updated": "Atualizado", "target-purged": "The content this flag referred to has been purged and is no longer available.", - "quick-filters": "Quick Filters", + "quick-filters": "Filtros Rápidos", "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", + "filter-reset": "Remover Filtros", + "filters": "Opções dos Filtros", "filter-reporterId": "Reporter UID", "filter-targetUid": "Flagged UID", "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", + "filter-type-all": "Todo o Conteúdo", + "filter-type-post": "Publicação", "filter-state": "State", "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "filter-cid": "Categoria", + "filter-quick-mine": "Atribuído a mim", + "filter-cid-all": "Todas as categorias", + "apply-filters": "Aplicar Filtros", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Links Rápidos", + "flagged-user": "Utilizador Sinalizado", + "view-profile": "Ver Perfil", + "start-new-chat": "Iniciar Nova Conversa", + "go-to-target": "Ver Alvo da Sinalização", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Ver Perfil", + "user-edit": "Editar Perfil", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Sinalizar Notas", + "add-note": "Adicionar Nota", + "no-notes": "Não existem notas partilhadas.", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Histórico de Sinalizações", + "back": "Voltar para a Lista de Sinalizações", + "no-history": "Não existe histórico de sinalizações.", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Todos os estados", + "state-open": "Novo/Abrir", + "state-wip": "Trabalho em Progresso", + "state-resolved": "Resolvido", + "state-rejected": "Rejeitado", + "no-assignee": "Não Atribuído", + "note-added": "Nota Adicionada.", "modal-title": "Report Inappropriate Content", "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-reason-offensive": "Ofensivo", + "modal-reason-custom": "Motivo para reportar este conteúdo...", + "modal-submit": "Submeter Relatório", + "modal-submit-success": "Conteúdo sinalizado para moderação." } \ No newline at end of file diff --git a/public/language/ru/flags.json b/public/language/ru/flags.json index c4bfed6f23..6a5776a4f2 100644 --- a/public/language/ru/flags.json +++ b/public/language/ru/flags.json @@ -2,59 +2,59 @@ "state": "State", "reporter": "Reporter", "reported-at": "Reported At", - "description": "Description", + "description": "Описание", "no-flags": "Hooray! No flags found.", "assignee": "Assignee", - "update": "Update", - "updated": "Updated", + "update": "Обновить", + "updated": "Обновлено", "target-purged": "The content this flag referred to has been purged and is no longer available.", "quick-filters": "Quick Filters", "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", + "filter-reset": "Убрать фильтры", "filters": "Filter Options", "filter-reporterId": "Reporter UID", "filter-targetUid": "Flagged UID", "filter-type": "Flag Type", - "filter-type-all": "All Content", + "filter-type-all": "Весь контент", "filter-type-post": "Post", "filter-state": "State", "filter-assignee": "Assignee UID", - "filter-cid": "Category", + "filter-cid": "Категория", "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", + "filter-cid-all": "Все категории", "apply-filters": "Apply Filters", "quick-links": "Quick Links", "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", + "view-profile": "Просмотреть профиль", + "start-new-chat": "Начать новый чат", "go-to-target": "View Flag Target", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Просмотреть профиль", + "user-edit": "Изменить Профиль", "notes": "Flag Notes", - "add-note": "Add Note", + "add-note": "Добавить примечание", "no-notes": "No shared notes.", "history": "Flag History", "back": "Back to Flags List", "no-history": "No flag history.", - "state-all": "All states", - "state-open": "New/Open", + "state-all": "Все государства", + "state-open": "Новый/Открыть", "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", + "state-resolved": "Решен", + "state-rejected": "Отклонен", + "no-assignee": "Не назначенный ", "note-added": "Note Added", "modal-title": "Report Inappropriate Content", "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", - "modal-reason-spam": "Spam", + "modal-reason-spam": "Спам", "modal-reason-offensive": "Offensive", "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", + "modal-submit": "Представить отчет", "modal-submit-success": "Content has been flagged for moderation." } \ No newline at end of file diff --git a/public/language/sk/flags.json b/public/language/sk/flags.json index c4bfed6f23..21bdc1bf31 100644 --- a/public/language/sk/flags.json +++ b/public/language/sk/flags.json @@ -3,7 +3,7 @@ "reporter": "Reporter", "reported-at": "Reported At", "description": "Description", - "no-flags": "Hooray! No flags found.", + "no-flags": "Hurá! Žiadne označenia neboli nájdené.", "assignee": "Assignee", "update": "Update", "updated": "Updated", @@ -26,7 +26,7 @@ "apply-filters": "Apply Filters", "quick-links": "Quick Links", - "flagged-user": "Flagged User", + "flagged-user": "Označený používateľ", "view-profile": "View Profile", "start-new-chat": "Start New Chat", "go-to-target": "View Flag Target", diff --git a/public/language/sr/flags.json b/public/language/sr/flags.json index c4bfed6f23..e60225af70 100644 --- a/public/language/sr/flags.json +++ b/public/language/sr/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "Стање", + "reporter": "Извештач", + "reported-at": "Пријављено", + "description": "Опис", + "no-flags": "Ура! Нема заставица.", + "assignee": "Заступник", + "update": "Ажурирај", + "updated": "Ажурирано", + "target-purged": "Садржај на који се односи ова заставица је очишћен и није више доступан.", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "quick-filters": "Брзи филтери", + "filter-active": "Постоји један или више активних филтера на овом списку заставица", + "filter-reset": "Уклони заставице", + "filters": "Опције филтера", + "filter-reporterId": "UID извештача", + "filter-targetUid": "UID означеног", + "filter-type": "Тип заставице", + "filter-type-all": "Сав садржај", + "filter-type-post": "Порука", + "filter-state": "Стање", + "filter-assignee": "UID заступника", + "filter-cid": "Категорија", + "filter-quick-mine": "Додељено мени", + "filter-cid-all": "Све категорије", + "apply-filters": "Примени филтере", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Брзе везе", + "flagged-user": "Означени корисник", + "view-profile": "Погледај профил", + "start-new-chat": "Започни ново ћаскање", + "go-to-target": "Погледај циљ означавања", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Погледај профил", + "user-edit": "Уреди профил", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Белешке о заставицама", + "add-note": "Додај белешку", + "no-notes": "Нема дељених бележака.", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Историја заставица", + "back": "Назад на списак заставица", + "no-history": "Нема историје заставица", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Сва стања", + "state-open": "Ново/Отвори", + "state-wip": "Рад у току", + "state-resolved": "Решено", + "state-rejected": "Одбијено", + "no-assignee": "Недодељено", + "note-added": "Белешка је додата", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", - "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-title": "Пријави неприкладан садржај", + "modal-body": "Наведите разлог за означавање %1 %2 за проверу. Алтернативно, користите један од тастера за брзу пријаву ко је применљиво.", + "modal-reason-spam": "Непожељно", + "modal-reason-offensive": "Увредљиво", + "modal-reason-custom": "Разлог за пријаву овог садржаја...", + "modal-submit": "Пошаљи извештај", + "modal-submit-success": "Садржај је означен за модерацију." } \ No newline at end of file diff --git a/public/language/tr/flags.json b/public/language/tr/flags.json index c4bfed6f23..612ccb6500 100644 --- a/public/language/tr/flags.json +++ b/public/language/tr/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "Bildiri", + "reporter": "Muhabir", + "reported-at": "Bildirildi", + "description": "Açıklama", + "no-flags": "Yaşasın! Bayrak bulunamadı.", + "assignee": "Vekil", + "update": "Güncelle", + "updated": "Güncellendi", + "target-purged": "İlgili bayrağın içeriği temizlendi ve artık mevcut değil.", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "quick-filters": "Akıllı Filtre", + "filter-active": "Bayraklar listesinde etkin olan bir veya daha fazla filtre var", + "filter-reset": "Filtreleri Kaldır", + "filters": "Filtre Ayarı", + "filter-reporterId": "Muhabir UID", + "filter-targetUid": "Bayraklanan UID", + "filter-type": "Bayrak Tipi", + "filter-type-all": "Bütün İçerik", + "filter-type-post": "İleti", + "filter-state": "Bildiri", + "filter-assignee": "Vekil UID", + "filter-cid": "Kategori", + "filter-quick-mine": "Vekil atandı", + "filter-cid-all": "Bütün Kategoriler", + "apply-filters": "Filtreleri Onayla", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "Akıllı Bağlantılar", + "flagged-user": "Bayraklanan Kullanıcı", + "view-profile": "Profili Gör", + "start-new-chat": "Yeni Sohbet Başlat", + "go-to-target": "Bayrak Hedefini Gör", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "Profili Gör", + "user-edit": "Profili Düzenle", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "Bayrak Notu", + "add-note": "Not Ekle", + "no-notes": "Not paylaşılmadı", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "Bayrak Geçmişi", + "back": "Bayrak Listesine Geri Dön", + "no-history": "Bayrak geçmişi yok", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "Bütün Bildiriler", + "state-open": "Yeni/Açık", + "state-wip": "Yapım Aşamasında", + "state-resolved": "Çözüldü", + "state-rejected": "Reddedildi", + "no-assignee": "Atanmadı", + "note-added": "Not eklendi", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", - "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-title": "Uygunsuz İçeriği Rapor Et", + "modal-body": "%1 %2 için bayraklama nedenini belirtin. Alternatif olarak hızlı rapor butonlarından birinini kullanabilirsin.", + "modal-reason-spam": "Gereksiz", + "modal-reason-offensive": "Saldırgan", + "modal-reason-custom": "Bir içeriği bildirme nedeni...", + "modal-submit": "Raporu Gönder", + "modal-submit-success": "İçerik, denetlemek için bayraklandı." } \ No newline at end of file diff --git a/public/language/zh-CN/flags.json b/public/language/zh-CN/flags.json index c4bfed6f23..0346594fbc 100644 --- a/public/language/zh-CN/flags.json +++ b/public/language/zh-CN/flags.json @@ -1,60 +1,60 @@ { - "state": "State", - "reporter": "Reporter", - "reported-at": "Reported At", - "description": "Description", - "no-flags": "Hooray! No flags found.", - "assignee": "Assignee", - "update": "Update", - "updated": "Updated", - "target-purged": "The content this flag referred to has been purged and is no longer available.", + "state": "状态", + "reporter": "举报人", + "reported-at": "举报于", + "description": "描述", + "no-flags": "啊哈!没发现任何的举报。", + "assignee": "代理人", + "update": "更新", + "updated": "已更新", + "target-purged": "被举报的内容已经被清除,不再可用。", - "quick-filters": "Quick Filters", - "filter-active": "There are one or more filters active in this list of flags", - "filter-reset": "Remove Filters", - "filters": "Filter Options", - "filter-reporterId": "Reporter UID", - "filter-targetUid": "Flagged UID", - "filter-type": "Flag Type", - "filter-type-all": "All Content", - "filter-type-post": "Post", - "filter-state": "State", - "filter-assignee": "Assignee UID", - "filter-cid": "Category", - "filter-quick-mine": "Assigned to me", - "filter-cid-all": "All categories", - "apply-filters": "Apply Filters", + "quick-filters": "快速过滤器", + "filter-active": "该列中有一个或更多激活的过滤器", + "filter-reset": "删除过滤器", + "filters": "过滤器选项", + "filter-reporterId": "举报者UID", + "filter-targetUid": "被举报者 UID", + "filter-type": "举报类型", + "filter-type-all": "所有内容", + "filter-type-post": "帖子", + "filter-state": "状态", + "filter-assignee": "代理人UID", + "filter-cid": "版块", + "filter-quick-mine": "委托给我", + "filter-cid-all": "全部版块", + "apply-filters": "应用过滤器", - "quick-links": "Quick Links", - "flagged-user": "Flagged User", - "view-profile": "View Profile", - "start-new-chat": "Start New Chat", - "go-to-target": "View Flag Target", + "quick-links": "快速链接", + "flagged-user": "被举报的用户", + "view-profile": "查看个人资料", + "start-new-chat": "开始新会话", + "go-to-target": "查看举报目标", - "user-view": "View Profile", - "user-edit": "Edit Profile", + "user-view": "查看资料", + "user-edit": "编辑资料", - "notes": "Flag Notes", - "add-note": "Add Note", - "no-notes": "No shared notes.", + "notes": "举报备注", + "add-note": "添加备注", + "no-notes": "没有共享的备注内容。", - "history": "Flag History", - "back": "Back to Flags List", - "no-history": "No flag history.", + "history": "举报历史", + "back": "返回举报列表", + "no-history": "没有举报历史。", - "state-all": "All states", - "state-open": "New/Open", - "state-wip": "Work in Progress", - "state-resolved": "Resolved", - "state-rejected": "Rejected", - "no-assignee": "Not Assigned", - "note-added": "Note Added", + "state-all": "所有状态", + "state-open": "新建/打开", + "state-wip": "正在处理", + "state-resolved": "已解决", + "state-rejected": "已拒绝", + "no-assignee": "未指派", + "note-added": "备注已添加", - "modal-title": "Report Inappropriate Content", - "modal-body": "Please specify your reason for flagging %1 %2 for review. Alternatively, use one of the quick report buttons if applicable.", - "modal-reason-spam": "Spam", - "modal-reason-offensive": "Offensive", - "modal-reason-custom": "Reason for reporting this content...", - "modal-submit": "Submit Report", - "modal-submit-success": "Content has been flagged for moderation." + "modal-title": "举报不适内容", + "modal-body": "请选择或者输入您举报 %1%2 的原因以便版主进行审核。", + "modal-reason-spam": "垃圾信息", + "modal-reason-offensive": "人身攻击", + "modal-reason-custom": "举报此内容的理由……", + "modal-submit": "提交举报", + "modal-submit-success": "已举报此内容。" } \ No newline at end of file diff --git a/public/language/zh-TW/flags.json b/public/language/zh-TW/flags.json index c4bfed6f23..05a722bae2 100644 --- a/public/language/zh-TW/flags.json +++ b/public/language/zh-TW/flags.json @@ -5,7 +5,7 @@ "description": "Description", "no-flags": "Hooray! No flags found.", "assignee": "Assignee", - "update": "Update", + "update": "更新", "updated": "Updated", "target-purged": "The content this flag referred to has been purged and is no longer available.", @@ -17,7 +17,7 @@ "filter-targetUid": "Flagged UID", "filter-type": "Flag Type", "filter-type-all": "All Content", - "filter-type-post": "Post", + "filter-type-post": "文章", "filter-state": "State", "filter-assignee": "Assignee UID", "filter-cid": "Category", From 3ae3d788be3dcf11d6cf0d89a65c22d8a1c4cccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 9 May 2017 13:24:39 -0400 Subject: [PATCH 027/263] up composer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 36b7d2032f..b4a6a96933 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "4.4.6", + "nodebb-plugin-composer-default": "4.4.7", "nodebb-plugin-dbsearch": "2.0.2", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.1.5", From 5ccb488b023cd1d0e93f1333f97c4f10d442dc9d Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 9 May 2017 11:45:09 -0600 Subject: [PATCH 028/263] Fix indents --- src/notifications.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/notifications.js b/src/notifications.js index a158a036fb..fef4a4a4d2 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -194,10 +194,10 @@ function pushToUids(uids, notification, callback) { plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next); }, function (data, next) { - if (!data || !data.notification || !data.uids || !data.uids.length) { - return callback(); - } - + if (!data || !data.notification || !data.uids || !data.uids.length) { + return callback(); + } + uids = data.uids; notification = data.notification; From 5a9b7b673a06e8e00c23ffef1570760ab6a3c890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 9 May 2017 14:16:21 -0400 Subject: [PATCH 029/263] closes #5659 --- src/flags.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/flags.js b/src/flags.js index 9022af2c45..09d4c6e498 100644 --- a/src/flags.js +++ b/src/flags.js @@ -15,7 +15,7 @@ var utils = require('../public/src/utils'); var _ = require('underscore'); var S = require('string'); -var Flags = {}; +var Flags = module.exports; Flags.get = function (flagId, callback) { async.waterfall([ @@ -26,6 +26,9 @@ Flags.get = function (flagId, callback) { notes: async.apply(Flags.getNotes, flagId), }), function (data, next) { + if (!data.base) { + return callback(); + } // Second stage async.parallel({ userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture']), From bf78786a686c058c76d38f7a77f56ff516713a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 9 May 2017 14:31:32 -0400 Subject: [PATCH 030/263] flag controller tests --- src/controllers/mods.js | 101 +++++++++++++++++++------------------- src/socket.io/flags.js | 4 +- test/controllers-admin.js | 75 +++++++++++++++++++++++++++- test/flags.js | 1 + 4 files changed, 126 insertions(+), 55 deletions(-) diff --git a/src/controllers/mods.js b/src/controllers/mods.js index 6365650323..32e416a20d 100644 --- a/src/controllers/mods.js +++ b/src/controllers/mods.js @@ -7,61 +7,62 @@ var categories = require('../categories'); var flags = require('../flags'); var analytics = require('../analytics'); -var modsController = { - flags: {}, -}; +var modsController = module.exports; +modsController.flags = {}; modsController.flags.list = function (req, res, next) { - async.parallel({ - isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid), - moderatedCids: async.apply(user.getModeratedCids, req.uid), - }, function (err, results) { - if (err) { - return next(err); - } else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) { - return next(new Error('[[error:no-privileges]]')); - } - - if (!results.isAdminOrGlobalMod && results.moderatedCids.length) { - res.locals.cids = results.moderatedCids; - } - - // Parse query string params for filters - var hasFilter = false; - var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick']; - var filters = valid.reduce(function (memo, cur) { - if (req.query.hasOwnProperty(cur)) { - memo[cur] = req.query[cur]; + var filters; + var hasFilter; + async.waterfall([ + function (next) { + async.parallel({ + isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid), + moderatedCids: async.apply(user.getModeratedCids, req.uid), + }, next); + }, + function (results, next) { + if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) { + return next(new Error('[[error:no-privileges]]')); } - return memo; - }, {}); - hasFilter = !!Object.keys(filters).length; - - if (res.locals.cids) { - if (!filters.cid) { - // If mod and no cid filter, add filter for their modded categories - filters.cid = res.locals.cids; - } else if (Array.isArray(filters.cid)) { - // Remove cids they do not moderate - filters.cid = filters.cid.filter(function (cid) { - return res.locals.cids.indexOf(String(cid)) !== -1; - }); - } else if (res.locals.cids.indexOf(String(filters.cid)) === -1) { - filters.cid = res.locals.cids; - hasFilter = false; + if (!results.isAdminOrGlobalMod && results.moderatedCids.length) { + res.locals.cids = results.moderatedCids; } - } - async.parallel({ - flags: async.apply(flags.list, filters, req.uid), - analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30), - categories: async.apply(categories.buildForSelect, req.uid), - }, function (err, data) { - if (err) { - return next(err); + // Parse query string params for filters + hasFilter = false; + var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick']; + filters = valid.reduce(function (memo, cur) { + if (req.query.hasOwnProperty(cur)) { + memo[cur] = req.query[cur]; + } + + return memo; + }, {}); + hasFilter = !!Object.keys(filters).length; + + if (res.locals.cids) { + if (!filters.cid) { + // If mod and no cid filter, add filter for their modded categories + filters.cid = res.locals.cids; + } else if (Array.isArray(filters.cid)) { + // Remove cids they do not moderate + filters.cid = filters.cid.filter(function (cid) { + return res.locals.cids.indexOf(String(cid)) !== -1; + }); + } else if (res.locals.cids.indexOf(String(filters.cid)) === -1) { + filters.cid = res.locals.cids; + hasFilter = false; + } } + async.parallel({ + flags: async.apply(flags.list, filters, req.uid), + analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30), + categories: async.apply(categories.buildForSelect, req.uid), + }, next); + }, + function (data) { // If res.locals.cids is populated, then slim down the categories list if (res.locals.cids) { data.categories = data.categories.filter(function (category) { @@ -92,8 +93,8 @@ modsController.flags.list = function (req, res, next) { filters: filters, title: '[[pages:flags]]', }); - }); - }); + }, + ], next); }; modsController.flags.detail = function (req, res, next) { @@ -124,5 +125,3 @@ modsController.flags.detail = function (req, res, next) { })); }); }; - -module.exports = modsController; diff --git a/src/socket.io/flags.js b/src/socket.io/flags.js index 2cc2881832..f2952b1671 100644 --- a/src/socket.io/flags.js +++ b/src/socket.io/flags.js @@ -5,7 +5,7 @@ var async = require('async'); var user = require('../user'); var flags = require('../flags'); -var SocketFlags = {}; +var SocketFlags = module.exports; SocketFlags.create = function (socket, data, callback) { if (!socket.uid) { @@ -98,5 +98,3 @@ SocketFlags.appendNote = function (socket, data, callback) { }, ], callback); }; - -module.exports = SocketFlags; diff --git a/test/controllers-admin.js b/test/controllers-admin.js index 5a17ca96a9..086d54e7a8 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -18,6 +18,7 @@ describe('Admin Controllers', function () { var pid; var adminUid; var regularUid; + var moderatorUid; var jar; before(function (done) { @@ -35,12 +36,16 @@ describe('Admin Controllers', function () { regularUid: function (next) { user.create({ username: 'regular' }, next); }, + moderatorUid: function (next) { + user.create({ username: 'moderator', password: 'modmod' }, next); + }, }, function (err, results) { if (err) { return done(err); } adminUid = results.adminUid; regularUid = results.regularUid; + moderatorUid = results.moderatorUid; cid = results.category.cid; topics.post({ uid: adminUid, title: 'test topic title', content: 'test topic content', cid: results.category.cid }, function (err, result) { @@ -128,7 +133,7 @@ describe('Admin Controllers', function () { }); it('should 404 for edit/email page if user does not exist', function (done) { - request(nconf.get('url') + '/api/user/doesnotexist/edit/email', { jar: jar, json: true }, function (err, res, body) { + request(nconf.get('url') + '/api/user/doesnotexist/edit/email', { jar: jar, json: true }, function (err, res) { assert.ifError(err); assert.equal(res.statusCode, 404); done(); @@ -446,6 +451,74 @@ describe('Admin Controllers', function () { }); }); + describe('mods page', function () { + var moderatorJar; + + before(function (done) { + helpers.loginUser('moderator', 'modmod', function (err, _jar) { + assert.ifError(err); + moderatorJar = _jar; + + groups.join('cid:' + cid + ':privileges:mods', moderatorUid, done); + }); + }); + + it('should error with no privileges', function (done) { + request(nconf.get('url') + '/api/flags', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(body.error, '[[error:no-privileges]]'); + done(); + }); + }); + + it('should load flags page data', function (done) { + request(nconf.get('url') + '/api/flags', { jar: moderatorJar, json: true }, function (err, res, body) { + assert.ifError(err); + assert(body); + assert(body.flags); + assert(body.categories); + assert(body.filters); + assert.equal(body.categories[cid], 'Test Category'); + assert.equal(body.filters.cid.indexOf(cid), -1); + done(); + }); + }); + + it('should return invalid data if flag does not exist', function (done) { + request(nconf.get('url') + '/api/flags/123123123', { jar: moderatorJar, json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(body.error, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should error with not enough reputation to flag', function (done) { + var socketFlags = require('../src/socket.io/flags'); + + socketFlags.create({ uid: regularUid }, { id: pid, type: 'post', reason: 'spam' }, function (err) { + assert.equal(err.message, '[[error:not-enough-reputation-to-flag]]'); + done(); + }); + }); + + it('should return flag details', function (done) { + var meta = require('../src/meta'); + var socketFlags = require('../src/socket.io/flags'); + var oldValue = meta.config['privileges:flag']; + meta.config['privileges:flag'] = 0; + socketFlags.create({ uid: regularUid }, { id: pid, type: 'post', reason: 'spam' }, function (err, data) { + meta.config['privileges:flag'] = oldValue; + assert.ifError(err); + request(nconf.get('url') + '/api/flags/' + data.flagId, { jar: moderatorJar, json: true }, function (err, res, body) { + assert.ifError(err); + assert(body); + assert.equal(body.reporter.username, 'regular'); + done(); + }); + }); + }); + }); + after(function (done) { db.emptydb(done); }); diff --git a/test/flags.js b/test/flags.js index 0426541cd8..5d45b8afef 100644 --- a/test/flags.js +++ b/test/flags.js @@ -16,6 +16,7 @@ var Meta = require('../src/meta'); describe('Flags', function () { before(function (done) { + Groups.resetCache(); // Create some stuff to flag async.waterfall([ async.apply(User.create, { username: 'testUser', password: 'abcdef', email: 'b@c.com' }), From ca1797542270892df53a529d424d3631420373f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 9 May 2017 14:48:31 -0400 Subject: [PATCH 031/263] osd route test --- test/controllers.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/controllers.js b/test/controllers.js index e1568f6802..3e1000e39f 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -1061,6 +1061,15 @@ describe('Controllers', function () { }); }); + it('should return osd data', function (done) { + request(nconf.get('url') + '/osd.xml', function (err, res, body) { + assert.ifError(err); + assert(body); + done(); + }); + }); + + after(function (done) { var analytics = require('../src/analytics'); analytics.writeData(function (err) { From 6d392089e4dac40c35ca07594c5fab66e5b212d6 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 9 May 2017 14:52:07 -0400 Subject: [PATCH 032/263] fixes #5660 - https://ecma-international.org/ecma-262/5.1/#sec-15.9.1.1 --- public/src/client/topic/postTools.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 8bbda4d79c..707a9a5b0f 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -367,7 +367,8 @@ define('forum/topic/postTools', [ } function showStaleWarning(callback) { - if (staleReplyAnyway || ajaxify.data.lastposttime >= (Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays))) { + var staleThreshold = Math.min(Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays), 8640000000000000); + if (staleReplyAnyway || ajaxify.data.lastposttime >= staleThreshold) { return callback(); } From 87fbb44ca8d5e2db9936c916cdd02519e0966c09 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 9 May 2017 15:01:02 -0400 Subject: [PATCH 033/263] fixes #5660 properly --- public/src/utils.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/src/utils.js b/public/src/utils.js index 8f2dcb8e87..4610a85bf6 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -197,7 +197,14 @@ return ''; } - return Date.prototype.toISOString ? new Date(parseInt(timestamp, 10)).toISOString() : timestamp; + // Prevent too-high values to be passed to Date object + timestamp = Math.min(timestamp, 8640000000000000); + + try { + return Date.prototype.toISOString ? new Date(parseInt(timestamp, 10)).toISOString() : timestamp; + } catch (e) { + return timestamp; + } }, tags: ['a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'keygen', 'label', 'legend', 'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr'], From ad14b0d60b7963a5e9182928f4da9e1653b72023 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 9 May 2017 15:07:59 -0400 Subject: [PATCH 034/263] more fixes for #5660 --- src/database/mongo/main.js | 1 + src/socket.io/user/ban.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/database/mongo/main.js b/src/database/mongo/main.js index 4ca3bb2cd3..9381fbbc39 100644 --- a/src/database/mongo/main.js +++ b/src/database/mongo/main.js @@ -92,6 +92,7 @@ module.exports = function (db, module) { }; module.pexpireAt = function (key, timestamp, callback) { + timestamp = Math.min(timestamp, 8640000000000000); module.setObjectField(key, 'expireAt', new Date(timestamp), callback); }; }; diff --git a/src/socket.io/user/ban.js b/src/socket.io/user/ban.js index 191b98767e..3af03becb8 100644 --- a/src/socket.io/user/ban.js +++ b/src/socket.io/user/ban.js @@ -10,6 +10,7 @@ var privileges = require('../../privileges'); var plugins = require('../../plugins'); var emailer = require('../../emailer'); var translator = require('../../translator'); +var utils = require('../../../public/src/utils'); module.exports = function (SocketUser) { SocketUser.banUsers = function (socket, data, callback) { @@ -108,7 +109,7 @@ module.exports = function (SocketUser) { subject: '[[email:banned.subject, ' + siteTitle + ']]', site_title: siteTitle, username: username, - until: until ? new Date(until).toString() : false, + until: until ? utils.toISOString(until) : false, reason: reason, }; From 2b2544a4b139f9ecfae8620a0ef59c18e7ddf440 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 9 May 2017 14:35:11 -0600 Subject: [PATCH 035/263] Deprecate non-standard `String.prototype.rtrim` --- public/src/utils.js | 10 ++++++++-- src/topics/create.js | 8 ++------ test/utils.js | 7 +++++++ 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/public/src/utils.js b/public/src/utils.js index 8f2dcb8e87..f1154b64e9 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -405,9 +405,13 @@ (relative_path.length > 0 ? targetLocation.pathname.indexOf(relative_path) === 0 : true) // Subfolder installs need this additional check ); }, + + rtrim: function (str) { + return str.replace(/\s+$/g, ''); + }, }; - /* eslint "no-extend-native": "off" */ + /* eslint "no-extend-native": "off" */ if (typeof String.prototype.startsWith !== 'function') { String.prototype.startsWith = function (prefix) { if (this.length < prefix.length) { @@ -429,9 +433,11 @@ }; } + // DEPRECATED: remove in 1.6 if (typeof String.prototype.rtrim !== 'function') { String.prototype.rtrim = function () { - return this.replace(/\s+$/g, ''); + console.warn('[deprecated] `String.prototype.rtrim` is deprecated as of NodeBB v1.5; use `utils.rtrim` instead.'); + return utils.rtrim(this); }; } diff --git a/src/topics/create.js b/src/topics/create.js index 2ec75d3781..f7ee2e326e 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -88,10 +88,6 @@ module.exports = function (Topics) { ], callback); }; - function rtrim(str) { - return str.replace(/\s+$/g, ''); - } - Topics.post = function (data, callback) { var uid = data.uid; data.title = String(data.title).trim(); @@ -106,7 +102,7 @@ module.exports = function (Topics) { }, function (next) { if (data.content) { - data.content = rtrim(data.content); + data.content = utils.rtrim(data.content); } check(data.content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); }, @@ -239,7 +235,7 @@ module.exports = function (Topics) { function (filteredData, next) { content = filteredData.content || data.content; if (content) { - content = rtrim(content); + content = utils.rtrim(content); } check(content, meta.config.minimumPostLength, meta.config.maximumPostLength, 'content-too-short', 'content-too-long', next); diff --git a/test/utils.js b/test/utils.js index 2beedcf3eb..9c8c2bfb12 100644 --- a/test/utils.js +++ b/test/utils.js @@ -171,4 +171,11 @@ describe('Utility Methods', function () { } done(); }); + + it('`utils.rtrim` should remove trailing space', function (done) { + assert.strictEqual(utils.rtrim(' thing '), ' thing'); + assert.strictEqual(utils.rtrim('\tthing\t\t'), '\tthing'); + assert.strictEqual(utils.rtrim('\t thing \t'), '\t thing'); + done(); + }); }); From 3c8460fee0155da22cd272369108b947525a8606 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 9 May 2017 18:19:22 -0400 Subject: [PATCH 036/263] disable timeout --- app.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.js b/app.js index 26336a74c8..0e5b8ff69b 100644 --- a/app.js +++ b/app.js @@ -203,7 +203,8 @@ function upgrade() { // Skip build tasks.pop(); } - + //disable mongo timeouts during upgrade + nconf.set('mongo:options:socketTimeoutMS', 0); async.series(tasks, function (err) { if (err) { winston.error(err.stack); From 1d672d902481fe76ccbec6cd7c491ca12bfb6fe6 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Wed, 10 May 2017 09:23:07 +0000 Subject: [PATCH 037/263] Latest translations and fallbacks --- public/language/uk/admin/advanced/database.json | 2 +- public/language/uk/admin/manage/groups.json | 2 +- public/language/uk/admin/settings/advanced.json | 2 +- public/language/uk/admin/settings/post.json | 4 ++-- public/language/uk/admin/settings/user.json | 2 +- public/language/uk/email.json | 8 ++++---- public/language/uk/error.json | 4 ++-- public/language/uk/modules.json | 2 +- public/language/uk/topic.json | 2 +- public/language/uk/user.json | 2 +- public/language/zh-CN/flags.json | 4 ++-- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/public/language/uk/admin/advanced/database.json b/public/language/uk/admin/advanced/database.json index 66b2956c90..3c4dc1cd6f 100644 --- a/public/language/uk/admin/advanced/database.json +++ b/public/language/uk/admin/advanced/database.json @@ -1,7 +1,7 @@ { "x-b": "%1 б", "x-mb": "%1 мб", - "x-gb": "%1 gb", + "x-gb": "%1 ГБ", "uptime-seconds": "Uptime в секундах", "uptime-days": "Uptime в днях", diff --git a/public/language/uk/admin/manage/groups.json b/public/language/uk/admin/manage/groups.json index 9758b901c8..d3b65abb46 100644 --- a/public/language/uk/admin/manage/groups.json +++ b/public/language/uk/admin/manage/groups.json @@ -1,7 +1,7 @@ { "name": "Назва групи", "description": "Опис групи", - "member-count": "Member Count", + "member-count": "Кількість Учасників", "system": "Системна група", "edit": "Редагувати", "search-placeholder": "Пошук", diff --git a/public/language/uk/admin/settings/advanced.json b/public/language/uk/admin/settings/advanced.json index 4f96f25c9b..e858adf80d 100644 --- a/public/language/uk/admin/settings/advanced.json +++ b/public/language/uk/admin/settings/advanced.json @@ -6,7 +6,7 @@ "headers.allow-from": "Задати ALLOW-FROM для розміщення NodeBB в iFrame", "headers.powered-by": "Налаштувати заголовок \"Powered By\", котрий відправляє NodeBB", "headers.acao": "Access-Control-Allow-Origin", - "headers.acao-help": "To deny access to all sites, leave empty", + "headers.acao-help": "Щоб заборонити доступ до всіх сайтів, залиште незаповненим", "headers.acam": "Access-Control-Allow-Methods", "headers.acah": "Access-Control-Allow-Headers", "traffic-management": "Керування трафіком", diff --git a/public/language/uk/admin/settings/post.json b/public/language/uk/admin/settings/post.json index ea770e1463..0be4036412 100644 --- a/public/language/uk/admin/settings/post.json +++ b/public/language/uk/admin/settings/post.json @@ -29,8 +29,8 @@ "unread": "Налаштування непрочитаних", "unread.cutoff": "За скільки днів показувати непрочитані", "unread.min-track-last": "Мінімальна кількість постів у темі перш ніж відслідковувати останні прочитані", - "recent": "Recent Settings", - "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", + "recent": "Останні Налаштування", + "recent.categoryFilter.disable": "Відключити фільтрування тем в ігнорованих категоріях на сторінці /recent", "signature": "Налаштування підписів", "signature.disable": "Вимкнути підписи", "signature.no-links": "Вимкнути посилання в підписах", diff --git a/public/language/uk/admin/settings/user.json b/public/language/uk/admin/settings/user.json index dc463f9ecd..cdcff8f963 100644 --- a/public/language/uk/admin/settings/user.json +++ b/public/language/uk/admin/settings/user.json @@ -37,7 +37,7 @@ "min-username-length": "Мінімальна довжина імені користувача", "max-username-length": "Максимальна довжина імені користувача", "min-password-length": "Мінімальна довжина пароля", - "min-password-strength": "Minimum Password Strength", + "min-password-strength": "Мінімальна Довжина Паролю", "max-about-me-length": "Максимальна довжина розділу \"Про мене\"", "terms-of-use": "Умови користування форумом (Залиште пустим, щоб вимкнути)", "user-search": "Пошук користувачів", diff --git a/public/language/uk/email.json b/public/language/uk/email.json index 236a1f3978..eddbf7c236 100644 --- a/public/language/uk/email.json +++ b/public/language/uk/email.json @@ -32,9 +32,9 @@ "notif.post.unsub.info": "Це поштове повідомлення було вислано вам, згідно ваших налаштувань підписки", "test.text1": "Це пробний лист для верифікації поштової служби. Всі налаштування вірні для NodeBB.", "unsub.cta": "Натисніть тут, щоб змінити ці налаштування", - "banned.subject": "You have been banned from %1", - "banned.text1": "The user %1 has been banned from %2.", - "banned.text2": "This ban will last until %1.", - "banned.text3": "This is the reason why you have been banned:", + "banned.subject": "Ви були забанені на %1", + "banned.text1": "Користувач %1 був забанений на %2.", + "banned.text2": "Тривалість бану - до %1.", + "banned.text3": "Це причина, чому ви були забанені:", "closing": "Дякуємо!" } \ No newline at end of file diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 2cc9650c8e..0ae2e3a6f2 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -30,7 +30,7 @@ "password-too-long": "Пароль задовгий", "user-banned": "Користувача забанено", "user-banned-reason": "Вибачте, але цей акаунт було забанено (Причина: %1)", - "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-banned-reason-until": "Вибачте, цей акаунт забанений до %1 (Причина: %2)", "user-too-new": "Вибачте, але вам необхідно зачекати %1 секунд(и), перед першим постом", "blacklisted-ip": "Вибачте, але ваша IP-адреса була забанена в цій спільноті. Якщо ви гадаєте, що це сталось помилково, зв'яжіться з адміністратором.", "ban-expiry-missing": "Вкажіть, будь ласка, кінцеву дату бану", @@ -105,7 +105,7 @@ "chat-disabled": "Чат вимкнено", "too-many-messages": "Ви надіслали забагато повідомлень, зачекайте трішки.", "invalid-chat-message": "Невірне повідомлення чату", - "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "chat-message-too-long": "Повідомлення чату не можуть бути довшими за %1 символів.", "cant-edit-chat-message": "Ви не можете редагувати повідомлення", "cant-remove-last-user": "Ви не можете видалити останнього користувача", "cant-delete-chat-message": "Ви не можете видалити це повідомлення", diff --git a/public/language/uk/modules.json b/public/language/uk/modules.json index 6cb7697cc5..ef440ec8ff 100644 --- a/public/language/uk/modules.json +++ b/public/language/uk/modules.json @@ -20,7 +20,7 @@ "chat.three_months": "3 місяці", "chat.delete_message_confirm": "Ви впевнені, що хочете видалити це повідомлення?", "chat.add-users-to-room": "Додати користувачів до кімнати", - "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.confirm-chat-with-dnd-user": "Користувач змінив свій статус на DnD (Не турбувати). Ви дійсно бажаєте надіслати йому повідомлення в чат?", "composer.compose": "Редактор повідомлень", "composer.show_preview": "Показати попередній перегляд", "composer.hide_preview": "Сховати попередній перегляд", diff --git a/public/language/uk/topic.json b/public/language/uk/topic.json index 111efb25d9..b936e85ea5 100644 --- a/public/language/uk/topic.json +++ b/public/language/uk/topic.json @@ -30,7 +30,7 @@ "pinned": "Закріплена", "moved": "Переміщена", "bookmark_instructions": "Натисніть тут, щоб повернутися до останнього прочитаного посту у цій темі.", - "flag_title": "Поскаржитись цей пост модератору", + "flag_title": "Поскаржитись на цей пост модератору", "deleted_message": "Цю тему було видалено. Лише користувачі з правом керування темами можуть її бачити.", "following_topic.message": "Відтепер ви будете отримувати сповіщення коли хтось постить в цю тему.", "not_following_topic.message": "Ви будете бачити цю тему в списку непрочитаних, але ви не будете отримувати сповіщень, коли хтось постить до неї.", diff --git a/public/language/uk/user.json b/public/language/uk/user.json index 0d5e93857f..20d3dfe51f 100644 --- a/public/language/uk/user.json +++ b/public/language/uk/user.json @@ -60,7 +60,7 @@ "username_taken_workaround": "Ім'я користувача, що ви обрали, вже було зайняте, то ж ми його трішки змінили. Ви тепер відомі як %1", "password_same_as_username": "Ваш пароль співпадає з іменем користувача. Оберіть інший пароль, будь ласка.", "password_same_as_email": "Ваш пароль співпадає з електронною поштою. Оберіть інший пароль, будь ласка.", - "weak_password": "Weak password.", + "weak_password": "Слабкий пароль", "upload_picture": "Завантажити зображення", "upload_a_picture": "Завантажити зображення", "remove_uploaded_picture": "Видалити завантажене зображення", diff --git a/public/language/zh-CN/flags.json b/public/language/zh-CN/flags.json index 0346594fbc..189330bdfd 100644 --- a/public/language/zh-CN/flags.json +++ b/public/language/zh-CN/flags.json @@ -4,7 +4,7 @@ "reported-at": "举报于", "description": "描述", "no-flags": "啊哈!没发现任何的举报。", - "assignee": "代理人", + "assignee": "受委托人", "update": "更新", "updated": "已更新", "target-purged": "被举报的内容已经被清除,不再可用。", @@ -19,7 +19,7 @@ "filter-type-all": "所有内容", "filter-type-post": "帖子", "filter-state": "状态", - "filter-assignee": "代理人UID", + "filter-assignee": "受委托人 UID", "filter-cid": "版块", "filter-quick-mine": "委托给我", "filter-cid-all": "全部版块", From 71137dbf43419dd6cb4f8db85e4619af9a086449 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Wed, 10 May 2017 11:21:25 -0400 Subject: [PATCH 038/263] pass reputation to flag reporter payload as well --- src/flags.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flags.js b/src/flags.js index 09d4c6e498..458e2bdf56 100644 --- a/src/flags.js +++ b/src/flags.js @@ -31,7 +31,7 @@ Flags.get = function (flagId, callback) { } // Second stage async.parallel({ - userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture']), + userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture', 'reputation']), targetObj: async.apply(Flags.getTarget, data.base.type, data.base.targetId, data.base.uid), }, function (err, payload) { // Final object return construction From aabfe8f41fea69aabf394b7ad5475079bece28d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 14:02:58 -0400 Subject: [PATCH 039/263] fix lint --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 0e5b8ff69b..edb8565559 100644 --- a/app.js +++ b/app.js @@ -203,7 +203,7 @@ function upgrade() { // Skip build tasks.pop(); } - //disable mongo timeouts during upgrade + // disable mongo timeouts during upgrade nconf.set('mongo:options:socketTimeoutMS', 0); async.series(tasks, function (err) { if (err) { From 73f375252f3e9f36d16e9f675290aa8e24a98a39 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 9 May 2017 22:04:59 -0600 Subject: [PATCH 040/263] Automatically install dependencies --- nodebb | 44 +++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/nodebb b/nodebb index 22051bb810..50bcc65fee 100755 --- a/nodebb +++ b/nodebb @@ -2,36 +2,34 @@ 'use strict'; -var cproc; -var args; -var fs; -var path; -var request; -var semver; -var prompt; -var async; +var fs = require('fs'); +var path = require('path'); +var cproc = require('child_process'); +// check to make sure dependencies are installed try { - require('colors'); - cproc = require('child_process'); - args = require('minimist')(process.argv.slice(2)); - fs = require('fs'); - path = require('path'); - request = require('request'); - semver = require('semver'); - prompt = require('prompt'); - async = require('async'); + fs.readFileSync(path.join(__dirname, 'node_modules/async/package.json')); } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - process.stdout.write('NodeBB could not be started because it\'s dependencies have not been installed.\n'); - process.stdout.write('Please ensure that you have executed "npm install --production" prior to running NodeBB.\n\n'); - process.stdout.write('For more information, please see: https://docs.nodebb.org/en/latest/installing/os.html\n\n'); - process.stdout.write('Could not start: ' + e.code + '\n'); + if (e.code === 'ENOENT') { + process.stdout.write('Dependencies not yet installed.\n'); + process.stdout.write('Installing them now...\n\n'); - process.exit(1); + cproc.execSync('npm i --production', { + cwd: __dirname, + stdio: [0, 1, 2], + }); + } else { + throw e; } } +require('colors'); +var args = require('minimist')(process.argv.slice(2)); +var request = require('request'); +var semver = require('semver'); +var prompt = require('prompt'); +var async = require('async'); + var loaderPath = path.join(__dirname, 'loader.js'); var appPath = path.join(__dirname, 'app.js'); From eedc0889fc1b4b3a42c49aae996a770b3ab6f7e4 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 9 May 2017 22:08:28 -0600 Subject: [PATCH 041/263] Fix failed lint --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 0e5b8ff69b..edb8565559 100644 --- a/app.js +++ b/app.js @@ -203,7 +203,7 @@ function upgrade() { // Skip build tasks.pop(); } - //disable mongo timeouts during upgrade + // disable mongo timeouts during upgrade nconf.set('mongo:options:socketTimeoutMS', 0); async.series(tasks, function (err) { if (err) { From 970c7a8caa7a91a3ee7895b85b54de7f6ed11b9f Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 9 May 2017 22:20:40 -0600 Subject: [PATCH 042/263] Defer winston logging until after tests --- test/defer-logger.js | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/defer-logger.js diff --git a/test/defer-logger.js b/test/defer-logger.js new file mode 100644 index 0000000000..3ddb9de45c --- /dev/null +++ b/test/defer-logger.js @@ -0,0 +1,40 @@ +'use strict'; + +var util = require('util'); +var winston = require('winston'); + +function DeferLogger(options) { + options = options || {}; + + this.name = 'DeferLogger'; + this.level = options.level || 'info'; + + this.logged = options.logged; +} + +util.inherits(DeferLogger, winston.Transport); + +DeferLogger.prototype.log = function log(level, msg, meta, callback) { + this.logged.push([level, msg, meta]); + callback(null, true); +}; + +var winstonLogged = []; + +before(function () { + // defer winston logs until the end + winston.remove(winston.transports.Console); + + winston.add(DeferLogger, { + logged: winstonLogged, + }); +}); + +after(function () { + console.log('\n\n'); + + var con = new winston.transports.Console(); + winstonLogged.forEach(function (args) { + con.log(args[0], args[1], args[2], function () {}); + }); +}); From 759c3f5584efdaace97ae6795f48ec1755facad4 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Tue, 9 May 2017 22:39:04 -0600 Subject: [PATCH 043/263] Fix soundpacks not working --- src/plugins/load.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/load.js b/src/plugins/load.js index f4ba868eea..04b65e9163 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -263,12 +263,12 @@ module.exports = function (Plugins) { soundpack.id = pluginData.id; soundpack.dir = path.join(pluginData.path, soundpack.dir); async.each(Object.keys(soundpack.sounds), function (key, next) { - file.exists(path.join(soundpack.dir, soundpack.sounds[key]), function (exists) { + file.exists(path.join(soundpack.dir, soundpack.sounds[key]), function (err, exists) { if (!exists) { delete soundpack.sounds[key]; } - next(); + next(err); }); }, function (err) { if (err) { From 29c0b8337e31d825873aab14d72b6d3d9950e27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 14:43:52 -0400 Subject: [PATCH 044/263] closes #5645 --- public/src/modules/settings.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index ecd99a9521..93583d6ed9 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -461,7 +461,9 @@ define('settings', function () { for (var key in values) { if (values.hasOwnProperty(key)) { try { - values[key] = JSON.parse(values[key]); + if (!utils.isNumber(values[key])) { + values[key] = JSON.parse(values[key]); + } } catch (e) { // Leave the value as is } From 66dee3452f90006017b2fcffe36f6999783a8dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 15:34:36 -0400 Subject: [PATCH 045/263] more tests for admin uploads --- src/controllers/admin/uploads.js | 77 +++++++++++++------------------ src/meta/sounds.js | 4 +- test/files/test.wav | Bin 0 -> 26124 bytes test/uploads.js | 37 +++++++++++++++ 4 files changed, 72 insertions(+), 46 deletions(-) create mode 100644 test/files/test.wav diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 4280a2793b..1eeaba8041 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -14,7 +14,7 @@ var plugins = require('../../plugins'); var allowedImageTypes = ['image/png', 'image/jpeg', 'image/pjpeg', 'image/jpg', 'image/gif', 'image/svg+xml']; -var uploadsController = {}; +var uploadsController = module.exports; uploadsController.uploadCategoryPicture = function (req, res, next) { var uploadedFile = req.files.files[0]; @@ -23,11 +23,7 @@ uploadsController.uploadCategoryPicture = function (req, res, next) { try { params = JSON.parse(req.body.params); } catch (e) { - fs.unlink(uploadedFile.path, function (err) { - if (err) { - winston.error(err); - } - }); + deleteTempFile(uploadedFile.path); return next(e); } @@ -43,11 +39,7 @@ uploadsController.uploadFavicon = function (req, res, next) { if (validateUpload(req, res, next, uploadedFile, allowedTypes)) { file.saveFileToLocal('favicon.ico', 'system', uploadedFile.path, function (err, image) { - fs.unlink(uploadedFile.path, function (err) { - if (err) { - winston.error(err); - } - }); + deleteTempFile(uploadedFile.path); if (err) { return next(err); } @@ -80,11 +72,7 @@ uploadsController.uploadTouchIcon = function (req, res, next) { }), ], next); }, function (err) { - fs.unlink(uploadedFile.path, function (err) { - if (err) { - winston.error(err); - } - }); + deleteTempFile(uploadedFile.path); if (err) { return next(err); @@ -108,18 +96,19 @@ uploadsController.uploadSound = function (req, res, next) { return next(Error('[[error:invalid-data]]')); } - file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function (err) { + async.waterfall([ + function (next) { + file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, next); + }, + function (uploadedSound, next) { + meta.sounds.build(next); + }, + ], function (err) { + deleteTempFile(uploadedFile.path); if (err) { return next(err); } - - meta.sounds.build(function (err) { - if (err) { - return next(err); - } - - res.json([{}]); - }); + res.json([{}]); }); }; @@ -142,12 +131,7 @@ function upload(name, req, res, next) { function validateUpload(req, res, next, uploadedFile, allowedTypes) { if (allowedTypes.indexOf(uploadedFile.type) === -1) { - fs.unlink(uploadedFile.path, function (err) { - if (err) { - winston.error(err); - } - }); - + deleteTempFile(uploadedFile.path); res.json({ error: '[[error:invalid-image-type, ' + allowedTypes.join(', ') + ']]' }); return false; } @@ -156,24 +140,27 @@ function validateUpload(req, res, next, uploadedFile, allowedTypes) { } function uploadImage(filename, folder, uploadedFile, req, res, next) { - function done(err, image) { - fs.unlink(uploadedFile.path, function (err) { - if (err) { - winston.error(err); + async.waterfall([ + function (next) { + if (plugins.hasListeners('filter:uploadImage')) { + plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.user.uid }, next); + } else { + file.saveFileToLocal(filename, folder, uploadedFile.path, next); } - }); + }, + ], function (err, image) { + deleteTempFile(uploadedFile.path); if (err) { return next(err); } - res.json([{ name: uploadedFile.name, url: image.url.startsWith('http') ? image.url : nconf.get('relative_path') + image.url }]); - } - - if (plugins.hasListeners('filter:uploadImage')) { - plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.user.uid }, done); - } else { - file.saveFileToLocal(filename, folder, uploadedFile.path, done); - } + }); } -module.exports = uploadsController; +function deleteTempFile(path) { + fs.unlink(path, function (err) { + if (err) { + winston.error(err); + } + }); +} \ No newline at end of file diff --git a/src/meta/sounds.js b/src/meta/sounds.js index 3de45b388c..e7bb7c3599 100644 --- a/src/meta/sounds.js +++ b/src/meta/sounds.js @@ -89,7 +89,9 @@ module.exports = function (Meta) { }, ], cb); }, - ], callback); + ], function (err) { + callback(err); + }); }); }; diff --git a/test/files/test.wav b/test/files/test.wav new file mode 100644 index 0000000000000000000000000000000000000000..da967a0ae0504beecf2372db00be52700550f6b4 GIT binary patch literal 26124 zcmeI436!2imA|Vy-JQL&bUJ$oSxCr6!WII^umuD~A+m#vVgMD1qA)VJD}ti}1A>B} zAR>wbC}0Ey2_TznAwX6VvhV3mI!mY1NvCU`rz-DshhhH5|C}>t&Y9D1pYHGb>aD7K z@9+NZty}ee$;VDU@x-!ALOA=_vrf2V+Lilu4XY^?xwxySLmX&qV2zu zT!QB9xak=C7W4bbIlbQRej%giQ%b+c)p~q>Um7Zyy@HvWn6YusoN3}#e_|{Rzr7U!!wPV;Fcc9(&y>x3A+NZuv z+!Gfwb77HpnXqe&ThXCinm;nT_KS~WD?*nv*Dmz%v*YqA zAO>Z@vFfM{QT0`Q?B|^7q&(Zz!D}}od~NY(_F?vkQkmNL`U^^7D3V+#1)SbNkRE45eRHsE_OEvn=%~ z51o^4+MuuHw7v;gp>ArjKF(p*4Y4C*oufVSHpX|;96kA583u*gxESr%#loH!{Nguckw&73vZu?a@*PB@h#TYSW%DxYQd@l zKRsy8ypazDck9tz9{9N)9YwWg7@o#%jjK{WeQrFci$2bgqQAGt&Y?Fb^#RM;q;uv& zDK_k4ta+e4G1Qp)YM0Sff+k|unBq>!9aA6uk#+AL^r4t2TfRGQ1DMExN_eybgg3<{ z;M1J+cV64HxohZ;M&r>pg!OTD@`d_z#4_*639*#Vjoht^OF^vx9+1{+^_P0~l* z!am7oE91)vZv}Oye-F51jCKtJ((CH@23o(7crPAeC`P_l4|OuWtHT(y9gENF(DW79 zunPZFBput)S3Np{X+4(9gHGtLJpEKdOjdBW5;VMwX=BOw?iEJycSQ2>3Uu|J{K|PK zuN{ub>s6=3OZ5?Nd9Vwti#%*UF}BT^Fo!nrsU6yhm3nSv{!QeQU1(qr^NpDH2>XG_ zAZ93K&K?-Aczpw#bYR}McB3|ejl)Wa-r2m%0X=Qgt zo;GT0u~ry&%3BZL^o4T!5_`_G0Zz&X^P0SMta(Jg>c7SDX*7Q^#dt@oGkdA0z7Q8< z)_kLF{lX!5pfbE0pNh-l^TcKqZ3iU`%!k^!4f*TiT)bsY>zHJ@zPom4qw-zX%+-E2 zrx^FL+1SeKSV*}q$3;0k{4K**{Tb6gtcZU@{&R6qI1GzMgjMl1b7wvU(CY%4BNA6vqo4lLf0+aCq|b7v$j$Py;Gz5!bJwy# z;m9;*Vf+)aOOn=dt(q9>6-I}*L(B z*TmuBWKi56*+$n6Mk^7_yS|3;&G6>H&KW?0L3 z*FNjuQetL3vp1022PKx*;XmWqT-+TFvNWGQ{nfdNp8`wm6SI!=uEx$@WpgSF2_>LAxRxh}u-n)Rsq%a$$3F;B|@SH;|?JmITp3Y$!ra@nNN*7xndoa_c+artDs-z zihgojuIucxeb2gFi8Sl-yuXmwuJ_iu^2^WaV~&)EUfaOeS1~tddf%9kE%vGBz3YRu zLJzoHMl{!f{dT-1H`QGkUcMI_YY=sJUgtA^sgtXOS0k3nMSbip){1xs=oz=JPh#cg zjGuYYJ(+fCcM0<9xwkdx=RM;bT*Dlond9CS?YDwz|*SkVbU!Bvs(DllQcU{c* zmn1vA@BU96j7j;E_ln+ekK7E_m9$ycY)8*6Db}pvm2EG7#8{4I++woB^)#=?^=NMX zk-yf;o03oUfmj=#ddaA97VXx*+G4C)uc(!{h=;MKZTecDS%(@&))CHcU-@aJC4#=U z)wRyJ6bt>P94~#Zp8jgPb)|F3ZT*<*j7ZBetX~5jeTm9_6F=+Hx@DYaj+w*dOFe$t zjef?xm|9P`MyPk5vuAM4tD`coChf+2>SV6g204`1pKZ*Xxo58OpiJW!q) zW;1!%T#@;%?IB@f4dy=1JZh~gCY_0O>jLeR+s@%!>Zd%hP=R@h)LfnHrK$y5$M&GsQYBNAY2rF5dK0FXE4p{#cwE{yAI`ZVpqz7g*&E4tK{ZK50`fo+{pLjO|{Z7X(Uk~@e#hvkrcwT%Y-a=hHEPN_ljV-59jg1PwO;Rqy z#&O|+u$G<6sbNYS6u%b#5noNlim72xzy8_#>vFq=J@L{FMKUr4~~aY zd(_c>CI}o6F9?@`{WfeoIb1>8CGmKyTNB6P(JtgEcOSIy?p{M~5>Rbx*SCoAIJ}I`J_U4*n=SMqG^wYhijdV+J#N zR~$fIYKVuy#@*QZDzm?Wuf75DcgJhv6>#yl;Vp9RR+!x@{ws)`g_nL2{))G5BHnuw zqf6jb7h+;wybs=Wq_UU-`i;c(J@MA~2RyF*X8xZMqlaV5nQ;ufzCPXqwvUEopk7Ob z`V2Xz5^k>}mK;~TVCg}7o3dsKMucxvHuXRiXV^Pj$e)^Uj1+=CcLyB_gqgnWj_Y{{d`K7i2za&0pl9|@?dM8!mz3f#+aaU?ZpH+E*$bhX&iNQUc1Y`g1U44$4roCm_EgfrOx&H<@! z5{1_@Zb0}~aG!wxuO}}&Nsh1@Sps*hAHIsWE++D}A>#pLJqg1$;kz-g_u6m=79JLE zqWu@l)(^WsfF51og7G&q?V(G-+dV-U8Gbl-&%ie^Vb#p*NMB>sxF16jYC;DvjvO^kRIRNK=&3a$F!l@Zup9d0D9&Wt@+ zcVfJaC_RWf_mWLtVXd2p9v=daF04|^nc+BWJTm-acni*%*Y`)GvGB@TV>WrSE1|jcoC?dL)Pv_gnDXPiL5#J@Xq*YvhHi~3_LP{eE1Ss z?l#t>8F0>%vWvr&%r!YY3ajUnw@)JPP6G2auyP?WIsotVg(H!QLEpM7+d|FWNS65< zedOf|xN#2H+)D1~!`M6E$-_j}0pR&%m@gBUsGo#I98Jfg@0sC`$oOGAm;G@kxPBR4s}8e}H56+fQRQ43sNn$5Apdt`Dg~*ngI7MLb7oX zXV9<|CMm(Gi1RtLGl3h zwr|6#yT~Go;y&c0$*}7~`0Pa@{flutdEmi#Fmrtj8~0(K*$8?ype0t;|DJK0a~jcS zJ`C~{dOZHCBC_5h$E*QyE64fh`UZAPWUed8zyb81BX87_8>}t1fqzG`hqaAQC0-(z z)*;JN+@&y}f_OQGHLxe%d6As+47Lu3x1WQnU5TJauzNQ7xgTv=&jyg*Mor&^HDDfV z#Edu)?;c0QS@CbdqfM}RHGY|km7`hrFD6s;!C$Z7r~Ap7tGF`(E?MijLt2Z4=DHz<-xvsmOU~j-*i?~;VUDjgz!9`C>jf1XuygL!C&gJwU zz>IZxYaW=m_t+Qd)>RvbWOw5m!P0ZG0rVUUI`U@?-hPSIY8^B7qrHD>?~b+=aAZmH z`zE|Hirh1v9O3@YUG7F+oNRHhK+wWuk6~yOg?5M`>-PFKV!99?*3<>M$ zUqyBtgeAR*%XavG1G&a1+szCcnAzt{8|dr#Vh4KlA~&=Hb)VZdgP~_Xjwnq!9f+Nt zzAVAMr69Q$IqplVkYjDMf%)dZ<+X6ECu0Y}pbkWWx>cd0`_p8YSkBXI_Lb_Wl8Y-FV$jFjj=W3*zuRYIP!Q6A< zqCOi5cYA@b`}j5Z!l!@MO6u1Y$?8^xO>5A036bilaUXbD$&8+qxa!FZ&%Q?DSM6WK z>6vq;oMSkQUyF123j4Ht_wkH4t6)QCJv(SfW*g**jx{?o{1W_${B)hJ7UXfW|<4_ z8<5k9&+g5YvjQK@LpR51lQo*p|9lE3C%4kRfq6Ys6?6IGWnERovi|VwYZ+Rt;jd32 z_6J+zc_ng}pub!*MoSrM|6X`To0c-har)-~uo_HuH2)g2#?~vWea7>3oOB#Zb+wAt zFHpODnG=dJjCh#yhlj)5a65JG9nEJnFDJ5^!}yX8IiuI*f--Ej+ zgu+R_I1pBz&Ns}VGfgz8;w|*dYar_WFo2f|JLG5xNtoOOfhbkCIbc5t zMi`&V$XK_b*Il6X1ROYm_PfK=DN~-$`OzHkxHUWkiqp}njCEu-Y^zKBScVq|lgC|; zdSkh{_`B?bwZ&@S8{roA+Yf_mHGA-rVfHl6G(U}AFA^aKai;kgNS0$+JI=oDNSXL> zc6R14^X?98HBROeM`uvyUy6t4!otfq)AZSZdR@V3pLNI2@y|2ZW<8=F=GG(etY-)2 z_eaSe)?M2eX$*ajv!}t}dv&~!z1l6D4NfC!u3?Y#8W{ba^`;kk1=do(C)A$oH-K!~ z9c|mQCTaO==I5o<8^qN9e`=@v=dxv|&ORO&?oDG}~VlSuld1+dcOWDO= z4)c#nIm~?XHvVvpI~;wl1%01rUkA4y#j2q&=NL{{T+659S?lI%w6Q)gR>u$%AEa{j zDTq9g)9$tWcFZxvjB)j;cp_SzP3?UGXKr`H-$2~{2t1auw)y;TC;JHZY9Hi8;Uh43 zI5Fg|;i0s@m*io_-?8UQ9;W45C+YJ~@lb=(*u(aM<;C5P4$=`Pdrz zRS@-g$o}Yc0zHgx&ylZWP52qv!+d8fc*b%I`TAklco;ILp{=X6Y&eYFqEEG^@X zW22d)7e3MMmze*@aQJCr;$y_fcf!xXKH}A{r}ao&J>NKz+~;!vcTel7c4i=H0d|fj zkDVOOLhEYIA*NFS--6Chr*;2xT^$9icjJv%d_PnWe{8?DdGnXVI~K+(ODXIpoYh5kxTKbZN?WcAq&7oH;C zjfMQ2xd-txF2%xF{Np}k74|KswoxzZq@EyZt^OC*$+tmrUvMz@JJ)lZZM#qQ3{u^! z&#d|MfmoWO)Z04OI30<8@4`YkXYF(VGCzV(T#w!%bL2X&Ju8VjB)shq( zd8)E={p<7cTH@_lM$9C_8d(cHJ3RpFeO~R($o*?w;2 z=P!P{QoWS30r{dlgxTba&t})+b@!+KDA&EFvAm5D8$jM~yP9u05RD?W5!vgQQ7fFs z{fqg-J%LLup3l9z zXK&(a?l+FqOIZL;i_y6}VPx#PS96~v|DDrk%HlW%P` z;dAHj30CgLe3E0Hb3g08(mbr~K4VtCIC&1>KFvA2%pI;VZxivh8wcv9PR5FH;5gSS zIbN7402zeTU)r_)Mj}i#@c7@RkpI!-L+0ztSzcjo{(9qlBAyG1=m~ms9xq* z@s}TR-P~kOb1$nLW%$|nvOaN->^k5&uHD_iq(ic`BH=9WZMUy=>k|Ae7k%%3NYA>j z^_^O|=k*N2=pD^^VZCEsTR=XueiS2j=L4yBccIBLT9siRzni1%evAtfL zD9=16btN_ao!|3Dck>JJt+Cdbp4uUQeeWf{`JACIEFHmJzn2;~S6!KGx0dOS9R0Eu zPu0=t`INTj??bqw_8TgG*TQc*D4}1&&D2)iuUE zqMwXk^PTg!zIZlK4yM{|44QMyYt}^d#F;kx+(13#n>@BA5fizpKE|(JGRM2u zvmWuOgWs)^m*VR;c04mSYx>cVlNijEW zl%$=m2VP?4uXgAk+vHVVv;0=|TBQ3uA-`?bBk87Wao0}m&~{(Vd0Foo?&rLw&c>LW z^IXq;yfVzU{>THrOJtk)my^-)2ZXliBldbg?`Qw|WFY^0xXe?}1w7gD$-L)+K0))xf3xK`GdI%a zcL0r&d_QOPP6=Oq=(;Fo&aaKGx2~buXm0S1`$u`LY-85Gg*pXg@MK8beD)0OjtnqBVRPx3> z`dXE`E~$mz{WPApC;9Tt7<7zWFz3o8<*2`M)x-X_IKO^ZR@PBO@^dV)GIrHJ?}auc z+r=hVt(hP?>n;CnYR*gAQ^bL~iMVpbz;XVFiED^s^rQL66<(e6upDw8 zr90lSg>$0s)PMPF6);>kMpPff(;$VAmFW>Q*th%`y&oZ>bapocSE8a6+yo?clozH%S z9`Y#1q3HPUa`XKxGw-pK<2_%s!tvQZawxZZPv0uTIn~vB+N3{n{2O=X2>tB8uQW#N zZ$8Yi>HM}A7|LqdD`a?{L zae1uLGhQNPKCzFoGEc-Sra1JTmvi}>b;`AYn0d+1LiZLMvrWn=d{^jM;HEq^$@g)@xh-&c1oXU;Aox#y@ja{fZbC6USt1a!gq-<+<~G&Y^x@ zTBpBlYq@gnD?Y1}xgqOitFfSzyoQTho>9yEKLGMRqv&7EraarsoBGB}+5UK?h9o8P zKzxlB=gjwV9iujSOs*=om_K8dv34I&@E}djpQ2ykZ$G!@PG+F^XZh;t`H*dVBeah9Rt+`O(td5TLo^35Q=lP5K zzKW-sWy@Obdrv*R3OV^x(KGk8t>~NcSb=YgErrfmZ!OMTbexxSsAXYy##D?7-17|f zv%hxbpNw5$w>Fz!z4B;B6j*0F9GzS99skYK!Y1eQ$`T9vi}j)S_xia{@z?Q%j*jqi>#MLQ>yr6Z z^tVmB9QEFjMO*83vyjsCsrhwpe6!T7R}1dilA|HVSH?QeTHsM|#2NDamhpMk)_-$s zx7ek`mivxx`Q&_Rxp!N>Us$L`g-=_L%)!MI|LdD^YAx%3;(d$$dG;1P_tyWtva^Ln>))emp6|W#iWyqk|DL&uQG36(=-$%4 zw~t%e-~XLD{=e;h|8f6Ytr@HLv-`aowcz{SJO9@{|M&Uxe+`K?d)g3aL!b?THU!!b jXhWb4fi?u%5NJc74S_ZU+7M_%pbddG1lka2L*U;5LICc( literal 0 HcmV?d00001 diff --git a/test/uploads.js b/test/uploads.js index a4fe95c5df..6e20fcffcb 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -173,6 +173,23 @@ describe('Upload Controllers', function () { }); }); + it('should fail to upload invalid file type', function (done) { + helpers.uploadFile(nconf.get('url') + '/api/admin/category/uploadpicture', path.join(__dirname, '../test/files/503.html'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token, function (err, res, body) { + assert.ifError(err); + console.log(body); + assert.equal(body.error, '[[error:invalid-image-type, image/png, image/jpeg, image/pjpeg, image/jpg, image/gif, image/svg+xml]]'); + done(); + }); + }); + + it('should fail to upload category image with invalid json params', function (done) { + helpers.uploadFile(nconf.get('url') + '/api/admin/category/uploadpicture', path.join(__dirname, '../test/files/test.png'), { params: 'invalid json' }, jar, csrf_token, function (err, res, body) { + assert.ifError(err); + assert.equal(body.error, 'Unexpected token i in JSON at position 0'); + done(); + }); + }); + it('should upload category image', function (done) { helpers.uploadFile(nconf.get('url') + '/api/admin/category/uploadpicture', path.join(__dirname, '../test/files/test.png'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token, function (err, res, body) { assert.ifError(err); @@ -183,6 +200,26 @@ describe('Upload Controllers', function () { }); }); + + it('should fail to upload invalid sound file', function (done) { + helpers.uploadFile(nconf.get('url') + '/api/admin/upload/sound', path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 500); + assert.equal(body.error, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should upload sound file', function (done) { + helpers.uploadFile(nconf.get('url') + '/api/admin/upload/sound', path.join(__dirname, '../test/files/test.wav'), { }, jar, csrf_token, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + console.log(body); + done(); + }); + }); + it('should upload favicon', function (done) { helpers.uploadFile(nconf.get('url') + '/api/admin/uploadfavicon', path.join(__dirname, '../test/files/favicon.ico'), {}, jar, csrf_token, function (err, res, body) { assert.ifError(err); From 90018a7aabbbd8c851b6fe0789469d32ca9e2743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 15:35:14 -0400 Subject: [PATCH 046/263] fix lint --- src/controllers/admin/uploads.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 1eeaba8041..4e9cac610e 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -163,4 +163,4 @@ function deleteTempFile(path) { winston.error(err); } }); -} \ No newline at end of file +} From 873801e9f316856b1745a0b797a914835625e747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 15:51:19 -0400 Subject: [PATCH 047/263] fix test on nodev4 remove console.logs --- public/language/en-GB/error.json | 1 + src/controllers/admin/uploads.js | 2 +- test/uploads.js | 4 +--- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 89222f622f..8d106b6a79 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -1,5 +1,6 @@ { "invalid-data": "Invalid Data", + "invalid-json": "Invalid JSON", "not-logged-in": "You don't seem to be logged in.", "account-locked": "Your account has been locked temporarily", diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index 4e9cac610e..b23f67ea0a 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -24,7 +24,7 @@ uploadsController.uploadCategoryPicture = function (req, res, next) { params = JSON.parse(req.body.params); } catch (e) { deleteTempFile(uploadedFile.path); - return next(e); + return next(new Error('[[error:invalid-json]]')); } if (validateUpload(req, res, next, uploadedFile, allowedImageTypes)) { diff --git a/test/uploads.js b/test/uploads.js index 6e20fcffcb..57b7a67ed0 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -176,7 +176,6 @@ describe('Upload Controllers', function () { it('should fail to upload invalid file type', function (done) { helpers.uploadFile(nconf.get('url') + '/api/admin/category/uploadpicture', path.join(__dirname, '../test/files/503.html'), { params: JSON.stringify({ cid: cid }) }, jar, csrf_token, function (err, res, body) { assert.ifError(err); - console.log(body); assert.equal(body.error, '[[error:invalid-image-type, image/png, image/jpeg, image/pjpeg, image/jpg, image/gif, image/svg+xml]]'); done(); }); @@ -185,7 +184,7 @@ describe('Upload Controllers', function () { it('should fail to upload category image with invalid json params', function (done) { helpers.uploadFile(nconf.get('url') + '/api/admin/category/uploadpicture', path.join(__dirname, '../test/files/test.png'), { params: 'invalid json' }, jar, csrf_token, function (err, res, body) { assert.ifError(err); - assert.equal(body.error, 'Unexpected token i in JSON at position 0'); + assert.equal(body.error, '[[error:invalid-json]]'); done(); }); }); @@ -215,7 +214,6 @@ describe('Upload Controllers', function () { assert.ifError(err); assert.equal(res.statusCode, 200); assert(body); - console.log(body); done(); }); }); From 73f31640d787fb2400134e61ba608f65c58a5bb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 16:27:44 -0400 Subject: [PATCH 048/263] more tests --- src/controllers/admin/blacklist.js | 25 +++++++++++++------------ src/controllers/globalmods.js | 24 ++++++++++++++---------- test/controllers-admin.js | 17 +++++++++++++++++ test/uploads.js | 18 ++++++++++++++++++ 4 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/controllers/admin/blacklist.js b/src/controllers/admin/blacklist.js index 7ce4edda49..fb92f377b5 100644 --- a/src/controllers/admin/blacklist.js +++ b/src/controllers/admin/blacklist.js @@ -1,19 +1,20 @@ 'use strict'; +var async = require('async'); var meta = require('../../meta'); -var blacklistController = {}; +var blacklistController = module.exports; blacklistController.get = function (req, res, next) { - meta.blacklist.get(function (err, rules) { - if (err) { - return next(err); - } - res.render('admin/manage/ip-blacklist', { - rules: rules, - title: '[[pages:ip-blacklist]]', - }); - }); + async.waterfall([ + function (next) { + meta.blacklist.get(next); + }, + function (rules) { + res.render('admin/manage/ip-blacklist', { + rules: rules, + title: '[[pages:ip-blacklist]]', + }); + }, + ], next); }; - -module.exports = blacklistController; diff --git a/src/controllers/globalmods.js b/src/controllers/globalmods.js index 793c33653e..1cae97a4e2 100644 --- a/src/controllers/globalmods.js +++ b/src/controllers/globalmods.js @@ -1,18 +1,22 @@ 'use strict'; +var async = require('async'); + var user = require('../user'); var adminBlacklistController = require('./admin/blacklist'); -var globalModsController = {}; +var globalModsController = module.exports; globalModsController.ipBlacklist = function (req, res, next) { - user.isAdminOrGlobalMod(req.uid, function (err, isAdminOrGlobalMod) { - if (err || !isAdminOrGlobalMod) { - return next(err); - } - - adminBlacklistController.get(req, res, next); - }); + async.waterfall([ + function (next) { + user.isAdminOrGlobalMod(req.uid, next); + }, + function (isAdminOrGlobalMod, next) { + if (!isAdminOrGlobalMod) { + return next(); + } + adminBlacklistController.get(req, res, next); + }, + ], next); }; - -module.exports = globalModsController; diff --git a/test/controllers-admin.js b/test/controllers-admin.js index 086d54e7a8..83d3b94fad 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -414,6 +414,23 @@ describe('Admin Controllers', function () { }); }); + it('/ip-blacklist should 404 for regular user', function (done) { + request(nconf.get('url') + '/api/ip-blacklist', { json: true }, function (err, res, body) { + assert.ifError(err); + assert(body); + assert.equal(res.statusCode, 404); + done(); + }); + }); + + it('should load /ip-blacklist', function (done) { + request(nconf.get('url') + '/api/ip-blacklist', { jar: jar, json: true }, function (err, res, body) { + assert.ifError(err); + assert(body); + done(); + }); + }); + it('should load /admin/appearance/themes', function (done) { request(nconf.get('url') + '/api/admin/appearance/themes', { jar: jar, json: true }, function (err, res, body) { assert.ifError(err); diff --git a/test/uploads.js b/test/uploads.js index 57b7a67ed0..5acc480e4c 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -218,6 +218,24 @@ describe('Upload Controllers', function () { }); }); + it('should upload default avatar', function (done) { + helpers.uploadFile(nconf.get('url') + '/api/admin/uploadDefaultAvatar', path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert.equal(body[0].url, nconf.get('relative_path') + '/assets/uploads/system/avatar-default.png'); + done(); + }); + }); + + it('should upload og image', function (done) { + helpers.uploadFile(nconf.get('url') + '/api/admin/uploadOgImage', path.join(__dirname, '../test/files/test.png'), { }, jar, csrf_token, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert.equal(body[0].url, nconf.get('relative_path') + '/assets/uploads/system/og-image.png'); + done(); + }); + }); + it('should upload favicon', function (done) { helpers.uploadFile(nconf.get('url') + '/api/admin/uploadfavicon', path.join(__dirname, '../test/files/favicon.ico'), {}, jar, csrf_token, function (err, res, body) { assert.ifError(err); From bd6aca7d9473f37adeadc7df0fd5be6b5358e872 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 10 May 2017 15:05:54 -0600 Subject: [PATCH 049/263] More friendly error message if module loading fails --- nodebb | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/nodebb b/nodebb index 50bcc65fee..2b71185d4e 100755 --- a/nodebb +++ b/nodebb @@ -23,12 +23,30 @@ try { } } -require('colors'); -var args = require('minimist')(process.argv.slice(2)); -var request = require('request'); -var semver = require('semver'); -var prompt = require('prompt'); -var async = require('async'); +var minimist; +var request; +var semver; +var prompt; +var async; + +try { + require('colors'); + minimist = require('minimist'); + request = require('request'); + semver = require('semver'); + prompt = require('prompt'); + async = require('async'); +} catch (e) { + process.stdout.write( + '\x1b[31mNodeBB could not be initialised because there was an error while loading dependencies.\n' + + 'Please run "\x1b[33mnpm install --production\x1b[31m" and try again.\x1b[0m\n\n' + + 'For more information, please see: https://docs.nodebb.org/en/latest/installing/os.html\n\n' + ); + + throw e; +} + +var args = minimist(process.argv.slice(2)); var loaderPath = path.join(__dirname, 'loader.js'); var appPath = path.join(__dirname, 'app.js'); From 608a7d120c3af6004524a049948b447f520e4b44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 21:02:36 -0400 Subject: [PATCH 050/263] error handler tests --- src/controllers/errors.js | 4 +- src/middleware/index.js | 4 +- test/controllers.js | 137 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/controllers/errors.js b/src/controllers/errors.js index 0e61b557cc..99614f9e87 100644 --- a/src/controllers/errors.js +++ b/src/controllers/errors.js @@ -16,14 +16,14 @@ exports.handleURIErrors = function (err, req, res, next) { res.redirect(nconf.get('relative_path') + cidMatch[0]); } else { winston.warn('[controller] Bad request: ' + req.path); - if (res.locals.isAPI) { + if (req.path.startsWith(nconf.get('relative_path') + '/api')) { res.status(400).json({ error: '[[global:400.title]]', }); } else { var middleware = require('../middleware'); middleware.buildHeader(req, res, function () { - res.render('400', { error: validator.escape(String(err.message)) }); + res.status(400).render('400', { error: validator.escape(String(err.message)) }); }); } } diff --git a/src/middleware/index.js b/src/middleware/index.js index de0bd6797e..d4cdb0bfd2 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -122,9 +122,9 @@ middleware.pageView = function (req, res, next) { middleware.pluginHooks = function (req, res, next) { async.each(plugins.loadedHooks['filter:router.page'] || [], function (hookObj, next) { hookObj.method(req, res, next); - }, function () { + }, function (err) { // If it got here, then none of the subscribed hooks did anything, or there were no hooks - next(); + next(err); }); }; diff --git a/test/controllers.js b/test/controllers.js index 3e1000e39f..5c0f143758 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -1069,6 +1069,143 @@ describe('Controllers', function () { }); }); + describe('handle errors', function () { + var plugins = require('../src/plugins'); + after(function (done) { + plugins.loadedHooks['filter:router.page'] = undefined; + done(); + }); + + it('should handle topic malformed uri', function (done) { + request(nconf.get('url') + '/topic/1/a%AFc', function (err, res, body) { + assert.ifError(err); + assert(body); + done(); + }); + }); + + it('should handle category malformed uri', function (done) { + request(nconf.get('url') + '/category/1/a%AFc', function (err, res, body) { + assert.ifError(err); + assert(body); + done(); + }); + }); + + it('should handle malformed uri ', function (done) { + request(nconf.get('url') + '/user/a%AFc', function (err, res, body) { + assert.ifError(err); + assert(body); + assert.equal(res.statusCode, 400); + done(); + }); + }); + + it('should handle malformed uri in api', function (done) { + request(nconf.get('url') + '/api/user/a%AFc', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 400); + assert.equal(body.error, '[[global:400.title]]'); + done(); + }); + }); + + it('should handle CSRF error', function (done) { + plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; + plugins.loadedHooks['filter:router.page'].push({ + method: function (req, res, next) { + var err = new Error('csrf-error'); + err.code = 'EBADCSRFTOKEN'; + next(err); + }, + }); + + request(nconf.get('url') + '/users', { }, function (err, res, body) { + plugins.loadedHooks['filter:router.page'] = []; + assert.ifError(err); + assert.equal(res.statusCode, 403); + done(); + }); + }); + + it('should handle black-list error', function (done) { + plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; + plugins.loadedHooks['filter:router.page'].push({ + method: function (req, res, next) { + var err = new Error('blacklist error message'); + err.code = 'blacklisted-ip'; + next(err); + }, + }); + + request(nconf.get('url') + '/users', { }, function (err, res, body) { + plugins.loadedHooks['filter:router.page'] = []; + assert.ifError(err); + assert.equal(res.statusCode, 403); + assert.equal(body, 'blacklist error message'); + done(); + }); + }); + + it('should handle page redirect through error', function (done) { + plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; + plugins.loadedHooks['filter:router.page'].push({ + method: function (req, res, next) { + var err = new Error('redirect'); + err.status = 302; + err.path = '/popular'; + plugins.loadedHooks['filter:router.page'] = []; + next(err); + }, + }); + + request(nconf.get('url') + '/users', { }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + done(); + }); + }); + + it('should handle api page redirect through error', function (done) { + plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; + plugins.loadedHooks['filter:router.page'].push({ + method: function (req, res, next) { + var err = new Error('redirect'); + err.status = 308; + err.path = '/api/popular'; + plugins.loadedHooks['filter:router.page'] = []; + next(err); + }, + }); + + request(nconf.get('url') + '/api/users', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 308); + assert(body, '/api/popular'); + done(); + }); + }); + + it('should handle error page', function (done) { + plugins.loadedHooks['filter:router.page'] = plugins.loadedHooks['filter:router.page'] || []; + plugins.loadedHooks['filter:router.page'].push({ + method: function (req, res, next) { + var err = new Error('regular error'); + next(err); + }, + }); + + request(nconf.get('url') + '/users', function (err, res, body) { + plugins.loadedHooks['filter:router.page'] = []; + assert.ifError(err); + assert.equal(res.statusCode, 500); + assert(body); + done(); + }); + }); + }); + after(function (done) { var analytics = require('../src/analytics'); From e252c5195d6f6006006efa530327896e883e8ace Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 21:46:11 -0400 Subject: [PATCH 051/263] coverPhoto tests --- src/coverPhoto.js | 5 ++--- test/coverPhoto.js | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 test/coverPhoto.js diff --git a/src/coverPhoto.js b/src/coverPhoto.js index 024ae48f20..59fc897ac4 100644 --- a/src/coverPhoto.js +++ b/src/coverPhoto.js @@ -1,9 +1,10 @@ 'use strict'; -var coverPhoto = {}; + var meta = require('./meta'); var nconf = require('nconf'); +var coverPhoto = module.exports; coverPhoto.getDefaultGroupCover = function (groupName) { return getCover('groups', groupName); @@ -28,5 +29,3 @@ function getCover(type, id) { return nconf.get('relative_path') + '/assets/images/cover-default.png'; } - -module.exports = coverPhoto; diff --git a/test/coverPhoto.js b/test/coverPhoto.js new file mode 100644 index 0000000000..cb80e72938 --- /dev/null +++ b/test/coverPhoto.js @@ -0,0 +1,24 @@ +'use strict'; + +var assert = require('assert'); +var async = require('async'); + +var db = require('./mocks/databasemock'); +var coverPhoto = require('../src/coverPhoto'); +var meta = require('../src/meta'); + +describe('coverPhoto', function () { + it('should get default group cover', function (done) { + meta.config['groups:defaultCovers'] = 'image1.png,image2.png'; + var result = coverPhoto.getDefaultGroupCover('registered-users'); + assert.equal(result, 'image2.png'); + done(); + }); + + it('should get default default profile cover', function (done) { + meta.config['profile:defaultCovers'] = ' image1.png ,image2.png '; + var result = coverPhoto.getDefaultProfileCover(1); + assert.equal(result, 'image2.png'); + done(); + }); +}); From 052afb10f7be5c4b80d63a6f430e361c16de74f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Wed, 10 May 2017 22:05:23 -0400 Subject: [PATCH 052/263] show output if upload fails --- test/helpers/index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/helpers/index.js b/test/helpers/index.js index 2c37f85500..2b999246f7 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -104,7 +104,10 @@ helpers.uploadFile = function (uploadEndPoint, filePath, body, jar, csrf_token, if (err) { return callback(err); } - callback(err, res, body); + if (res.statusCode !== 200) { + console.log(body); + } + callback(null, res, body); }); }; From 544faa7861952ba59a8a4b2ebf511531d3c3c21a Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 11 May 2017 09:34:16 -0400 Subject: [PATCH 053/263] Adding Croatian to languages, closes #5668. Hvala ti! --- .tx/config | 65 +++++++++ public/language/hr/admin/admin.json | 7 + public/language/hr/admin/advanced/cache.json | 11 ++ .../language/hr/admin/advanced/database.json | 36 +++++ public/language/hr/admin/advanced/errors.json | 14 ++ public/language/hr/admin/advanced/events.json | 6 + public/language/hr/admin/advanced/logs.json | 7 + .../hr/admin/appearance/customise.json | 9 ++ .../language/hr/admin/appearance/skins.json | 9 ++ .../language/hr/admin/appearance/themes.json | 11 ++ .../language/hr/admin/development/info.json | 16 +++ .../language/hr/admin/development/logger.json | 12 ++ public/language/hr/admin/extend/plugins.json | 47 ++++++ public/language/hr/admin/extend/rewards.json | 17 +++ public/language/hr/admin/extend/widgets.json | 19 +++ .../language/hr/admin/general/dashboard.json | 64 +++++++++ .../language/hr/admin/general/homepage.json | 7 + .../language/hr/admin/general/languages.json | 5 + .../language/hr/admin/general/navigation.json | 27 ++++ public/language/hr/admin/general/social.json | 5 + public/language/hr/admin/general/sounds.json | 9 ++ .../language/hr/admin/manage/categories.json | 68 +++++++++ public/language/hr/admin/manage/groups.json | 35 +++++ .../hr/admin/manage/ip-blacklist.json | 15 ++ .../hr/admin/manage/registration.json | 20 +++ public/language/hr/admin/manage/tags.json | 18 +++ public/language/hr/admin/manage/users.json | 91 ++++++++++++ public/language/hr/admin/menu.json | 74 ++++++++++ .../language/hr/admin/settings/advanced.json | 19 +++ public/language/hr/admin/settings/chat.json | 9 ++ .../language/hr/admin/settings/cookies.json | 11 ++ public/language/hr/admin/settings/email.json | 25 ++++ .../language/hr/admin/settings/general.json | 32 +++++ public/language/hr/admin/settings/group.json | 12 ++ public/language/hr/admin/settings/guest.json | 8 ++ .../hr/admin/settings/notifications.json | 5 + .../hr/admin/settings/pagination.json | 9 ++ public/language/hr/admin/settings/post.json | 46 ++++++ .../hr/admin/settings/reputation.json | 9 ++ .../language/hr/admin/settings/sockets.json | 6 + public/language/hr/admin/settings/tags.json | 12 ++ .../language/hr/admin/settings/uploads.json | 28 ++++ public/language/hr/admin/settings/user.json | 60 ++++++++ .../hr/admin/settings/web-crawler.json | 10 ++ public/language/hr/category.json | 20 +++ public/language/hr/email.json | 40 ++++++ public/language/hr/error.json | 132 +++++++++++++++++ public/language/hr/flags.json | 60 ++++++++ public/language/hr/global.json | 107 ++++++++++++++ public/language/hr/groups.json | 58 ++++++++ public/language/hr/language.json | 5 + public/language/hr/login.json | 12 ++ public/language/hr/modules.json | 49 +++++++ public/language/hr/notifications.json | 48 +++++++ public/language/hr/pages.json | 52 +++++++ public/language/hr/recent.json | 19 +++ public/language/hr/register.json | 23 +++ public/language/hr/reset_password.json | 17 +++ public/language/hr/search.json | 42 ++++++ public/language/hr/success.json | 6 + public/language/hr/tags.json | 7 + public/language/hr/topic.json | 119 +++++++++++++++ public/language/hr/unread.json | 13 ++ public/language/hr/uploads.json | 6 + public/language/hr/user.json | 136 ++++++++++++++++++ public/language/hr/users.json | 21 +++ 66 files changed, 2017 insertions(+) create mode 100644 public/language/hr/admin/admin.json create mode 100644 public/language/hr/admin/advanced/cache.json create mode 100644 public/language/hr/admin/advanced/database.json create mode 100644 public/language/hr/admin/advanced/errors.json create mode 100644 public/language/hr/admin/advanced/events.json create mode 100644 public/language/hr/admin/advanced/logs.json create mode 100644 public/language/hr/admin/appearance/customise.json create mode 100644 public/language/hr/admin/appearance/skins.json create mode 100644 public/language/hr/admin/appearance/themes.json create mode 100644 public/language/hr/admin/development/info.json create mode 100644 public/language/hr/admin/development/logger.json create mode 100644 public/language/hr/admin/extend/plugins.json create mode 100644 public/language/hr/admin/extend/rewards.json create mode 100644 public/language/hr/admin/extend/widgets.json create mode 100644 public/language/hr/admin/general/dashboard.json create mode 100644 public/language/hr/admin/general/homepage.json create mode 100644 public/language/hr/admin/general/languages.json create mode 100644 public/language/hr/admin/general/navigation.json create mode 100644 public/language/hr/admin/general/social.json create mode 100644 public/language/hr/admin/general/sounds.json create mode 100644 public/language/hr/admin/manage/categories.json create mode 100644 public/language/hr/admin/manage/groups.json create mode 100644 public/language/hr/admin/manage/ip-blacklist.json create mode 100644 public/language/hr/admin/manage/registration.json create mode 100644 public/language/hr/admin/manage/tags.json create mode 100644 public/language/hr/admin/manage/users.json create mode 100644 public/language/hr/admin/menu.json create mode 100644 public/language/hr/admin/settings/advanced.json create mode 100644 public/language/hr/admin/settings/chat.json create mode 100644 public/language/hr/admin/settings/cookies.json create mode 100644 public/language/hr/admin/settings/email.json create mode 100644 public/language/hr/admin/settings/general.json create mode 100644 public/language/hr/admin/settings/group.json create mode 100644 public/language/hr/admin/settings/guest.json create mode 100644 public/language/hr/admin/settings/notifications.json create mode 100644 public/language/hr/admin/settings/pagination.json create mode 100644 public/language/hr/admin/settings/post.json create mode 100644 public/language/hr/admin/settings/reputation.json create mode 100644 public/language/hr/admin/settings/sockets.json create mode 100644 public/language/hr/admin/settings/tags.json create mode 100644 public/language/hr/admin/settings/uploads.json create mode 100644 public/language/hr/admin/settings/user.json create mode 100644 public/language/hr/admin/settings/web-crawler.json create mode 100644 public/language/hr/category.json create mode 100644 public/language/hr/email.json create mode 100644 public/language/hr/error.json create mode 100644 public/language/hr/flags.json create mode 100644 public/language/hr/global.json create mode 100644 public/language/hr/groups.json create mode 100644 public/language/hr/language.json create mode 100644 public/language/hr/login.json create mode 100644 public/language/hr/modules.json create mode 100644 public/language/hr/notifications.json create mode 100644 public/language/hr/pages.json create mode 100644 public/language/hr/recent.json create mode 100644 public/language/hr/register.json create mode 100644 public/language/hr/reset_password.json create mode 100644 public/language/hr/search.json create mode 100644 public/language/hr/success.json create mode 100644 public/language/hr/tags.json create mode 100644 public/language/hr/topic.json create mode 100644 public/language/hr/unread.json create mode 100644 public/language/hr/uploads.json create mode 100644 public/language/hr/user.json create mode 100644 public/language/hr/users.json diff --git a/.tx/config b/.tx/config index d88ed808e1..f2e0db3200 100644 --- a/.tx/config +++ b/.tx/config @@ -21,6 +21,7 @@ trans.fi = public/language/fi/category.json trans.fr = public/language/fr/category.json trans.gl = public/language/gl/category.json trans.he = public/language/he/category.json +trans.hr = public/language/hr/category.json trans.hu = public/language/hu/category.json trans.id = public/language/id/category.json trans.it = public/language/it/category.json @@ -69,6 +70,7 @@ trans.fi = public/language/fi/login.json trans.fr = public/language/fr/login.json trans.gl = public/language/gl/login.json trans.he = public/language/he/login.json +trans.hr = public/language/hr/login.json trans.hu = public/language/hu/login.json trans.id = public/language/id/login.json trans.it = public/language/it/login.json @@ -117,6 +119,7 @@ trans.fi = public/language/fi/recent.json trans.fr = public/language/fr/recent.json trans.gl = public/language/gl/recent.json trans.he = public/language/he/recent.json +trans.hr = public/language/hr/recent.json trans.hu = public/language/hu/recent.json trans.id = public/language/id/recent.json trans.it = public/language/it/recent.json @@ -165,6 +168,7 @@ trans.fi = public/language/fi/unread.json trans.fr = public/language/fr/unread.json trans.gl = public/language/gl/unread.json trans.he = public/language/he/unread.json +trans.hr = public/language/hr/unread.json trans.hu = public/language/hu/unread.json trans.id = public/language/id/unread.json trans.it = public/language/it/unread.json @@ -213,6 +217,7 @@ trans.fi = public/language/fi/modules.json trans.fr = public/language/fr/modules.json trans.gl = public/language/gl/modules.json trans.he = public/language/he/modules.json +trans.hr = public/language/hr/modules.json trans.hu = public/language/hu/modules.json trans.id = public/language/id/modules.json trans.it = public/language/it/modules.json @@ -261,6 +266,7 @@ trans.fi = public/language/fi/register.json trans.fr = public/language/fr/register.json trans.gl = public/language/gl/register.json trans.he = public/language/he/register.json +trans.hr = public/language/hr/register.json trans.hu = public/language/hu/register.json trans.id = public/language/id/register.json trans.it = public/language/it/register.json @@ -309,6 +315,7 @@ trans.fi = public/language/fi/user.json trans.fr = public/language/fr/user.json trans.gl = public/language/gl/user.json trans.he = public/language/he/user.json +trans.hr = public/language/hr/user.json trans.hu = public/language/hu/user.json trans.id = public/language/id/user.json trans.it = public/language/it/user.json @@ -357,6 +364,7 @@ trans.fi = public/language/fi/global.json trans.fr = public/language/fr/global.json trans.gl = public/language/gl/global.json trans.he = public/language/he/global.json +trans.hr = public/language/hr/global.json trans.hu = public/language/hu/global.json trans.id = public/language/id/global.json trans.it = public/language/it/global.json @@ -405,6 +413,7 @@ trans.fi = public/language/fi/notifications.json trans.fr = public/language/fr/notifications.json trans.gl = public/language/gl/notifications.json trans.he = public/language/he/notifications.json +trans.hr = public/language/hr/notifications.json trans.hu = public/language/hu/notifications.json trans.id = public/language/id/notifications.json trans.it = public/language/it/notifications.json @@ -453,6 +462,7 @@ trans.fi = public/language/fi/reset_password.json trans.fr = public/language/fr/reset_password.json trans.gl = public/language/gl/reset_password.json trans.he = public/language/he/reset_password.json +trans.hr = public/language/hr/reset_password.json trans.hu = public/language/hu/reset_password.json trans.id = public/language/id/reset_password.json trans.it = public/language/it/reset_password.json @@ -501,6 +511,7 @@ trans.fi = public/language/fi/users.json trans.fr = public/language/fr/users.json trans.gl = public/language/gl/users.json trans.he = public/language/he/users.json +trans.hr = public/language/hr/users.json trans.hu = public/language/hu/users.json trans.id = public/language/id/users.json trans.it = public/language/it/users.json @@ -549,6 +560,7 @@ trans.fi = public/language/fi/language.json trans.fr = public/language/fr/language.json trans.gl = public/language/gl/language.json trans.he = public/language/he/language.json +trans.hr = public/language/hr/language.json trans.hu = public/language/hu/language.json trans.id = public/language/id/language.json trans.it = public/language/it/language.json @@ -597,6 +609,7 @@ trans.fi = public/language/fi/pages.json trans.fr = public/language/fr/pages.json trans.gl = public/language/gl/pages.json trans.he = public/language/he/pages.json +trans.hr = public/language/hr/pages.json trans.hu = public/language/hu/pages.json trans.id = public/language/id/pages.json trans.it = public/language/it/pages.json @@ -645,6 +658,7 @@ trans.fi = public/language/fi/topic.json trans.fr = public/language/fr/topic.json trans.gl = public/language/gl/topic.json trans.he = public/language/he/topic.json +trans.hr = public/language/hr/topic.json trans.hu = public/language/hu/topic.json trans.id = public/language/id/topic.json trans.it = public/language/it/topic.json @@ -693,6 +707,7 @@ trans.fi = public/language/fi/success.json trans.fr = public/language/fr/success.json trans.gl = public/language/gl/success.json trans.he = public/language/he/success.json +trans.hr = public/language/hr/success.json trans.hu = public/language/hu/success.json trans.id = public/language/id/success.json trans.it = public/language/it/success.json @@ -741,6 +756,7 @@ trans.fi = public/language/fi/error.json trans.fr = public/language/fr/error.json trans.gl = public/language/gl/error.json trans.he = public/language/he/error.json +trans.hr = public/language/hr/error.json trans.hu = public/language/hu/error.json trans.id = public/language/id/error.json trans.it = public/language/it/error.json @@ -789,6 +805,7 @@ trans.fi = public/language/fi/flags.json trans.fr = public/language/fr/flags.json trans.gl = public/language/gl/flags.json trans.he = public/language/he/flags.json +trans.hr = public/language/hr/flags.json trans.hu = public/language/hu/flags.json trans.id = public/language/id/flags.json trans.it = public/language/it/flags.json @@ -836,6 +853,7 @@ trans.fi = public/language/fi/tags.json trans.fr = public/language/fr/tags.json trans.gl = public/language/gl/tags.json trans.he = public/language/he/tags.json +trans.hr = public/language/hr/tags.json trans.hu = public/language/hu/tags.json trans.id = public/language/id/tags.json trans.it = public/language/it/tags.json @@ -884,6 +902,7 @@ trans.fi = public/language/fi/email.json trans.fr = public/language/fr/email.json trans.gl = public/language/gl/email.json trans.he = public/language/he/email.json +trans.hr = public/language/hr/email.json trans.hu = public/language/hu/email.json trans.id = public/language/id/email.json trans.it = public/language/it/email.json @@ -932,6 +951,7 @@ trans.fi = public/language/fi/search.json trans.fr = public/language/fr/search.json trans.gl = public/language/gl/search.json trans.he = public/language/he/search.json +trans.hr = public/language/hr/search.json trans.hu = public/language/hu/search.json trans.id = public/language/id/search.json trans.it = public/language/it/search.json @@ -980,6 +1000,7 @@ trans.fi = public/language/fi/groups.json trans.fr = public/language/fr/groups.json trans.gl = public/language/gl/groups.json trans.he = public/language/he/groups.json +trans.hr = public/language/hr/groups.json trans.hu = public/language/hu/groups.json trans.id = public/language/id/groups.json trans.it = public/language/it/groups.json @@ -1028,6 +1049,7 @@ trans.fi = public/language/fi/uploads.json trans.fr = public/language/fr/uploads.json trans.gl = public/language/gl/uploads.json trans.he = public/language/he/uploads.json +trans.hr = public/language/hr/uploads.json trans.hu = public/language/hu/uploads.json trans.id = public/language/id/uploads.json trans.it = public/language/it/uploads.json @@ -1076,6 +1098,7 @@ trans.fi = public/language/fi/admin/admin.json trans.fr = public/language/fr/admin/admin.json trans.gl = public/language/gl/admin/admin.json trans.he = public/language/he/admin/admin.json +trans.hr = public/language/hr/admin/admin.json trans.hu = public/language/hu/admin/admin.json trans.id = public/language/id/admin/admin.json trans.it = public/language/it/admin/admin.json @@ -1124,6 +1147,7 @@ trans.fi = public/language/fi/admin/menu.json trans.fr = public/language/fr/admin/menu.json trans.gl = public/language/gl/admin/menu.json trans.he = public/language/he/admin/menu.json +trans.hr = public/language/hr/admin/menu.json trans.hu = public/language/hu/admin/menu.json trans.id = public/language/id/admin/menu.json trans.it = public/language/it/admin/menu.json @@ -1172,6 +1196,7 @@ trans.fi = public/language/fi/admin/advanced/cache.json trans.fr = public/language/fr/admin/advanced/cache.json trans.gl = public/language/gl/admin/advanced/cache.json trans.he = public/language/he/admin/advanced/cache.json +trans.hr = public/language/hr/admin/advanced/cache.json trans.hu = public/language/hu/admin/advanced/cache.json trans.id = public/language/id/admin/advanced/cache.json trans.it = public/language/it/admin/advanced/cache.json @@ -1220,6 +1245,7 @@ trans.fi = public/language/fi/admin/advanced/database.json trans.fr = public/language/fr/admin/advanced/database.json trans.gl = public/language/gl/admin/advanced/database.json trans.he = public/language/he/admin/advanced/database.json +trans.hr = public/language/hr/admin/advanced/database.json trans.hu = public/language/hu/admin/advanced/database.json trans.id = public/language/id/admin/advanced/database.json trans.it = public/language/it/admin/advanced/database.json @@ -1268,6 +1294,7 @@ trans.fi = public/language/fi/admin/advanced/errors.json trans.fr = public/language/fr/admin/advanced/errors.json trans.gl = public/language/gl/admin/advanced/errors.json trans.he = public/language/he/admin/advanced/errors.json +trans.hr = public/language/hr/admin/advanced/errors.json trans.hu = public/language/hu/admin/advanced/errors.json trans.id = public/language/id/admin/advanced/errors.json trans.it = public/language/it/admin/advanced/errors.json @@ -1316,6 +1343,7 @@ trans.fi = public/language/fi/admin/advanced/events.json trans.fr = public/language/fr/admin/advanced/events.json trans.gl = public/language/gl/admin/advanced/events.json trans.he = public/language/he/admin/advanced/events.json +trans.hr = public/language/hr/admin/advanced/events.json trans.hu = public/language/hu/admin/advanced/events.json trans.id = public/language/id/admin/advanced/events.json trans.it = public/language/it/admin/advanced/events.json @@ -1364,6 +1392,7 @@ trans.fi = public/language/fi/admin/advanced/logs.json trans.fr = public/language/fr/admin/advanced/logs.json trans.gl = public/language/gl/admin/advanced/logs.json trans.he = public/language/he/admin/advanced/logs.json +trans.hr = public/language/hr/admin/advanced/logs.json trans.hu = public/language/hu/admin/advanced/logs.json trans.id = public/language/id/admin/advanced/logs.json trans.it = public/language/it/admin/advanced/logs.json @@ -1412,6 +1441,7 @@ trans.fi = public/language/fi/admin/appearance/customise.json trans.fr = public/language/fr/admin/appearance/customise.json trans.gl = public/language/gl/admin/appearance/customise.json trans.he = public/language/he/admin/appearance/customise.json +trans.hr = public/language/hr/admin/appearance/customise.json trans.hu = public/language/hu/admin/appearance/customise.json trans.id = public/language/id/admin/appearance/customise.json trans.it = public/language/it/admin/appearance/customise.json @@ -1460,6 +1490,7 @@ trans.fi = public/language/fi/admin/appearance/skins.json trans.fr = public/language/fr/admin/appearance/skins.json trans.gl = public/language/gl/admin/appearance/skins.json trans.he = public/language/he/admin/appearance/skins.json +trans.hr = public/language/hr/admin/appearance/skins.json trans.hu = public/language/hu/admin/appearance/skins.json trans.id = public/language/id/admin/appearance/skins.json trans.it = public/language/it/admin/appearance/skins.json @@ -1508,6 +1539,7 @@ trans.fi = public/language/fi/admin/appearance/themes.json trans.fr = public/language/fr/admin/appearance/themes.json trans.gl = public/language/gl/admin/appearance/themes.json trans.he = public/language/he/admin/appearance/themes.json +trans.hr = public/language/hr/admin/appearance/themes.json trans.hu = public/language/hu/admin/appearance/themes.json trans.id = public/language/id/admin/appearance/themes.json trans.it = public/language/it/admin/appearance/themes.json @@ -1556,6 +1588,7 @@ trans.fi = public/language/fi/admin/development/info.json trans.fr = public/language/fr/admin/development/info.json trans.gl = public/language/gl/admin/development/info.json trans.he = public/language/he/admin/development/info.json +trans.hr = public/language/hr/admin/development/info.json trans.hu = public/language/hu/admin/development/info.json trans.id = public/language/id/admin/development/info.json trans.it = public/language/it/admin/development/info.json @@ -1604,6 +1637,7 @@ trans.fi = public/language/fi/admin/development/logger.json trans.fr = public/language/fr/admin/development/logger.json trans.gl = public/language/gl/admin/development/logger.json trans.he = public/language/he/admin/development/logger.json +trans.hr = public/language/hr/admin/development/logger.json trans.hu = public/language/hu/admin/development/logger.json trans.id = public/language/id/admin/development/logger.json trans.it = public/language/it/admin/development/logger.json @@ -1652,6 +1686,7 @@ trans.fi = public/language/fi/admin/extend/plugins.json trans.fr = public/language/fr/admin/extend/plugins.json trans.gl = public/language/gl/admin/extend/plugins.json trans.he = public/language/he/admin/extend/plugins.json +trans.hr = public/language/hr/admin/extend/plugins.json trans.hu = public/language/hu/admin/extend/plugins.json trans.id = public/language/id/admin/extend/plugins.json trans.it = public/language/it/admin/extend/plugins.json @@ -1700,6 +1735,7 @@ trans.fi = public/language/fi/admin/extend/rewards.json trans.fr = public/language/fr/admin/extend/rewards.json trans.gl = public/language/gl/admin/extend/rewards.json trans.he = public/language/he/admin/extend/rewards.json +trans.hr = public/language/hr/admin/extend/rewards.json trans.hu = public/language/hu/admin/extend/rewards.json trans.id = public/language/id/admin/extend/rewards.json trans.it = public/language/it/admin/extend/rewards.json @@ -1748,6 +1784,7 @@ trans.fi = public/language/fi/admin/extend/widgets.json trans.fr = public/language/fr/admin/extend/widgets.json trans.gl = public/language/gl/admin/extend/widgets.json trans.he = public/language/he/admin/extend/widgets.json +trans.hr = public/language/hr/admin/extend/widgets.json trans.hu = public/language/hu/admin/extend/widgets.json trans.id = public/language/id/admin/extend/widgets.json trans.it = public/language/it/admin/extend/widgets.json @@ -1796,6 +1833,7 @@ trans.fi = public/language/fi/admin/general/dashboard.json trans.fr = public/language/fr/admin/general/dashboard.json trans.gl = public/language/gl/admin/general/dashboard.json trans.he = public/language/he/admin/general/dashboard.json +trans.hr = public/language/hr/admin/general/dashboard.json trans.hu = public/language/hu/admin/general/dashboard.json trans.id = public/language/id/admin/general/dashboard.json trans.it = public/language/it/admin/general/dashboard.json @@ -1844,6 +1882,7 @@ trans.fi = public/language/fi/admin/general/homepage.json trans.fr = public/language/fr/admin/general/homepage.json trans.gl = public/language/gl/admin/general/homepage.json trans.he = public/language/he/admin/general/homepage.json +trans.hr = public/language/hr/admin/general/homepage.json trans.hu = public/language/hu/admin/general/homepage.json trans.id = public/language/id/admin/general/homepage.json trans.it = public/language/it/admin/general/homepage.json @@ -1892,6 +1931,7 @@ trans.fi = public/language/fi/admin/general/languages.json trans.fr = public/language/fr/admin/general/languages.json trans.gl = public/language/gl/admin/general/languages.json trans.he = public/language/he/admin/general/languages.json +trans.hr = public/language/hr/admin/general/languages.json trans.hu = public/language/hu/admin/general/languages.json trans.id = public/language/id/admin/general/languages.json trans.it = public/language/it/admin/general/languages.json @@ -1940,6 +1980,7 @@ trans.fi = public/language/fi/admin/general/navigation.json trans.fr = public/language/fr/admin/general/navigation.json trans.gl = public/language/gl/admin/general/navigation.json trans.he = public/language/he/admin/general/navigation.json +trans.hr = public/language/hr/admin/general/navigation.json trans.hu = public/language/hu/admin/general/navigation.json trans.id = public/language/id/admin/general/navigation.json trans.it = public/language/it/admin/general/navigation.json @@ -1988,6 +2029,7 @@ trans.fi = public/language/fi/admin/general/social.json trans.fr = public/language/fr/admin/general/social.json trans.gl = public/language/gl/admin/general/social.json trans.he = public/language/he/admin/general/social.json +trans.hr = public/language/hr/admin/general/social.json trans.hu = public/language/hu/admin/general/social.json trans.id = public/language/id/admin/general/social.json trans.it = public/language/it/admin/general/social.json @@ -2036,6 +2078,7 @@ trans.fi = public/language/fi/admin/general/sounds.json trans.fr = public/language/fr/admin/general/sounds.json trans.gl = public/language/gl/admin/general/sounds.json trans.he = public/language/he/admin/general/sounds.json +trans.hr = public/language/hr/admin/general/sounds.json trans.hu = public/language/hu/admin/general/sounds.json trans.id = public/language/id/admin/general/sounds.json trans.it = public/language/it/admin/general/sounds.json @@ -2084,6 +2127,7 @@ trans.fi = public/language/fi/admin/manage/categories.json trans.fr = public/language/fr/admin/manage/categories.json trans.gl = public/language/gl/admin/manage/categories.json trans.he = public/language/he/admin/manage/categories.json +trans.hr = public/language/hr/admin/manage/categories.json trans.hu = public/language/hu/admin/manage/categories.json trans.id = public/language/id/admin/manage/categories.json trans.it = public/language/it/admin/manage/categories.json @@ -2132,6 +2176,7 @@ trans.fi = public/language/fi/admin/manage/groups.json trans.fr = public/language/fr/admin/manage/groups.json trans.gl = public/language/gl/admin/manage/groups.json trans.he = public/language/he/admin/manage/groups.json +trans.hr = public/language/hr/admin/manage/groups.json trans.hu = public/language/hu/admin/manage/groups.json trans.id = public/language/id/admin/manage/groups.json trans.it = public/language/it/admin/manage/groups.json @@ -2180,6 +2225,7 @@ trans.fi = public/language/fi/admin/manage/ip-blacklist.json trans.fr = public/language/fr/admin/manage/ip-blacklist.json trans.gl = public/language/gl/admin/manage/ip-blacklist.json trans.he = public/language/he/admin/manage/ip-blacklist.json +trans.hr = public/language/hr/admin/manage/ip-blacklist.json trans.hu = public/language/hu/admin/manage/ip-blacklist.json trans.id = public/language/id/admin/manage/ip-blacklist.json trans.it = public/language/it/admin/manage/ip-blacklist.json @@ -2228,6 +2274,7 @@ trans.fi = public/language/fi/admin/manage/registration.json trans.fr = public/language/fr/admin/manage/registration.json trans.gl = public/language/gl/admin/manage/registration.json trans.he = public/language/he/admin/manage/registration.json +trans.hr = public/language/hr/admin/manage/registration.json trans.hu = public/language/hu/admin/manage/registration.json trans.id = public/language/id/admin/manage/registration.json trans.it = public/language/it/admin/manage/registration.json @@ -2276,6 +2323,7 @@ trans.fi = public/language/fi/admin/manage/tags.json trans.fr = public/language/fr/admin/manage/tags.json trans.gl = public/language/gl/admin/manage/tags.json trans.he = public/language/he/admin/manage/tags.json +trans.hr = public/language/hr/admin/manage/tags.json trans.hu = public/language/hu/admin/manage/tags.json trans.id = public/language/id/admin/manage/tags.json trans.it = public/language/it/admin/manage/tags.json @@ -2324,6 +2372,7 @@ trans.fi = public/language/fi/admin/manage/users.json trans.fr = public/language/fr/admin/manage/users.json trans.gl = public/language/gl/admin/manage/users.json trans.he = public/language/he/admin/manage/users.json +trans.hr = public/language/hr/admin/manage/users.json trans.hu = public/language/hu/admin/manage/users.json trans.id = public/language/id/admin/manage/users.json trans.it = public/language/it/admin/manage/users.json @@ -2372,6 +2421,7 @@ trans.fi = public/language/fi/admin/settings/advanced.json trans.fr = public/language/fr/admin/settings/advanced.json trans.gl = public/language/gl/admin/settings/advanced.json trans.he = public/language/he/admin/settings/advanced.json +trans.hr = public/language/hr/admin/settings/advanced.json trans.hu = public/language/hu/admin/settings/advanced.json trans.id = public/language/id/admin/settings/advanced.json trans.it = public/language/it/admin/settings/advanced.json @@ -2420,6 +2470,7 @@ trans.fi = public/language/fi/admin/settings/cookies.json trans.fr = public/language/fr/admin/settings/cookies.json trans.gl = public/language/gl/admin/settings/cookies.json trans.he = public/language/he/admin/settings/cookies.json +trans.hr = public/language/hr/admin/settings/cookies.json trans.hu = public/language/hu/admin/settings/cookies.json trans.id = public/language/id/admin/settings/cookies.json trans.it = public/language/it/admin/settings/cookies.json @@ -2468,6 +2519,7 @@ trans.fi = public/language/fi/admin/settings/general.json trans.fr = public/language/fr/admin/settings/general.json trans.gl = public/language/gl/admin/settings/general.json trans.he = public/language/he/admin/settings/general.json +trans.hr = public/language/hr/admin/settings/general.json trans.hu = public/language/hu/admin/settings/general.json trans.id = public/language/id/admin/settings/general.json trans.it = public/language/it/admin/settings/general.json @@ -2516,6 +2568,7 @@ trans.fi = public/language/fi/admin/settings/guest.json trans.fr = public/language/fr/admin/settings/guest.json trans.gl = public/language/gl/admin/settings/guest.json trans.he = public/language/he/admin/settings/guest.json +trans.hr = public/language/hr/admin/settings/guest.json trans.hu = public/language/hu/admin/settings/guest.json trans.id = public/language/id/admin/settings/guest.json trans.it = public/language/it/admin/settings/guest.json @@ -2564,6 +2617,7 @@ trans.fi = public/language/fi/admin/settings/pagination.json trans.fr = public/language/fr/admin/settings/pagination.json trans.gl = public/language/gl/admin/settings/pagination.json trans.he = public/language/he/admin/settings/pagination.json +trans.hr = public/language/hr/admin/settings/pagination.json trans.hu = public/language/hu/admin/settings/pagination.json trans.id = public/language/id/admin/settings/pagination.json trans.it = public/language/it/admin/settings/pagination.json @@ -2612,6 +2666,7 @@ trans.fi = public/language/fi/admin/settings/reputation.json trans.fr = public/language/fr/admin/settings/reputation.json trans.gl = public/language/gl/admin/settings/reputation.json trans.he = public/language/he/admin/settings/reputation.json +trans.hr = public/language/hr/admin/settings/reputation.json trans.hu = public/language/hu/admin/settings/reputation.json trans.id = public/language/id/admin/settings/reputation.json trans.it = public/language/it/admin/settings/reputation.json @@ -2660,6 +2715,7 @@ trans.fi = public/language/fi/admin/settings/tags.json trans.fr = public/language/fr/admin/settings/tags.json trans.gl = public/language/gl/admin/settings/tags.json trans.he = public/language/he/admin/settings/tags.json +trans.hr = public/language/hr/admin/settings/tags.json trans.hu = public/language/hu/admin/settings/tags.json trans.id = public/language/id/admin/settings/tags.json trans.it = public/language/it/admin/settings/tags.json @@ -2708,6 +2764,7 @@ trans.fi = public/language/fi/admin/settings/user.json trans.fr = public/language/fr/admin/settings/user.json trans.gl = public/language/gl/admin/settings/user.json trans.he = public/language/he/admin/settings/user.json +trans.hr = public/language/hr/admin/settings/user.json trans.hu = public/language/hu/admin/settings/user.json trans.id = public/language/id/admin/settings/user.json trans.it = public/language/it/admin/settings/user.json @@ -2756,6 +2813,7 @@ trans.fi = public/language/fi/admin/settings/chat.json trans.fr = public/language/fr/admin/settings/chat.json trans.gl = public/language/gl/admin/settings/chat.json trans.he = public/language/he/admin/settings/chat.json +trans.hr = public/language/hr/admin/settings/chat.json trans.hu = public/language/hu/admin/settings/chat.json trans.id = public/language/id/admin/settings/chat.json trans.it = public/language/it/admin/settings/chat.json @@ -2804,6 +2862,7 @@ trans.fi = public/language/fi/admin/settings/email.json trans.fr = public/language/fr/admin/settings/email.json trans.gl = public/language/gl/admin/settings/email.json trans.he = public/language/he/admin/settings/email.json +trans.hr = public/language/hr/admin/settings/email.json trans.hu = public/language/hu/admin/settings/email.json trans.id = public/language/id/admin/settings/email.json trans.it = public/language/it/admin/settings/email.json @@ -2852,6 +2911,7 @@ trans.fi = public/language/fi/admin/settings/group.json trans.fr = public/language/fr/admin/settings/group.json trans.gl = public/language/gl/admin/settings/group.json trans.he = public/language/he/admin/settings/group.json +trans.hr = public/language/hr/admin/settings/group.json trans.hu = public/language/hu/admin/settings/group.json trans.id = public/language/id/admin/settings/group.json trans.it = public/language/it/admin/settings/group.json @@ -2900,6 +2960,7 @@ trans.fi = public/language/fi/admin/settings/notifications.json trans.fr = public/language/fr/admin/settings/notifications.json trans.gl = public/language/gl/admin/settings/notifications.json trans.he = public/language/he/admin/settings/notifications.json +trans.hr = public/language/hr/admin/settings/notifications.json trans.hu = public/language/hu/admin/settings/notifications.json trans.id = public/language/id/admin/settings/notifications.json trans.it = public/language/it/admin/settings/notifications.json @@ -2948,6 +3009,7 @@ trans.fi = public/language/fi/admin/settings/post.json trans.fr = public/language/fr/admin/settings/post.json trans.gl = public/language/gl/admin/settings/post.json trans.he = public/language/he/admin/settings/post.json +trans.hr = public/language/hr/admin/settings/post.json trans.hu = public/language/hu/admin/settings/post.json trans.id = public/language/id/admin/settings/post.json trans.it = public/language/it/admin/settings/post.json @@ -2996,6 +3058,7 @@ trans.fi = public/language/fi/admin/settings/sockets.json trans.fr = public/language/fr/admin/settings/sockets.json trans.gl = public/language/gl/admin/settings/sockets.json trans.he = public/language/he/admin/settings/sockets.json +trans.hr = public/language/hr/admin/settings/sockets.json trans.hu = public/language/hu/admin/settings/sockets.json trans.id = public/language/id/admin/settings/sockets.json trans.it = public/language/it/admin/settings/sockets.json @@ -3044,6 +3107,7 @@ trans.fi = public/language/fi/admin/settings/uploads.json trans.fr = public/language/fr/admin/settings/uploads.json trans.gl = public/language/gl/admin/settings/uploads.json trans.he = public/language/he/admin/settings/uploads.json +trans.hr = public/language/hr/admin/settings/uploads.json trans.hu = public/language/hu/admin/settings/uploads.json trans.id = public/language/id/admin/settings/uploads.json trans.it = public/language/it/admin/settings/uploads.json @@ -3092,6 +3156,7 @@ trans.fi = public/language/fi/admin/settings/web-crawler.json trans.fr = public/language/fr/admin/settings/web-crawler.json trans.gl = public/language/gl/admin/settings/web-crawler.json trans.he = public/language/he/admin/settings/web-crawler.json +trans.hr = public/language/hr/admin/settings/web-crawler.json trans.hu = public/language/hu/admin/settings/web-crawler.json trans.id = public/language/id/admin/settings/web-crawler.json trans.it = public/language/it/admin/settings/web-crawler.json diff --git a/public/language/hr/admin/admin.json b/public/language/hr/admin/admin.json new file mode 100644 index 0000000000..fef743caf9 --- /dev/null +++ b/public/language/hr/admin/admin.json @@ -0,0 +1,7 @@ +{ + "alert.confirm-reload": "Sigurni ste da želite ponovno pokrenuti NodeBB?", + "alert.confirm-restart": "Sigurni ste da želite ponovno pokrenuti NodeBB?", + + "acp-title": "%1 | NodeBB Administratorska kontrolna ploča", + "settings-header-contents": "Sadržaj" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/cache.json b/public/language/hr/admin/advanced/cache.json new file mode 100644 index 0000000000..937db239bf --- /dev/null +++ b/public/language/hr/admin/advanced/cache.json @@ -0,0 +1,11 @@ +{ + "post-cache": "Objava predmemorija", + "posts-in-cache": "Objave u predmemoriji", + "average-post-size": "Prosječna veličina objave", + "length-to-max": "Dužina / Maksimum", + "percent-full": "%1% Puno", + "post-cache-size": "Veličina predmemorije objave", + "items-in-cache": "Artikli u predmemoriji", + "control-panel": "Kontrolna ploča", + "update-settings": "Obnovi postavke predmemorije" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/database.json b/public/language/hr/admin/advanced/database.json new file mode 100644 index 0000000000..e79a4b6192 --- /dev/null +++ b/public/language/hr/admin/advanced/database.json @@ -0,0 +1,36 @@ +{ + "x-b": "%1 b", + "x-mb": "%1 mb", + "x-gb": "%1 gb", + "uptime-seconds": "Na mreži u sekundama", + "uptime-days": "Na mreži u danima", + + "mongo": "Mongo", + "mongo.version": "Verzija MongoDB", + "mongo.storage-engine": "Način pohrane", + "mongo.collections": "Kolekcije", + "mongo.objects": "Objekti", + "mongo.avg-object-size": "Avg. Object Size", + "mongo.data-size": "Veličina datoteke", + "mongo.storage-size": "Veličina pohrane", + "mongo.index-size": "Veličina indexa", + "mongo.file-size": "Veličina datoteke", + "mongo.resident-memory": "Rezidentna memorija", + "mongo.virtual-memory": "Virtualna memorija", + "mongo.mapped-memory": "Mapirana memorija", + "mongo.raw-info": "MongoDB sirove informacije", + + "redis": "Redis", + "redis.version": "Redis verzija", + "redis.connected-clients": "Spojeni klijenti", + "redis.connected-slaves": "Povezani robovi", + "redis.blocked-clients": "Blokirani klijenti", + "redis.used-memory": "Iskorištena memorija", + "redis.memory-frag-ratio": "Omjer fragmentiranja memorije", + "redis.total-connections-recieved": "Ukupno primljeni veza", + "redis.total-commands-processed": "Ukupne prcesirane komande", + "redis.iops": "Instante operacije po sekundi", + "redis.keyspace-hits": "Keyspace Hits", + "redis.keyspace-misses": "Keyspace Misses", + "redis.raw-info": "Redis sirova informacija" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/errors.json b/public/language/hr/admin/advanced/errors.json new file mode 100644 index 0000000000..04e9e32776 --- /dev/null +++ b/public/language/hr/admin/advanced/errors.json @@ -0,0 +1,14 @@ +{ + "figure-x": "Figura %1", + "error-events-per-day": "%1 događaja po danu", + "error.404": "404 Nije pronađeno", + "error.503": "503 Usluga nedostupna", + "manage-error-log": "Upravljaj dnevnikom grešaka", + "export-error-log": "Izvedi dnevnik grešaka (CSV)", + "clear-error-log": "Očisti dnevnik grešaka", + "route": "Putanja", + "count": "Zbroj", + "no-routes-not-found": "Huura! Nema 404 grešaka!", + "clear404-confirm": "Sigurni ste da želite očistiti 404 greše iz dnevnika?", + "clear404-success": "\"404 Nije pronađen\" greške očišćene" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/events.json b/public/language/hr/admin/advanced/events.json new file mode 100644 index 0000000000..a113f933e4 --- /dev/null +++ b/public/language/hr/admin/advanced/events.json @@ -0,0 +1,6 @@ +{ + "events": "Događanja", + "no-events": "Nema događaja", + "control-panel": "Kontrolna ploča događanja", + "delete-events": "Obriši događanja" +} \ No newline at end of file diff --git a/public/language/hr/admin/advanced/logs.json b/public/language/hr/admin/advanced/logs.json new file mode 100644 index 0000000000..8d87365ce6 --- /dev/null +++ b/public/language/hr/admin/advanced/logs.json @@ -0,0 +1,7 @@ +{ + "logs": "Dnevnik", + "control-panel": "Dnevnik kontrolne ploče", + "reload": "Učitaj dnevnik ponovno", + "clear": "Očisti dnevnik ", + "clear-success": "Dnevnik čist!" +} \ No newline at end of file diff --git a/public/language/hr/admin/appearance/customise.json b/public/language/hr/admin/appearance/customise.json new file mode 100644 index 0000000000..0809a8e20c --- /dev/null +++ b/public/language/hr/admin/appearance/customise.json @@ -0,0 +1,9 @@ +{ + "custom-css": "CSS po narudžbi", + "custom-css.description": "Unesi CSS deklaracije koje će biti upisane poslije svih stilova.", + "custom-css.enable": "Omogući dodatni CSS", + + "custom-header": "Uobičajno zaglavlje", + "custom-header.description": "Unesite Vaš HTML ovdje(npr. JavaScript Meta Tags itd.)koji će biti dodani <head> sekciji marže Vašeg foruma.", + "custom-header.enable": "Omogući uobičajeno zaglavlje" +} \ No newline at end of file diff --git a/public/language/hr/admin/appearance/skins.json b/public/language/hr/admin/appearance/skins.json new file mode 100644 index 0000000000..1b68ae2ecd --- /dev/null +++ b/public/language/hr/admin/appearance/skins.json @@ -0,0 +1,9 @@ +{ + "loading": "Učitavam Izgled ...", + "homepage": "Naslovnica", + "select-skin": "Odaberi izgled", + "current-skin": "Trenutni izgled", + "skin-updated": "Izgled promijenjen", + "applied-success": "%1 izgled je primjenjen", + "revert-success": "Izgled povraćen na osnovne boje" +} \ No newline at end of file diff --git a/public/language/hr/admin/appearance/themes.json b/public/language/hr/admin/appearance/themes.json new file mode 100644 index 0000000000..593a589aca --- /dev/null +++ b/public/language/hr/admin/appearance/themes.json @@ -0,0 +1,11 @@ +{ + "checking-for-installed": "Provjeravam instalirane teme ...", + "homepage": "Naslovnica", + "select-theme": "Odaberi temu", + "current-theme": "Trenutna tema", + "no-themes": "Nisu pronađene instalirane teme", + "revert-confirm": "Sigurni ste da želite povratiti zadani NodeBB izgled ?", + "theme-changed": "Tema promijenjena", + "revert-success": "Uspješno ste vratili vaš NodeBB u početno zadanu temu.", + "restart-to-activate": "Ponovno pokrenite NodeBB da bi aktivirali izgled" +} \ No newline at end of file diff --git a/public/language/hr/admin/development/info.json b/public/language/hr/admin/development/info.json new file mode 100644 index 0000000000..0d7b3eae8e --- /dev/null +++ b/public/language/hr/admin/development/info.json @@ -0,0 +1,16 @@ +{ + "you-are-on": "Info - Vi ste %1:%2", + "host": "Domaćin", + "pid": "pid", + "nodejs": "nodejs", + "online": "Na mreži", + "git": "git", + "load": "učitaj", + "uptime": "uptime", + + "registered": "Registriran", + "sockets": "Sockets", + "guests": "Gosti", + + "info": "Info" +} \ No newline at end of file diff --git a/public/language/hr/admin/development/logger.json b/public/language/hr/admin/development/logger.json new file mode 100644 index 0000000000..e4c9b130f6 --- /dev/null +++ b/public/language/hr/admin/development/logger.json @@ -0,0 +1,12 @@ +{ + "logger-settings": "Postavke dnevnika", + "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", + "explanation": "Jednostavno potvrdite ili onemogućite postavke prijave da upalite ili ugasite prijave na brzinu.Ponovno pokretanje nije potrebno.", + "enable-http": "Dozvoli HTTP dnevnik", + "enable-socket": "Omogući socket.io dnevnik događanja ", + "file-path": "Putanja da datoteke dnevnika", + "file-path-placeholder": "/path/to/log/file.log ::: Ostavite prazno kako bi ste se mogli ulogirati u vaš terminal", + + "control-panel": "Kontrolna ploča dnevnika", + "update-settings": "Obnovi postavke dnevnika " +} \ No newline at end of file diff --git a/public/language/hr/admin/extend/plugins.json b/public/language/hr/admin/extend/plugins.json new file mode 100644 index 0000000000..31dc0981d0 --- /dev/null +++ b/public/language/hr/admin/extend/plugins.json @@ -0,0 +1,47 @@ +{ + "installed": "Instalirano", + "active": "Aktivno", + "inactive": "Neaktivan", + "out-of-date": "Izvan datuma", + "none-found": "Dodatci nisu pronađeni.", + "none-active": "Nema aktivnih dodataka", + "find-plugins": "Pronađi dodatke", + + "plugin-search": "Pretraga dodataka", + "plugin-search-placeholder": "Pretraži za dodatak ...", + "reorder-plugins": "Promjenite redosljed dodataka", + "order-active": "Posloži aktivne dodatke", + "dev-interested": "Interesira vas pisanje dodataka za NodeBB?", + "docs-info": "Punu dokumentaciju u vezi autorstva dodatka mogu biti nađeni na target=\"_blank\" href=\"https://docs.nodebb.org/en/latest/plugins/create.html\">NodeBB Docs Portal.", + + "order.description": "Određeni dodatci rade idealno kada su pokrenuti prije/poslije drugih dodataka.", + "order.explanation": "Dodatci se učitavaju u slijedu zadanom ovdje,od vrha prema dnu.", + + "plugin-item.themes": "Predlošci", + "plugin-item.deactivate": "Deaktiviraj", + "plugin-item.activate": "Aktiviraj", + "plugin-item.install": "Instaliraj", + "plugin-item.uninstall": "Deinstaliraj", + "plugin-item.settings": "Postavke", + "plugin-item.installed": "Instalirano", + "plugin-item.latest": "Najnovije", + "plugin-item.upgrade": "Nadogradnja", + "plugin-item.more-info": "Za više informacija:", + "plugin-item.unknown": "Nepoznato", + "plugin-item.unknown-explanation": "Stanje ovog dodatka se nemože utvrditi, vjerovatno zbog greške u konfiguraciji.", + + "alert.enabled": "Dodatak omogućen", + "alert.disabled": "Dodatak onemogućen", + "alert.upgraded": "Dodatak nadograđen", + "alert.installed": "Dodatak instaliran", + "alert.uninstalled": "Dodatak deinstaliran", + "alert.activate-success": "Pokrenite NodeBB za aktivaciju dodataka", + "alert.deactivate-success": "Dodatak uspjepno deaktiviran", + "alert.upgrade-success": "Ponovno pokrenite NodeBB da bi potpuno nadogradili dodatke", + "alert.install-success": "Dodatak instaliran, aktivirajte ga.", + "alert.uninstall-success": "Dodatak je uspješno deaktiviran i deinstaliran.", + "alert.suggest-error": "

NodeBB could not reach the package manager, proceed with installation of latest version?

Server returned (%1): %2
", + "alert.package-manager-unreachable": "

NodeBB nemože uspostaviti komunikaciju sa upraviteljem paketa, nadogradnja se ne preporučuje u ovom trenutku.

", + "alert.incompatible": "

Your version of NodeBB (v%1) is only cleared to upgrade to v%2 of this plugin. Please update your NodeBB if you wish to install a newer version of this plugin.

", + "alert.possibly-incompatible": "

No Compatibility Information Found

This plugin did not specify a specific version for installation given your NodeBB version. Full compatibility cannot be guaranteed, and may cause your NodeBB to no longer start properly.

In the event that NodeBB cannot boot properly:

$ ./nodebb reset plugin=\"%1\"

Continue installation of latest version of this plugin?

" +} diff --git a/public/language/hr/admin/extend/rewards.json b/public/language/hr/admin/extend/rewards.json new file mode 100644 index 0000000000..5e20c3872f --- /dev/null +++ b/public/language/hr/admin/extend/rewards.json @@ -0,0 +1,17 @@ +{ + "rewards": "Nagrade", + "condition-if-users": "Ako korisnici", + "condition-is": "ls:", + "condition-then": "Tada:", + "max-claims": "Koliko puta nagrada može biti osvojena.", + "zero-infinite": "Upišite 0 za beskonačno", + "delete": "Obriši", + "enable": "Omogući", + "disable": "onemogući", + "control-panel": "Kontrola nagrada", + "new-reward": "Nova nagrada", + + "alert.delete-success": "Uspješno obrisana nagrada", + "alert.no-inputs-found": "Ilegalna nagrada - nije pronađen unos!", + "alert.save-success": "Uspješno spremljene nagrade" +} \ No newline at end of file diff --git a/public/language/hr/admin/extend/widgets.json b/public/language/hr/admin/extend/widgets.json new file mode 100644 index 0000000000..72362aafa0 --- /dev/null +++ b/public/language/hr/admin/extend/widgets.json @@ -0,0 +1,19 @@ +{ + "available": "Dostupni dodatci", + "explanation": "Select a widget from the dropdown menu and then drag and drop it into a template's widget area on the left.", + "none-installed": "No widgets found! Activate the essential widgets plugin in the plugins control panel.", + "containers.available": "Dostupni kontejneri", + "containers.explanation": "Povucite i ispustite na vrhu bilo kojeg aktivnog widgeta", + "containers.none": "Ništa", + "container.well": "`", + "container.jumbotron": "Jumbotron", + "container.panel": "Ploča", + "container.panel-header": "Ploča zaglavlja", + "container.panel-body": "Tijelo ploče", + "container.alert": "Upozorenje", + + "alert.confirm-delete": "Sigurni ste da želite obrisati ovaj widget?", + "alert.updated": "Widgeti ažurirani", + "alert.update-success": "Uspješno promijenjeni widgeti" + +} \ No newline at end of file diff --git a/public/language/hr/admin/general/dashboard.json b/public/language/hr/admin/general/dashboard.json new file mode 100644 index 0000000000..6cbb1de650 --- /dev/null +++ b/public/language/hr/admin/general/dashboard.json @@ -0,0 +1,64 @@ +{ + "forum-traffic": "Promet foruma", + "page-views": "Broj pogleda", + "unique-visitors": "Jedinstveni posjetitelji", + "users": "Korisnici", + "posts": "Objave", + "topics": "Teme", + "page-views-last-month": "Broj pregleda prošli mjesec", + "page-views-this-month": "Pregleda ovaj mjesec", + "page-views-last-day": "Pogleda u zadnja 24 sata", + + "stats.day": "Dan", + "stats.week": "Tjedan", + "stats.month": "Mjesec", + "stats.all": "Sve vrijeme", + + "updates": "Nadogradnje", + "running-version": "Ovo je verzija NodeBB v%1.", + "keep-updated": "Uvijek se pobrinite da je Vaš NodeBB na najnovijoj verziji za najnovije sigurnosne mjere i popravke grešaka.", + "up-to-date": "

Vaš NodeBB je na najnovijoj verziji

", + "upgrade-available": "

Izašla je (v%1) nova verzija NodeBB.Razmotrite nadogradnju Vašeg foruma.

", + "prerelease-upgrade-available": "

Ovo je zastarjela verzija NodeBB. Nova verzija (v%1) je izašla.Razmotrite nadogradnju Vašeg NodeBB.

\n", + "prerelease-warning": "

Ovo je pre-release verzija NodeBB. Nenamjerne greške su moguće.

", + "running-in-development": "Forum je u razvojnom stanju. Forum bi mogao biti otvoren za napade; Molimo kontaktirajte vašeg sistemskog administratora", + + "notices": "Obavijest", + "restart-not-required": "Restart nije potreban", + "restart-required": "Potrebno je ponovno pokretanje", + "search-plugin-installed": "Dodatak pretrage instaliran", + "search-plugin-not-installed": "Dodatak pretrage nije instaliran", + "search-plugin-tooltip": "Instalirajte dodatak za pretragu sa stranice za upravljanje dodatcima da aktivirate mogućnost pretrage foruma.", + + "control-panel": "Kontrola sistema", + "reload": "Ponovno učitaj", + "restart": "Pokreni ponovno", + "restart-warning": "Ponovno pokretanje NodeBB će odbaciti sve uspostavljene veze na par sekundi.", + "maintenance-mode": "Održavanje", + "maintenance-mode-title": "Postavite mod za održavanje foruma", + "realtime-chart-updates": "Ažuriranja u stvarnom vremenu", + + "active-users": "Aktivni korisnici", + "active-users.users": "Korisnici", + "active-users.guests": "Gosti", + "active-users.total": "Ukupno", + "active-users.connections": "Veze", + + "anonymous-registered-users": "Anonimni vs Registrirani korisnici", + "anonymous": "Anomiman", + "registered": "Registriran", + + "user-presence": "Korisnik prisutan", + "on-categories": "Na listi kategorija", + "reading-posts": "Čita objave", + "browsing-topics": "Pretražuj teme", + "recent": "Nedavno", + "unread": "Nepročitano", + + "high-presence-topics": "Teme visoke prisutnosti", + + "graphs.page-views": "Pregled stranica", + "graphs.unique-visitors": "Jedninstveni posjetitelji", + "graphs.registered-users": "Registrirani korisnici", + "graphs.anonymous-users": "Anonimni korisnici" +} diff --git a/public/language/hr/admin/general/homepage.json b/public/language/hr/admin/general/homepage.json new file mode 100644 index 0000000000..a42652183e --- /dev/null +++ b/public/language/hr/admin/general/homepage.json @@ -0,0 +1,7 @@ +{ + "home-page": "Naslovnica", + "description": "Izaberi koja stranica će se prikazivati kada korisnici navigiraju u root URL Vašeg foruma", + "home-page-route": "Putanja naslovnice", + "custom-route": "Uobičajna putanja", + "allow-user-home-pages": "Dozvoli korisničke naslovnice" +} \ No newline at end of file diff --git a/public/language/hr/admin/general/languages.json b/public/language/hr/admin/general/languages.json new file mode 100644 index 0000000000..fc0d958019 --- /dev/null +++ b/public/language/hr/admin/general/languages.json @@ -0,0 +1,5 @@ +{ + "language-settings": "Postavke jezika", + "description": "Zadani jezik odlučuje o postavkama jezika za sve korisnike foruma.
.Korisnici mogu sami odabrati jezik na stranici postavki jezika.", + "default-language": "Zadani jezik" +} \ No newline at end of file diff --git a/public/language/hr/admin/general/navigation.json b/public/language/hr/admin/general/navigation.json new file mode 100644 index 0000000000..c49c255bb6 --- /dev/null +++ b/public/language/hr/admin/general/navigation.json @@ -0,0 +1,27 @@ +{ + "icon": "Ikona:", + "change-icon": "promjena", + "route": "Putanja:", + "tooltip": "Napomena:", + "text": "Tekst:", + "text-class": "Text Class: opcija", + "id": "ID: opcionalno", + + "properties": "Postavke", + "only-admins": "Prikaži samo administratorima", + "only-global-mods-and-admins": "Prikazuj samo globalnim moderatorima i administratorima", + "only-logged-in": "Prikaži samo prijavljenim korisnicima", + "open-new-window": "Otvori u novom prozoru", + + "installed-plugins-required": "Potrebni su instalirani dodatci:", + "search-plugin": "Dodatak pretrage", + + "btn.delete": "Obriši", + "btn.disable": "Onemogući", + "btn.enable": "Omogući", + + "available-menu-items": "Dostupni artikli menija", + "custom-route": "Uobičajna putanja", + "core": "jezgra", + "plugin": "dodatak" +} \ No newline at end of file diff --git a/public/language/hr/admin/general/social.json b/public/language/hr/admin/general/social.json new file mode 100644 index 0000000000..b6f1c3ee29 --- /dev/null +++ b/public/language/hr/admin/general/social.json @@ -0,0 +1,5 @@ +{ + "post-sharing": "Dijeljenje objave", + "info-plugins-additional": "Dodaci mogu dodati dodatne mreže za dijeljenje objava.", + "save-success": "Uspješno spremljene mreže za razmjenu objava!" +} \ No newline at end of file diff --git a/public/language/hr/admin/general/sounds.json b/public/language/hr/admin/general/sounds.json new file mode 100644 index 0000000000..21bf8e26ff --- /dev/null +++ b/public/language/hr/admin/general/sounds.json @@ -0,0 +1,9 @@ +{ + "notifications": "Obavijesti", + "chat-messages": "Poruke", + "play-sound": "Pokreni", + "incoming-message": "Dolazna poruka", + "outgoing-message": "Odlazna poruka", + "upload-new-sound": "Učitaj novi zvuk", + "saved": "Postavke spremljene" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/categories.json b/public/language/hr/admin/manage/categories.json new file mode 100644 index 0000000000..f87dfb28f8 --- /dev/null +++ b/public/language/hr/admin/manage/categories.json @@ -0,0 +1,68 @@ +{ + "settings": "Postavke kategorije", + "privileges": "Privilegije", + + "name": "Ime kategorije", + "description": "Opis kategorije", + "bg-color": "Pozadniska boja", + "text-color": "Boja teksta", + "bg-image-size": "Veličina pozadinske slike", + "custom-class": "Obična klasa", + "num-recent-replies": "# nedavnih objava", + "ext-link": "Vanjska poveznica", + "upload-image": "Učitaj sliku", + "delete-image": "Ukloni", + "category-image": "Slika kategorije", + "parent-category": "Roditeljska kategorija", + "optional-parent-category": "(Opcionalno) Roditeljska kategorija", + "parent-category-none": "(Ništa)", + "copy-settings": "Kopiraj postavke iz ", + "optional-clone-settings": "(Opcionalno) Kloniraj postavke iz kategorije", + "purge": "Odbaci kategoriju", + + "enable": "Omogući", + "disable": "Onemogući", + "edit": "Uredi", + + "select-category": "Odabri kategoriju", + "set-parent-category": "Postavi roditeljsku kategoriju ", + + "privileges.description": "Možete konfigurirati pristup kontrolnim privilegijama u ovoj kategoriji unutar ove sekcije.Privilegije mogu biti dane bazirane na korisnicima ili grupama.Možete dodati nove korisnike u ovu tabelu pretragom u polju ispod.", + "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.section-viewing": "Privilegije pogleda", + "privileges.section-posting": "Privilegije objave", + "privileges.section-moderation": "Dozvole moderiranja", + "privileges.section-user": "Korisnik", + "privileges.search-user": "Dodaj korisnika", + "privileges.no-users": "U ovoj kategoriji nema privilegije za korisnika.", + "privileges.section-group": "Grupa", + "privileges.group-private": "Ova grupa je privatna", + "privileges.search-group": "Dodaj grupu", + "privileges.copy-to-children": "Kopiraj u dijete", + "privileges.copy-from-category": "Kopiraj iz kategorije", + "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + + "analytics.back": "Povratak na listu kategorija", + "analytics.title": "Analitika za \"%1\" kategoriju", + "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", + "analytics.pageviews-daily": "Oblik 2 – Pregledi po danu za ovu kategoriju", + "analytics.topics-daily": "Oblik 3 – Dnevne teme kreirane u ovoj kategoriji", + "analytics.posts-daily": "Oblik 4 – Dnevne objave u ovoj kategoriji", + + "alert.created": "Kreirano", + "alert.create-success": "Kategorija uspješno kreirana!", + "alert.none-active": "Nemate aktivnih kategorija.", + "alert.create": "Napravi kategoriju", + "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", + "alert.purge-success": "Kategorija odbačena!", + "alert.copy-success": "Postavke kopirane!", + "alert.set-parent-category": "Postavi roditeljsku kategoriju", + "alert.updated": "Promijenjene kategorije", + "alert.updated-success": "ID kategorije %1 uspješno promijenjen", + "alert.upload-image": "Učitaj sliku kategorije", + "alert.find-user": "Pronađi korisnika", + "alert.user-search": "Pretraži korisnika ovdje ...", + "alert.find-group": "Pronađi grupu", + "alert.group-search": "Pretraži grupu ovdje ..." +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/groups.json b/public/language/hr/admin/manage/groups.json new file mode 100644 index 0000000000..a6056842bd --- /dev/null +++ b/public/language/hr/admin/manage/groups.json @@ -0,0 +1,35 @@ +{ + "name": "Ime grupe", + "description": "Opis grupe", + "member-count": "Broj članova", + "system": "Sistemska grupa", + "edit": "Uredi", + "search-placeholder": "Pretraga", + "create": "Kreiraj grupu", + "description-placeholder": "Kratki opis grupe", + "create-button": "Napravi", + + "alerts.create-failure": "Uh-Oh

Nastao je problem sa stvaranjem Vaše grupe.Molimo probajte ponovo kasnije!

", + "alerts.confirm-delete": "Sigurni ste da želite obrisati ovu grupu?", + + "edit.name": "Ime", + "edit.description": "Opis", + "edit.user-title": "Naslov članova", + "edit.icon": "Ikona grupe", + "edit.label-color": "Boja oznake grupe", + "edit.show-badge": "Prikaži beđ", + "edit.private-details": "Ako je omogućeno, pridruživanje grupi zahtjeva dozvolu vlasnika grupe.", + "edit.private-override": "Upozorenje:Privatne grupe su onemogućene na sistemskoj razini,koje onemogućavaju ovu opciju", + "edit.disable-requests": "Onemogući zahtjeve za priključenje", + "edit.hidden": "Skriveno", + "edit.hidden-details": "Ako je uključeno,ova grupa neće biti prikazana u listi grupa i korisnici će morati biti pozvani ručno", + "edit.add-user": "Dodaj korisnika u grupu", + "edit.add-user-search": "Pretraži korisnike", + "edit.members": "Lista članova", + "control-panel": "Kontrolna ploča grupa", + "revert": "Povrati", + + "edit.no-users-found": "Korisnik nije pronađen", + "edit.confirm-remove-user": "Sigurni ste da želite ukloniti ovog korisnika?", + "edit.save-success": "Promjene spremljene!" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/ip-blacklist.json b/public/language/hr/admin/manage/ip-blacklist.json new file mode 100644 index 0000000000..374b5029d5 --- /dev/null +++ b/public/language/hr/admin/manage/ip-blacklist.json @@ -0,0 +1,15 @@ +{ + "lead": "Upišite IP za blokiranje ovdje.", + "description": "Blokiranje korisničkog računa neki put nije dovoljno za odbiti nepoželjno osobu.U tom slučaju najbolji način da se zaštiti forum je onemogućavanje spajanja na forum sa određene IP adrese ili spektrom IP adresa.", + "active-rules": "Aktivna pravila", + "validate": "Potvrdite blokade ", + "apply": "Primjeni blokade", + "hints": "Sintaktički savjeti", + "hint-1": "Odredite jednu IP adresu po liniji. Možete dodati IP blokove dokle god su upisani u CIDR formatu (npr. 192.168.100.0/22).", + "hint-2": "Možete dodati komentare tako da u početku reda upišete simbol ljestvi code>#", + + "validate.x-valid": "%1 od %2 pravila valjano.", + "validate.x-invalid": "Sljedeća %1 pravila su nevažeća:", + + "alerts.applied-success": "Blokiranje omogućeno" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/registration.json b/public/language/hr/admin/manage/registration.json new file mode 100644 index 0000000000..aba7529be9 --- /dev/null +++ b/public/language/hr/admin/manage/registration.json @@ -0,0 +1,20 @@ +{ + "queue": "Na čekanju", + "description": "Nema korisnika na čekanju za registraciju.
Za pokretanje ove mogućnosti odite na Settings → User → User Registration i postavite tip registracije u \"Admin Approval\".", + + "list.name": "Ime", + "list.email": "Email", + "list.ip": "IP", + "list.time": "Vrijeme", + "list.username-spam": "Učestalost %1 Pojavljivanje %2 Samouvjerenost %3", + "list.email-spam": "Učestalost %1 Pojavljivanje %2", + "list.ip-spam": "Učestalost: %1 Pojavljivanje: %2", + + "invitations": "Pozivnice", + "invitations.description": "Ispod je potpuni popis poslanih pozivnica.Koristite ctrl + f za pretragu liste po emailu ili korisničkom imenu.

Korisničko ime će biti prikazano na desno od emaila za korisnike koji su iskoristili svoje pozivnice.", + "invitations.inviter-username": "Korisničko ime pozivatelja", + "invitations.invitee-email": "Email adresa pozivatelja", + "invitations.invitee-username": "Korisničko ime pozivatelja (ako je registriran)", + + "invitations.confirm-delete": "Sigurni ste da želite obrisati ovu pozivnicu?" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/tags.json b/public/language/hr/admin/manage/tags.json new file mode 100644 index 0000000000..9976f4d310 --- /dev/null +++ b/public/language/hr/admin/manage/tags.json @@ -0,0 +1,18 @@ +{ + "none": "Vaš forum nema tema sa oznakama", + "bg-color": "Pozadinska boja", + "text-color": "Boja teksta", + "create-modify": "Napravi i uredi oznake", + "description": "Odaberite oznake klikom ili povlačenjem, koristite SHIFT za višestruki odabir.", + "create": "Napravi oznaku", + "modify": "Uredi oznake", + "delete": "Obriši odabrane oznake", + "search": "Pretraži za oznake ...", + "settings": "Kliknite ovdje za posjetu postavki oznaka", + "name": "Ime oznake", + + "alerts.editing-multiple": "Uređivanje više oznaka", + "alerts.editing-x": "Uredi \"%1\" oznake", + "alerts.confirm-delete": "Želite li obrisati odabrane oznake?", + "alerts.update-success": "Oznake promijenjene!" +} \ No newline at end of file diff --git a/public/language/hr/admin/manage/users.json b/public/language/hr/admin/manage/users.json new file mode 100644 index 0000000000..67d5981583 --- /dev/null +++ b/public/language/hr/admin/manage/users.json @@ -0,0 +1,91 @@ +{ + "users": "Korisnici", + "edit": "Uredi", + "make-admin": "Dodaj administratora", + "remove-admin": "Makni administratora", + "validate-email": "Potvrdite email", + "send-validation-email": "Pošalji email potvrde", + "password-reset-email": "Poslan email zahtjev za resetiranje lozinke", + "ban": "Blokiraj korisnika", + "temp-ban": "Blokiraj korisnika privremeno", + "unban": "Odblokiraj korisnika", + "reset-lockout": "Resetiraj zaključavanje", + "reset-flags": "Resetiraj zastave", + "delete": "Obriši korisnika", + "purge": "Obriši korisnike i sadržaj", + "download-csv": "Preuzmi CSV", + "invite": "Pozivnica", + "new": "Novi korisnik", + + "pills.latest": "Posljednji korisnici", + "pills.unvalidated": "Nije potvrđen", + "pills.no-posts": "Nema objava", + "pills.top-posters": "Najviše objava", + "pills.top-rep": "Najviše reputacije", + "pills.inactive": "Neaktivan", + "pills.flagged": "Najviše zastava", + "pills.banned": "Blokirani", + "pills.search": "Pretraga korisnika", + + "search.username": "Po korisničkom imenu", + "search.username-placeholder": "Unesite korisničko ime za pretragu", + "search.email": "Sa email-om", + "search.email-placeholder": "Unesite email za pretragu", + "search.ip": "Po IP adresi", + "search.ip-placeholder": "Unesite IP adresu za pretragu", + "search.not-found": "Korisnik nije pronađen!", + + "inactive.3-months": "3 mjeseca", + "inactive.6-months": "6 mjeseci", + "inactive.12-months": "12 mjeseci", + + "users.uid": "uid", + "users.username": "korisničko ime", + "users.email": "email", + "users.postcount": "postcount", + "users.reputation": "reputacija", + "users.flags": "flags", + "users.joined": "joined", + "users.last-online": "Zadnji online", + "users.banned": "blokiran", + + "create.username": "Korisničko ime", + "create.email": "Email", + "create.email-placeholder": "Email korisnika", + "create.password": "Lozinka", + "create.password-confirm": "Potvdri lozinku", + + "temp-ban.length": "Dužina blokade", + "temp-ban.reason": "Razlog (Opcionalno)", + "temp-ban.hours": "Sati", + "temp-ban.days": "Dani", + "temp-ban.explanation": "Unesite dužinu trajana blokade. Ukoliko je vrijeme 0 smatra se permanentnom blokadom.", + + "alerts.confirm-ban": "Sigurni ste da želite blokirati ovo korisnika trajno?", + "alerts.confirm-ban-multi": "Sigurni ste da želite blokirati korisnika permanentno?", + "alerts.ban-success": "Korisnik blokiran!", + "alerts.button-ban-x": "Blokiraj %1 korisnika", + "alerts.unban-success": "Korisnik odblokiran!", + "alerts.lockout-reset-success": "Zaključavanje resetirano!", + "alerts.flag-reset-success": "Flags(s) reset!", + "alerts.no-remove-yourself-admin": "Nemoguće je maknuti samog sebe iz administracije!", + "alerts.make-admin-success": "Korisnik je sada administrator.", + "alerts.confirm-remove-admin": "Sigurni ste da želite ukloniti administratora?", + "alerts.remove-admin-success": "Korisnik više nije administrator!", + "alerts.confirm-validate-email": "Želite li potvrditi email ovih korisnika?", + "alerts.validate-email-success": "Email potvrđen", + "alerts.password-reset-confirm": "Želite li poslati email za reset lozinke korisniku ?", + "alerts.confirm-delete": "Warning!
Do you really want to delete user(s)?
This action is not reversable! Only the user account will be deleted, their posts and topics will remain.", + "alerts.delete-success": "Korisnici obrisani!", + "alerts.confirm-purge": "Warning!
Do you really want to delete user(s) and their content?
This action is not reversable! All user data and content will be erased!", + "alerts.create": "Napravi korisnika", + "alerts.button-create": "Napravi", + "alerts.button-cancel": "Odustani", + "alerts.error-passwords-different": "Lozinke se moraju podudarati!", + "alerts.error-x": "Greška

%1

", + "alerts.create-success": "Korisnik kreiran!", + + "alerts.prompt-email": "Email:", + "alerts.email-sent-to": "Email pozivnica je poslana %1", + "alerts.x-users-found": "%1 korisnik pronađen! Vrijeme pretrage: %2 ms" +} \ No newline at end of file diff --git a/public/language/hr/admin/menu.json b/public/language/hr/admin/menu.json new file mode 100644 index 0000000000..088c6a06ef --- /dev/null +++ b/public/language/hr/admin/menu.json @@ -0,0 +1,74 @@ +{ + "section-general": "Glavno", + "general/dashboard": "Glavna ploča", + "general/homepage": "Naslovnica", + "general/navigation": "Navigacija", + "general/languages": "Jezici", + "general/sounds": "Zvukovi", + "general/social": "Socijalno", + + "section-manage": "Upravljanje", + "manage/categories": "Kategorije", + "manage/tags": "Oznake", + "manage/users": "Korisnici", + "manage/registration": "Lista zahtjeva za registraciju", + "manage/groups": "Grupe", + "manage/ip-blacklist": "IP blokade", + + "section-settings": "Postavke", + "settings/general": "Generalno", + "settings/reputation": "Reputacija", + "settings/email": "Email", + "settings/user": "Korisnik", + "settings/group": "Grupa", + "settings/guest": "Gosti", + "settings/uploads": "Slanje", + "settings/post": "Objave", + "settings/chat": "Razgovor", + "settings/pagination": "Numeriranje", + "settings/tags": "Oznake", + "settings/notifications": "Obavijesti", + "settings/cookies": "Kolačići", + "settings/web-crawler": "Web puzač", + "settings/sockets": "Utičnice", + "settings/advanced": "Napredno", + + "settings.page-title": "%1 Postavke", + + "section-appearance": "Izgled", + "appearance/themes": "Predlošci", + "appearance/skins": "Izgled", + "appearance/customise": "Uobičajni HTML i CSS", + + "section-extend": "Proširi", + "extend/plugins": "Dodatci", + "extend/widgets": "Widgeti", + "extend/rewards": "Nagrade", + + "section-social-auth": "Socijalna provjera autentičnosti", + + "section-plugins": "Dodatci", + "extend/plugins.install": "Instaliraj dodatke", + + "section-advanced": "Napredno", + "advanced/database": "Baza podataka", + "advanced/events": "Događanja", + "advanced/logs": "Dnevnik", + "advanced/errors": "Greške", + "advanced/cache": "Cache", + "development/logger": "Dnevnik", + "development/info": "Info", + + "reload-forum": "Ponovno učitaj forum", + "restart-forum": "ponovno pokreni forum", + "logout": "Odjava", + "view-forum": "Pogledaj forum", + + "search.placeholder": "Pretraga...", + "search.no-results": "Nema rezultata ...", + "search.search-forum": "Pretraži forum za ", + "search.keep-typing": "Upiši više da vidiš rezultate ...", + "search.start-typing": "Počni pisati da bi vidio rezultate...", + + "connection-lost": "Veza sa %1 je prekinuta, pokušavam se spojiti ..." +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/advanced.json b/public/language/hr/admin/settings/advanced.json new file mode 100644 index 0000000000..3bd50b1a1e --- /dev/null +++ b/public/language/hr/admin/settings/advanced.json @@ -0,0 +1,19 @@ +{ + "maintenance-mode": "Održavanje u toku", + "maintenance-mode.help": "Kada je forum u stanju održavanja,svi zahtjevi će biti preusmjereni statičnoj stranici.Administratori su izuzeti od ovog preusmjerenja i mogu normalno koristiti stranicu.", + "maintenance-mode.message": "Poruka održavanja", + "headers": "Zaglavlje", + "headers.allow-from": "Izaberi ALLOW-FROM da bi ste postavili NodeBB u iFrame.", + "headers.powered-by": "Uredi \"Powered by\" zaglavlje koje šalje NodeBB", + "headers.acao": "Pristup-Kontrola-Dozvoli-Izvor", + "headers.acao-help": "Za zabranu pristupa svim stranicama ostavi prazno", + "headers.acam": "Access-Control-Allow-Methods", + "headers.acah": "Access-Control-Allow-Headers", + "traffic-management": "Upravljanje prometom", + "traffic.help": "NodeBB koristi modul koji automatski odbija zahtjeve u situacijama visokog prometa.Možete ove postavke izmjeniti ovdje,iako su već zadane postavke dobra početna točka.", + "traffic.enable": "Omogući upravljanje prometom", + "traffic.event-lag": "Event Loop Lag Threshold (in milliseconds)", + "traffic.event-lag-help": "Smanjivanje ove vrijednosti smanjuje vrijeme čekanja za učitavanje stranica,ali će također pokazivati poruku \"prekomjerno opterećenje\" više korisnika(u takvim slučajevima potrebno je ponovo pokretanje).", + "traffic.lag-check-interval": "Provjeri interval (u milisekundama)", + "traffic.lag-check-interval-help": "Smanjivanje ove vrijednosti uzrokuje da NodeBB postane osjetljivji na oscilacije u prometu,takodjer može uzrokovati da provjere postanu preosjetljive(Biti će potrebno ponovno pokretanje)." +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/chat.json b/public/language/hr/admin/settings/chat.json new file mode 100644 index 0000000000..2ec0c9147e --- /dev/null +++ b/public/language/hr/admin/settings/chat.json @@ -0,0 +1,9 @@ +{ + "chat-settings": "Postavke razgovora", + "disable": "Onemogući razgovor", + "disable-editing": "Onemogući uređivanje/brisanje poruka razgovora", + "disable-editing-help": "Administratori i moderatori su izuzeti od ovih restrikcija", + "max-length": "Maksimalna dužina poruka u razgovoru", + "max-room-size": "Maksimalan broj korisnika u sobama za razgovor", + "delay": "Vrijeme između poruka razgovora u milisekundama" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/cookies.json b/public/language/hr/admin/settings/cookies.json new file mode 100644 index 0000000000..b38e7b223a --- /dev/null +++ b/public/language/hr/admin/settings/cookies.json @@ -0,0 +1,11 @@ +{ + "eu-consent": "Suglasnost EU", + "consent.enabled": "Omogućeno", + "consent.message": "Poruka obavijesti", + "consent.acceptance": "Poruka prihvaćanja", + "consent.link-text": "Odrednice Poveznice Tekst", + "consent.blank-localised-default": "Pusti prazno za zadanu NodeBB lokalizaciju", + "settings": "Postavke", + "cookie-domain": "Sesija kolačić domene", + "blank-default": "Ostavi prazno za osnovno" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/email.json b/public/language/hr/admin/settings/email.json new file mode 100644 index 0000000000..67ef2941c5 --- /dev/null +++ b/public/language/hr/admin/settings/email.json @@ -0,0 +1,25 @@ +{ + "email-settings": "Postavke emaila", + "address": "Email adresa", + "address-help": "Sljedeća email adresa je adresa koju će primatelj vidjeti u \"Od\" i \"Odgovori na\" poljima.", + "from": "Od imena", + "from-help": "Ime prikazano u dolaznom emailu.", + "gmail-routing": "Gmail putanja", + "gmail-routing-help1": "Postoje prijave da je Gmail Routing onemogućen na računima sa povišenom sigurnosti. U tom slučaju morati ćete konfigurirati vaš GMail račun da dozvoljava manje sigurne aplikacije.", + "gmail-routing-help2": "Za više informacija o ovom rješenju href=\"https://nodemailer.com/using-gmail/\"> molimo pročitajte NodeMailer članak o tom problemu .Alternativa je korištenje email dodatak treće strane poput SendGrid,MailGun itd. Pretražite moguće email dodatke ovdje.", + "gmail-transport": "Prosljedite email kroz Gmail/Google Apps račun", + "gmail-transport.username": "Korisničko ime", + "gmail-transport.username-help": "Unesite punu email adresu ovdje,naročito ako koristite domenu upravljanu preko Google Appsa.", + "gmail-transport.password": "Lozinka", + "template": "Uredi predložak emaila", + "template.select": "Odaberi predložak emaila", + "template.revert": "Povrati na original ", + "testing": "Testiranje emaila", + "testing.select": "Odaberi email predložak ", + "testing.send": "Pošalji testni email", + "testing.send-help": "Ovaj test mail će biti poslan svim trenutačno prijavljenim korisnicima na njihovu email adresu.", + "subscriptions": "Email pretplate", + "subscriptions.disable": "Onemogući obavijesti emailom za pretplatnika ", + "subscriptions.hour": "Pregled Sati.", + "subscriptions.hour-help": "Unesite broj koji pretstavlja vrijeme kada će se poslati pregled mailom (npr. 0 za ponoć, 17za 5 popodne).Imajte na umu da to vrijeme predstavlja vrijeme servera te ne mora predstavljati vrijeme na Vašem sistemu. Vrijeme servera je:
Sljedeći pregled će biti poslan ." +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/general.json b/public/language/hr/admin/settings/general.json new file mode 100644 index 0000000000..37c16a92b5 --- /dev/null +++ b/public/language/hr/admin/settings/general.json @@ -0,0 +1,32 @@ +{ + "site-settings": "Postavke stranice", + "title": "Naslov stranice", + "title.name": "Ime Vaše zajednice", + "title.show-in-header": "Prikaži naslov stranice u zaglavlju", + "browser-title": "Naslov pretraživača", + "browser-title-help": "Ako naslov pretraživača nije postavljen, koristit će se naziv foruma", + "title-layout": "Raspored naslova", + "title-layout-help": "Definiraj kako će naslov pretraživača biti strukturiran npr.: {pageTitle} | {browserTitle}", + "description.placeholder": "Kratak opis zajednice", + "description": "Opis foruma", + "keywords": "Ključne riječi", + "keywords-placeholder": "Ključne riječi koje opisuju Vašu zajednicu, odvojeni zarezom", + "logo": "Logo foruma", + "logo.image": "Slika", + "logo.image-placeholder": "Putanja logotipa za zaglavlje foruma", + "logo.upload": "Učitaj", + "logo.url": "URL", + "logo.url-placeholder": "URL loga stranice", + "logo.url-help": "U slučaju klika na logo,pošalji korisnike na ovu adresu.U slučaju praznog polja,korisnik će biti poslan na index foruma.", + "logo.alt-text": "Alt tekst", + "log.alt-text-placeholder": "Alternativni tekst za dostupnost", + "favicon": "Favicon", + "favicon.upload": "Učitaj", + "touch-icon": "Naslovnica/Touch ikona", + "touch-icon.upload": "Učitaj", + "touch-icon.help": "Recommended size and format: 192x192, PNG format only. If no touch icon is specified, NodeBB will fall back to using the favicon.", + "outgoing-links": "Odlazne poveznice", + "outgoing-links.warning-page": "Koristi upozorenje za odlazne poveznice", + "search-default-sort-by": "Pretraži zadani poredak", + "outgoing-links.whitelist": "Domene za koje se ne koristi odlazno upozorenje" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/group.json b/public/language/hr/admin/settings/group.json new file mode 100644 index 0000000000..c621da8cb1 --- /dev/null +++ b/public/language/hr/admin/settings/group.json @@ -0,0 +1,12 @@ +{ + "general": "Glavno", + "private-groups": "Privatne grupe", + "private-groups.help": "Ako je uključeno,ulazak u grupe zahtjevati će odobrenje vlasnika grupe (Default: enabled)", + "private-groups.warning": "Pazi! Ako je ova opcija isključena,a imate privatne grupe,automatski će postati javne.", + "allow-creation": "Dozvoli kreiranje grupe", + "allow-creation-help": "Ako je uključeno,korisnicima će biti omogućeno stvaranje grupa (Default: disabled)", + "max-name-length": "Maksimalna dužina imena grupe", + "cover-image": "Slika grupe", + "default-cover": " ", + "default-cover-help": "Dodaj slike sa zarezima između za grupe koje nemaju učitanu naslovnu sliku" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/guest.json b/public/language/hr/admin/settings/guest.json new file mode 100644 index 0000000000..8cc284acb4 --- /dev/null +++ b/public/language/hr/admin/settings/guest.json @@ -0,0 +1,8 @@ +{ + "handles": "Upravljanje gostima", + "handles.enabled": "Dozvoli upravljanje gostima", + "handles.enabled-help": "Ova opcija omogućava gostima da izaberi ime za svaku objavu koju naprave.Ako je onemogućena gosti će se zvati \"gost\".", + "privileges": "Privilegije gosta", + "privileges.can-search": "Dozvoli gostima pretragu bez prijave", + "privileges.can-search-users": "Dopusti gostima pretragu korisnika bez prijave" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/notifications.json b/public/language/hr/admin/settings/notifications.json new file mode 100644 index 0000000000..1d1c969f34 --- /dev/null +++ b/public/language/hr/admin/settings/notifications.json @@ -0,0 +1,5 @@ +{ + "notifications": "Obavijesti", + "welcome-notification": "Obavijest dobrodošlice", + "welcome-notification-link": "Poveznica objave dobrodošlice" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/pagination.json b/public/language/hr/admin/settings/pagination.json new file mode 100644 index 0000000000..3a38d2bfe9 --- /dev/null +++ b/public/language/hr/admin/settings/pagination.json @@ -0,0 +1,9 @@ +{ + "pagination": "Postavke numeriranja", + "enable": "Numeriraj teme i objave umjesto beskrajnog skrolanja.", + "topics": "Numeriranje tema", + "posts-per-page": "Objava po stranici ", + "categories": "Numeriranje kategorija", + "topics-per-page": "Tema po stranici", + "initial-num-load": "Početni broj tema za učitati u sekcijama nepročitano,nedavno i popularno." +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/post.json b/public/language/hr/admin/settings/post.json new file mode 100644 index 0000000000..e97f8d77e8 --- /dev/null +++ b/public/language/hr/admin/settings/post.json @@ -0,0 +1,46 @@ +{ + "sorting": "Redosljed objava", + "sorting.post-default": "Zadano sortiranje objava", + "sorting.oldest-to-newest": "Starije prema Novijem", + "sorting.newest-to-oldest": "Novije prema Starijem", + "sorting.most-votes": "Najviše glasova", + "sorting.topic-default": "Uobičajeno sortiranje tema", + "restrictions": "Restrikcije objave", + "restrictions.seconds-between": "Sekunde između objava", + "restrictions.seconds-between-new": "Sekunde prije objave za nove korisnike", + "restrictions.rep-threshold": "Prag reputacije prije nego je ova restrikcija maknuta.", + "restrictions.seconds-defore-new": "Sekunde prije novi korisnik može objavljivati", + "restrictions.seconds-edit-after": "Broj sekundi koje su korisnicima dozvoljene za mijenjanje objava nakon objavljivanja(u slučaju 0 ova postavka je ugašena).", + "restrictions.seconds-delete-after": "Broj sekundi koji korisnik ima za obrisati objavu nakon objavljivanja(u slučaju 0 ova postavka je ugašena).", + "restrictions.replies-no-delete": "Broj odgovora nakon što je korisniku zabranjeno brisati njegovu temu(u slučaju 0 ova postavka je ugašena).", + "restrictions.min-title-length": "Minimalna dužina naslova", + "restrictions.max-title-length": "Maksimalna dužina naslova", + "restrictions.min-post-length": "Minimalna dužina objave", + "restrictions.max-post-length": "Maksimalna dužina objave", + "restrictions.days-until-stale": "Dani do kad se tema smatra neaktivnom", + "restrictions.stale-help": "Ako je tema smatran neaktivnim,upozorenje će biti prikazano svim korisnicima koji pokušaju odgovoriti na temu", + "timestamp": "Vremenska oznaka", + "timestamp.cut-off": "Datum prekida (u danima)", + "timestamp.cut-off-help": "Dates & times will be shown in a relative manner (e.g. \"3 hours ago\" / \"5 days ago\"), and localised into various\n\t\t\t\t\tlanguages. After a certain point, this text can be switched to display the localised date itself\n\t\t\t\t\t(e.g. 5 Nov 2016 15:30).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", + "teaser": "Zadirkivač objava", + "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", + "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "teaser.first": "Prvi", + "unread": "Nepročitane postavke", + "unread.cutoff": "Nepročitano dani prekinutosti", + "unread.min-track-last": "Minimalni broj objava u temi prije praćenja zadnje pročitanog", + "recent": "Nedavne postavke", + "recent.categoryFilter.disable": "onemogući filtriranje tema u ignoriranim kategorijama na stranici /nedavno", + "signature": "Postavke potpisa", + "signature.disable": "Onemogući potpise", + "signature.no-links": "Onemogući odlazne poveznice u potpisima ", + "signature.no-images": "Onemogući slike u potpisima", + "signature.max-length": "Minimalna dužina potpisa", + "composer": "Postavke Composer-a", + "composer-help": "The following settings govern the functionality and/or appearance of the post composer shown\n\t\t\t\tto users when they create new topics, or reply to existing topics.", + "composer.show-help": "Prikaži \"Pomoć\"", + "composer.enable-plugin-help": "Dozvoli dodatcima da dodaju sadržaj u \"Pomoć\"", + "composer.custom-help": "Tekst \"Pomoć\"", + "ip-tracking": "IP praćenje", + "ip-tracking.each-post": "Prati IP adresu za svaku objavu" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/reputation.json b/public/language/hr/admin/settings/reputation.json new file mode 100644 index 0000000000..71e4fab8b6 --- /dev/null +++ b/public/language/hr/admin/settings/reputation.json @@ -0,0 +1,9 @@ +{ + "reputation": "Postavke reputacije", + "disable": "Onemogući reputacije", + "disable-down-voting": "Onemogući oduzimanje glasova", + "votes-are-public": "Svi glasovi su javni", + "thresholds": "Prag aktivnosti", + "min-rep-downvote": "Minimalna reputacija za glasanje protiv", + "min-rep-flag": "Minimalna reputacija za označavanje objava" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/sockets.json b/public/language/hr/admin/settings/sockets.json new file mode 100644 index 0000000000..7528fef036 --- /dev/null +++ b/public/language/hr/admin/settings/sockets.json @@ -0,0 +1,6 @@ +{ + "reconnection": "Postavke ponovnog spajanja", + "max-attempts": "Max pokušaji spajanja", + "default-placeholder": "Zadano: %1", + "delay": "Stanka u ponovnom spajanju" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/tags.json b/public/language/hr/admin/settings/tags.json new file mode 100644 index 0000000000..ea7284a928 --- /dev/null +++ b/public/language/hr/admin/settings/tags.json @@ -0,0 +1,12 @@ +{ + "tag": "Postavke oznaka", + "min-per-topic": "Najmanje oznaka za temu", + "max-per-topic": "Maksimalno oznaka po temi", + "min-length": "Minimalna dužina oznake", + "max-length": "Maksimalna dužina oznaka", + "goto-manage": "Klikni ovdje za upravljanje oznakama.", + "privacy": "Privatnost", + "list-private": "Postavi listu oznaka privatno", + "related-topics": "Slične teme", + "max-related-topics": "Maksimalni broj povezanih tema za prikaz(ako je podržano unutar predloška)" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/uploads.json b/public/language/hr/admin/settings/uploads.json new file mode 100644 index 0000000000..ced5c66195 --- /dev/null +++ b/public/language/hr/admin/settings/uploads.json @@ -0,0 +1,28 @@ +{ + "posts": "Objave", + "allow-files": "Dozvoli korisnicima učitavanje regularnih datoteka", + "private": "Učini datoteke privatnim", + "max-image-width": "Promjeni veličinu slike na zadanu širinu (u pikselima)", + "max-image-width-help": "(u pixelima,zadano:760 pixela,upiši 0 za onemogućiti opciju)", + "max-file-size": "Maksimalna veličina datoteka (in KiB)", + "max-file-size-help": "u kilobajtima,zadano:2048 kiB", + "allow-topic-thumbnails": "Dozvoli korisnicima da učitaju sliku teme", + "topic-thumb-size": "Veličina slike teme", + "allowed-file-extensions": "Dozvoljene ekstenzije datoteka", + "allowed-file-extensions-help": "Unesite popis dozvoljenih ekstenzija datoteka sa zarezima između (npr. pdf,xls,doc ).Prazan popis znači da su sve ekstenzije dozvoljene.", + "profile-avatars": "Avatar profila", + "allow-profile-image-uploads": "Dozvoli korisnicima da učitaju sliku profila", + "convert-profile-image-png": "Konvertiraj profilne slike u PNG", + "default-avatar": "Zadani osnovni avatar", + "upload": "Učitaj", + "profile-image-dimension": "Dimenzije slike profila", + "profile-image-dimension-help": "(u pikselima, zadano: 128 piksela)", + "max-profile-image-size": "Maksimalna veličina profilne slike", + "max-profile-image-size-help": "(u kilobajtima, početna postavka: 256 KiB)", + "max-cover-image-size": "Maksimalna veličina slike za naslovnicu", + "max-cover-image-size-help": "(in kilobytes, default: 2,048 KiB)", + "keep-all-user-images": "Zadrži stare verzije avatara i slike profila na serveru", + "profile-covers": "Slika profila", + "default-covers": "Osnovne slike naslovnica", + "default-covers-help": "Add comma-separated default cover images for accounts that don't have an uploaded cover image" +} diff --git a/public/language/hr/admin/settings/user.json b/public/language/hr/admin/settings/user.json new file mode 100644 index 0000000000..7a012120b0 --- /dev/null +++ b/public/language/hr/admin/settings/user.json @@ -0,0 +1,60 @@ +{ + "authentication": "Autentifikacija", + "allow-local-login": "Dozvoli lokalnu prijavu", + "require-email-confirmation": "Zahtjeva potvrdu email-om", + "email-confirm-interval": "Korisnik ne može ponovno poslati potvrdni email do ", + "email-confirm-email2": "prošlo je minuta", + "allow-login-with": "Dozvoli prijavu sa", + "allow-login-with.username-email": "Korisničko ime ili Email", + "allow-login-with.username": "Korisničko ime", + "allow-login-with.email": "Samo email", + "account-settings": "Postavke računa", + "disable-username-changes": "onemogući promjenu korisničkog imena", + "disable-email-changes": "Onemogući promjenu emaila", + "disable-password-changes": "Onemogući promjenu lozinke", + "allow-account-deletion": "Dozvoli brisanje računa korisnicima", + "user-info-private": "Postavi korisničke informacije privatne", + "themes": "Predlošci", + "disable-user-skins": "Onemogući korisnicima odabir predloška", + "account-protection": "Zaštita računa", + "login-attempts": "Pokušaji prijave po satu", + "login-attempts-help": "U slučaju pokušaja prijave na račun user's u tolikoj količini da prelazi ovaj prag,račun će biti zaključan na pre-konfigurirano vrijeme", + "lockout-duration": "Broj minuta u slučaju zaključavanja računa", + "login-days": "Dani za zapamtiti sesiju korisničke prijave", + "password-expiry-days": "Forsiraj reset lozinke nakon broja dana", + "registration": "Korisnička registracija", + "registration-type": "Tip registracije", + "registration-type.normal": "Nromalno", + "registration-type.admin-approval": "Administratorsko dopuštenje", + "registration-type.admin-approval-ip": "Administratorska dozovola za IP", + "registration-type.invite-only": "Samo uz pozivnicu", + "registration-type.admin-invite-only": "Samo uz pozivnicu administratora", + "registration-type.disabled": "Bez registracije", + "registration-type.help": "Normal - Users can register from the /register page.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", + "registration.max-invites": "Maksimalan broj pozivnica po korisniku", + "max-invites": "Maksimalan broj pozivnica po korisniku", + "max-invites-help": "0 bez restrikcija. Administrator ima neograničeno pozivnica
Primjenjivo samo za \"poziv na forum\"", + "min-username-length": "Minimalna dužina korisničkog imena", + "max-username-length": "Maksimalna dužina korisničkog imena", + "min-password-length": "Minimalna dužina lozinke", + "min-password-strength": "Minimalna snaga lozinke", + "max-about-me-length": "Maksimalna dužina \"O meni\"", + "terms-of-use": "Pravila korištenja foruma (ostavi prazno za isključeno)", + "user-search": "Korisnička pretraga", + "user-search-results-per-page": "Broj rezultata za prikaz", + "default-user-settings": "Osnovne korisničke postavke", + "show-email": "Prikaži email", + "show-fullname": "Prikaži puno ime", + "restrict-chat": "Dozvoli poruke samo od ljudi koje praim", + "outgoing-new-tab": "Otvori odlazne poveznive u novom prozoru ", + "topic-search": "Dopusti pretragu po temama", + "digest-freq": "Pretplatite se na pregled", + "digest-freq.off": "Isključi", + "digest-freq.daily": "Dnevno", + "digest-freq.weekly": "Tjedno", + "digest-freq.monthly": "Mjesečno", + "email-chat-notifs": "Pošalji email ukoliko stigne nova poruka dok nisam na mreži", + "email-post-notif": "Pošalji email pri odgovoru u teme na koje pratim", + "follow-created-topics": "Prati teme koje kreiram", + "follow-replied-topics": "Prati teme na koje odgovorim" +} \ No newline at end of file diff --git a/public/language/hr/admin/settings/web-crawler.json b/public/language/hr/admin/settings/web-crawler.json new file mode 100644 index 0000000000..1bcfcd2409 --- /dev/null +++ b/public/language/hr/admin/settings/web-crawler.json @@ -0,0 +1,10 @@ +{ + "crawlability-settings": "Postavke pretraživanja", + "robots-txt": "Obični Robots.txt ostavi prazno za osnovno", + "sitemap-feed-settings": "Sitemap i postavke feeda", + "disable-rss-feeds": "Onemogući RSS", + "disable-sitemap-xml": "Onemogući Sitemap.xml", + "sitemap-topics": "Broj tema za prikaz u mapi foruma", + "clear-sitemap-cache": "Očisti mapu foruma iz predmemorije", + "view-sitemap": "Pogledaj mapu foruma" +} \ No newline at end of file diff --git a/public/language/hr/category.json b/public/language/hr/category.json new file mode 100644 index 0000000000..c4eb7644fa --- /dev/null +++ b/public/language/hr/category.json @@ -0,0 +1,20 @@ +{ + "category": "Kategorija", + "subcategories": "Podkategorije", + "new_topic_button": "Nova Tema", + "guest-login-post": "Prijavi se za objavu", + "no_topics": "Nema tema u ovoj kategoriji.
Zašto ne probate napisati novu?", + "browsing": "pregledavanje", + "no_replies": "Nema odgovora", + "no_new_posts": "Nema novih tema.", + "share_this_category": "Podijeli ovu kategoriju", + "watch": "Prati", + "ignore": "Ignoriraj", + "watching": "Pratim", + "ignoring": "Ignoriram", + "watching.description": "Prikaži nepročitane teme", + "ignoring.description": "Ne prikazuj nepročitane teme", + "watch.message": "Sada pratite nove objave iz ove kategorije i svih potkategorija", + "ignore.message": "Sada ignorirate nove objave iz ove kategorije i svih potkategorija", + "watched-categories": "Praćene Kategorije" +} \ No newline at end of file diff --git a/public/language/hr/email.json b/public/language/hr/email.json new file mode 100644 index 0000000000..cdf26cd74f --- /dev/null +++ b/public/language/hr/email.json @@ -0,0 +1,40 @@ +{ + "password-reset-requested": "Zatraženo ponovno kreiranje lozinke - %1!", + "welcome-to": "Dobrodošli na %1", + "invite": "Poziv s %1", + "greeting_no_name": "Zdravo", + "greeting_with_name": "Zdravo %1", + "welcome.text1": "Zahvaljujemo na registraciji na %1!", + "welcome.text2": "Da bi u potpunosti aktivirali Vaš račun, moramo provjeriti da li ste Vi pravi vlasnik email adrese sa kojom ste se registrirali.", + "welcome.text3": "Administrator je prihvatio vaš zahtjev za registraciju. Možete se prijaviti koristeći svoje korisničko ime i lozinku.", + "welcome.cta": "Kliknite ovdje da bi potvrdili email adresu", + "invitation.text1": "%1 vas je pozvao da se pridružite %2", + "invitation.ctr": "Kliknite ovdje kako biste stvorili korisnički račun.", + "reset.text1": "Dobili smo zahtjev za ponovnim kreiranjem lozinke, vjerojatno jer ste ju zaboravili. Ako niste, molimo vas da ignorirate ovaj email.", + "reset.text2": "Da bi nastavili sa ponovnim kreiranjem lozinke, kliknite na ovaj link:", + "reset.cta": "Kliknite ovdje kako biste postavili novu lozinku.", + "reset.notify.subject": "Lozinka uspješno promijenjena.", + "reset.notify.text1": "Obavještavamo vas da vam je lozinka na %1 uspješno promijenjena.", + "reset.notify.text2": "Ako niste ovo odobrili, molimo vas obavijestite administratora.", + "digest.notifications": "Imate nepročitane obavijesti s %1:", + "digest.latest_topics": "Posljednje teme s %1", + "digest.cta": "Kliknite ovdje kako biste posjetili %1", + "digest.unsub.info": "Ovaj pregled je poslan zbog Vaših postavki pretplata.", + "digest.no_topics": "Nije bilo aktivnih tema u posljednjih %1", + "digest.day": "Dan", + "digest.week": "Tjedan", + "digest.month": "Mjesec", + "digest.subject": "Pregled za %1", + "notif.chat.subject": "Nova poruka od %1", + "notif.chat.cta": "Klikni ovdje za nastavak razgovora ", + "notif.chat.unsub.info": "Ova obavijest razgovora Vam je poslana na temelju vaših postavki pretplate.", + "notif.post.cta": "Pročitaj temu", + "notif.post.unsub.info": "Ova objava Vam je poslana na temelju vaših postavki pretplate.", + "test.text1": "Ovo je test email za provjeru Vaše konfiguracije.", + "unsub.cta": "Klikni ovdje za promjenu postavki", + "banned.subject": "Blokirani se na %1", + "banned.text1": "Korisnik %2 je blokirao %1.", + "banned.text2": "Blok će trajati do %1.", + "banned.text3": "Blokirani ste zbog:", + "closing": "Hvala!" +} \ No newline at end of file diff --git a/public/language/hr/error.json b/public/language/hr/error.json new file mode 100644 index 0000000000..1a7b56feeb --- /dev/null +++ b/public/language/hr/error.json @@ -0,0 +1,132 @@ +{ + "invalid-data": "Nevažeći podaci", + "not-logged-in": "Izgleda da niste prijavljeni", + "account-locked": "Vaš račun je privremeno blokiran", + "search-requires-login": "Pretraga zahtijeva prijavu - prijavite se ili se registrirajte.", + "invalid-cid": "Netočan ID kategorije", + "invalid-tid": "Netočan ID teme", + "invalid-pid": "Netočan ID objave", + "invalid-uid": "Netočan ID korisnika", + "invalid-username": "Netočno korisničko ime", + "invalid-email": "Netočan email", + "invalid-title": "Netočan naslov!", + "invalid-user-data": "Netočni korisnički podatci", + "invalid-password": "Netočna lozinka", + "invalid-username-or-password": "Upišite oboje, korisničko ime i lozinku", + "invalid-search-term": "Netočan upit pretraživanja", + "csrf-invalid": "Nismo Vas uspijeli prijaviti, najvjerovatnije zbog istekle sesije. Molimo pokušajte ponovno", + "invalid-pagination-value": "Netočno numeriranje stranica, mora biti %1 ili %2", + "username-taken": "Korisničko ime je zauzeto", + "email-taken": "Email je zauzet", + "email-not-confirmed": "Vaš email još nije potvrđen, kliknite ovdje da bi potvrdili svoj email.", + "email-not-confirmed-chat": "Nemožete razgovarati dok Vaš email nije potvrđen, kliknite ovdje da bi potvrdili svoj email.", + "email-not-confirmed-email-sent": "Vaš email još nije potvrđen, provjerite svoj sandučić za potvrdni email.", + "no-email-to-confirm": "Ovaj forum zahtijeva potvrdu emaila, kliknite ovdje da bi potvrdili svoj email.", + "email-confirm-failed": "Nismo u mogućnosti potvrditi Vaš email, pokušajte ponovno kasnije.", + "confirm-email-already-sent": "Potvrdni email je poslan, počekajte %1 minuta za ponovni pokušaj.", + "sendmail-not-found": "Sendmail nije pronađen, provjerite da li je instaliran?", + "username-too-short": "Korisničko ime prekratko", + "username-too-long": "Korisničko ime predugo", + "password-too-long": "Lozinka je preduga", + "user-banned": "Korisnik blokiran", + "user-banned-reason": "Ovaj račun je blokiran (Razlog: %1)", + "user-banned-reason-until": "Ovaj račun je blokiran do %1 (Razlog: %2)", + "user-too-new": "Pričekajte %1 sekundi prije prve objave", + "blacklisted-ip": "Vaša IP adresa je blokirana. Ako mislite da je ovo greška, kontaktirajte administratora.", + "ban-expiry-missing": "Postavite datum isteka blokade", + "no-category": "Kategorija ne postoji", + "no-topic": "Tema ne postoji", + "no-post": "Objava ne postoji", + "no-group": "Grupa ne postoji", + "no-user": "Korisnik ne postoji", + "no-teaser": "Zadirkivač ne postoji", + "no-privileges": "Nemate privilegije za ovu radnju.", + "category-disabled": "Kategorija onemogućena", + "topic-locked": "Tema zaključana", + "post-edit-duration-expired": "Dozvoljeno vam je uređivanje %1 sekundi nakon objave", + "post-edit-duration-expired-minutes": "Dozvoljeno vam je uređivanje %1 minuta nakon objave", + "post-edit-duration-expired-minutes-seconds": "Dozvoljeno vam je uređivanje %1 minuta %2 sekunde nakon objave", + "post-edit-duration-expired-hours": "Dozvoljeno vam je uređivanje %1 sat nakon objave", + "post-edit-duration-expired-hours-minutes": "Dozvoljeno vam je uređivanje %1 sat %2 minute nakon objave", + "post-edit-duration-expired-days": "Dozvoljeno vam je uređivanje %1 dan nakon objave", + "post-edit-duration-expired-days-hours": "Dozvoljeno vam je uređivanje %1 dan %2 sata nakon objave", + "post-delete-duration-expired": "Dozvoljeno vam je brisanje %1 sekundi nakon objave", + "post-delete-duration-expired-minutes": "Dozvoljeno vam je brisanje %1 minute nakon objave", + "post-delete-duration-expired-minutes-seconds": "Dozvoljeno vam je brisanje %1 minute %2 sekunde nakon objave", + "post-delete-duration-expired-hours": "Dozvoljeno vam je brisanje %1 sat nakon objave", + "post-delete-duration-expired-hours-minutes": "Dozvoljeno vam je brisanje %1 sat i %2 minute nakon objave", + "post-delete-duration-expired-days": "Dozvoljeno vam je brisanje %1 dan nakon objave", + "post-delete-duration-expired-days-hours": "Dozvoljeno vam je brisanje %1 dan %2 sata nakon objave", + "cant-delete-topic-has-reply": "Nemožete obrisati temu nakon odgovora", + "cant-delete-topic-has-replies": "Nemožete obrisati temu nakon %1 odgovora", + "content-too-short": "Unesite dužu objavu. Objava mora sadržavati bar %1 znaka. ", + "content-too-long": "Unestie kraću objavu. Objave ne mogu biti duže od %1 znaka.", + "title-too-short": "Unesite duži naslov, minimalno %1 znakova.", + "title-too-long": "Unesite kraći naslov, maksimalno %1 znakova.", + "category-not-selected": "Kategorija nije odabrana.", + "too-many-posts": "Možete objavljivati svakih %1 skeundi, pričekajte prije ponovne objave", + "too-many-posts-newbie": "Kao novi korisnik možete objaviti svakih %1 sekundi dok ne steknete %2 reputaciju - pričekajte prije ponovne objave", + "tag-too-short": "Unesite dužu oznaku. Oznake moraju sadržavati minimalno %1 znakova", + "tag-too-long": "Unesite kraću oznaku. Oznake moraju sadržavati maksimalno %1 znakova", + "not-enough-tags": "Nema dovoljno oznaka. Teme moraju imate bar %1 oznaku", + "too-many-tags": "Previše oznaka. Teme ne mogu imati više od %1 oznaka", + "still-uploading": "Pričekajte da se prijenos završi.", + "file-too-big": "Maksimalna veličina datoteke je %1 kB - učitajte manju datoteku", + "guest-upload-disabled": "Učitavanje datoteka za goste je isključeno", + "already-bookmarked": "Već ste zabilježili ovu objavu", + "already-unbookmarked": "Već ste odbilježili ovu objavu", + "cant-ban-other-admins": "Nemožete blokirati ostale administratore!", + "cant-remove-last-admin": "Vi ste jedini administrator. Dodajte korisnika kao administratora prije nego sebe odjavite kao administratora.", + "cant-delete-admin": "Ukloni administratorske privilegije sa ovog računa prije brisanja.", + "invalid-image-type": "Pogrešan format slike. Dozvoljeni formati: %1", + "invalid-image-extension": "Kriva ekstezija slike", + "invalid-file-type": "Netočan tip datoteke. Dozvoljeni formati su: %1", + "group-name-too-short": "Prekratko ime grupe", + "group-name-too-long": "Predugo ime Grupe", + "group-already-exists": "Grupa postoji", + "group-name-change-not-allowed": "Promjena imena grupe nije dozvoljena", + "group-already-member": "Već ste član ove grupe", + "group-not-member": "Niste član ove grupe", + "group-needs-owner": "Ova grupa zahtjeva bar jednog vlasnika", + "group-already-invited": "Ovaj korisnik je već pozvan", + "group-already-requested": "Vaš zahtjev za članstvom je već podnesen", + "post-already-deleted": "Ova objava je već obrisana", + "post-already-restored": "Ova objava je povraćena", + "topic-already-deleted": "Ova tema je već obrisana", + "topic-already-restored": "Ova tema je povraćena", + "cant-purge-main-post": "Nemožete odbaciti glavnu objavu, obrišite temu za brisanje", + "topic-thumbnails-are-disabled": "Slike tema su onemogućene", + "invalid-file": "Pogrešna datoteka", + "uploads-are-disabled": "Pohrana je onemogućena", + "signature-too-long": "Vaš potpis neže biti duži od %1 znaka", + "about-me-too-long": "O vama nemože biti duže od %1 znaka", + "cant-chat-with-yourself": "Nemoguće je razgovarati sam sa sobom!", + "chat-restricted": "Korisnik je ograničio razgovore. Mora vas pratiti prije nego možete razgovarati", + "chat-disabled": "Razgovor onemogućen", + "too-many-messages": "Poslali ste previše poruka, pričekajte.", + "invalid-chat-message": "Netočna poruka.", + "chat-message-too-long": "Poruka je preduga.Mora imati manje od %1 znakova", + "cant-edit-chat-message": "Nemate dopuštenje uređivati ovu poruku", + "cant-remove-last-user": "Ne možete obrisati zadnjeg korisnika", + "cant-delete-chat-message": "Nije dozvoljeno brisanje ove poruke", + "already-voting-for-this-post": "Već ste glasali za ovu objavu", + "reputation-system-disabled": "Sistem reputacije onemogućen.", + "downvoting-disabled": "Oduzimanje glasova je onemogućeno", + "not-enough-reputation-to-downvote": "Nemate dovoljno reputacije da bi ste glasali", + "not-enough-reputation-to-flag": "Nemate dovoljno reputacije da bi stavili zastavicu na ovu objavu", + "already-flagged": "Već ste označili zastavicom ovu objavu", + "reload-failed": "Problem kod ponovnog podizanja: \"%1\" will continue to serve the existing client-side assets.", + "registration-error": "Greška prilikom registracije", + "parse-error": "Došlo je do pogreške u komunikaciji sa serverom", + "wrong-login-type-email": "Upišite Vaš email za prijavu", + "wrong-login-type-username": "Upišite Vaše korisničko ime za prijavu", + "invite-maximum-met": "Pozvali ste maksimalan broj ljudi (%1 od %2).", + "no-session-found": "Nije pronađena sesija prijave!", + "not-in-room": "Korisnik nije u sobi", + "no-users-in-room": "Nema korisnika u sobi", + "cant-kick-self": "Ne možete sebe izbaciti iz grupe", + "no-users-selected": "Korisnici nisu odabrani", + "invalid-home-page-route": "Netočna putanja naslovnice", + "invalid-session": "Pogreška sesije", + "invalid-session-text": "Vaša sesija nije više aktivna ili se više ne poklapa sa serverom. Molimo osvježite stranicu." +} \ No newline at end of file diff --git a/public/language/hr/flags.json b/public/language/hr/flags.json new file mode 100644 index 0000000000..f86d22916c --- /dev/null +++ b/public/language/hr/flags.json @@ -0,0 +1,60 @@ +{ + "state": "Stanje", + "reporter": "Reporter", + "reported-at": "Prijavljeno u", + "description": "Opis", + "no-flags": "Huura! Nema pronađenih zastavica.", + "assignee": "Dodijeljeni", + "update": "Nadogradnja", + "updated": "Nadograđeno", + "target-purged": "Sadržaj koji je označen zastavom je odbačen i više nije dostupan.", + + "quick-filters": "Brzi filteri", + "filter-active": "Postoje jedan ili više filtera aktivnih u popisu zastava", + "filter-reset": "Ukloni filtere", + "filters": "Opcije filtera", + "filter-reporterId": "Reporter UID", + "filter-targetUid": "Flagged UID", + "filter-type": "Vrsta zastave", + "filter-type-all": "Sav sadržaj", + "filter-type-post": "Objave", + "filter-state": "Stanje", + "filter-assignee": "Asignee UID", + "filter-cid": "Kategorija", + "filter-quick-mine": "Dodijeljeno meni", + "filter-cid-all": "Sve kategorije", + "apply-filters": "Primjeni filtere", + + "quick-links": "Brze poveznice", + "flagged-user": "Označeni korisnici", + "view-profile": "Pogledaj profil", + "start-new-chat": "Pokreni novi razgovor", + "go-to-target": "Pogledaj metu zastave", + + "user-view": "Pogledaj profil", + "user-edit": "Uredi profil", + + "notes": "Bilješke zastave", + "add-note": "Dodaj bilješku", + "no-notes": "Nema podijeljenih bilješki", + + "history": "Povijest zastava", + "back": "Nazad na popis zastava", + "no-history": "Nema povijesti zastava.", + + "state-all": "Sva stanja", + "state-open": "Novo/Otvori", + "state-wip": "Rad u tijeku", + "state-resolved": "Riješeno", + "state-rejected": "Odbijeno", + "no-assignee": "Nije dodijeljeno", + "note-added": "Bilješka dodana", + + "modal-title": "Prijavi neprimjereni sadržaj", + "modal-body": "Navedite razlog označavanja zastavom %1 %2 .U suprotnom koristite jedan od dugmića za brzo prijavljivanje.", + "modal-reason-spam": "Spam", + "modal-reason-offensive": "Uvredljivo", + "modal-reason-custom": "Razlog prijavljivanja ovog sadržaja", + "modal-submit": "Podnesi izvještaj", + "modal-submit-success": "Ovaj sadržaj je označen zastavom u svrhu moderiranja," +} \ No newline at end of file diff --git a/public/language/hr/global.json b/public/language/hr/global.json new file mode 100644 index 0000000000..8d02d5bc58 --- /dev/null +++ b/public/language/hr/global.json @@ -0,0 +1,107 @@ +{ + "home": "Naslovna", + "search": "Pretraga", + "buttons.close": "Zatvori", + "403.title": "Pristup onemogućen", + "403.message": "Nemate pristup ovoj stranici .", + "403.login": "Pokušajte se prijaviti?", + "404.title": "Nije pronadjeno", + "404.message": "Ova stranica ne postoji. Vrati se na početnu.", + "500.title": "Interna greška.", + "500.message": "Ups! Čini se da nešto nije u redu.", + "400.title": "Krivi zahtjev.", + "400.message": "Izgleda da ovaj link nije ispravan, molimo provjerite i ponovo pokušajte. Ili se vratite na početnu stranicu.", + "register": "Registracija", + "login": "Prijava", + "please_log_in": "Molimo prijavite se.", + "logout": "Odjava", + "posting_restriction_info": "Objave su trenutačno omogućene samo registriranim korisnicima,kliknite ovdje za prijavu.", + "welcome_back": "Dobrodošli natrag", + "you_have_successfully_logged_in": "Uspješno ste se prijavili", + "save_changes": "Spremi promjene", + "save": "Spremi", + "close": "Zatvori", + "pagination": "Stranice", + "pagination.out_of": "%1 od %2", + "pagination.enter_index": "Unesi index", + "header.admin": "Admin", + "header.categories": "Kategorije", + "header.recent": "Posljednje", + "header.unread": "Nepročitano", + "header.tags": "Tagovi", + "header.popular": "Popularno", + "header.users": "Korisnici", + "header.groups": "Grupe", + "header.chats": "Razgovori", + "header.notifications": "Obavijesti", + "header.search": "Pretraga", + "header.profile": "Profil", + "header.navigation": "Navigacija", + "notifications.loading": "Učitavanje obavijesti", + "chats.loading": "Učitavam razgovore", + "motd.welcome": "Dobrodošli na Silicon Island Rijeka 2020 forum.", + "previouspage": "Prethodna stranica", + "nextpage": "Sljedeća stranica", + "alert.success": "Uspjeh!", + "alert.error": "Greška", + "alert.banned": "Blokiran", + "alert.banned.message": "Upravo ste banirani, sada ćete biti odjavljeni", + "alert.unfollow": "Više ne pratite %1", + "alert.follow": "Sada pratite %1", + "online": "Na mreži", + "users": "Korisnici", + "topics": "Teme", + "posts": "Objave", + "best": "Najbolje", + "upvoters": "Pozitivni glasači", + "upvoted": "Glasova za", + "downvoters": "Glasači protiv", + "downvoted": "Glasova protiv", + "views": "Pregleda", + "reputation": "Reputacija", + "read_more": "pročitaj više", + "more": "Više", + "posted_ago_by_guest": "postao gost prije %1", + "posted_ago_by": "postao %2 prije %1 ", + "posted_ago": "Objavljeno prije %1", + "posted_in": "Objavljeno u %1", + "posted_in_by": "Objavljeno u %1 od &2", + "posted_in_ago": "Objavljeno u %1 %2", + "posted_in_ago_by": "Objavljeno u %1 &2 od %3", + "user_posted_ago": "%1 je objavio %2", + "guest_posted_ago": "Gost je objavio %1", + "last_edited_by": "Zadnji put uređeno &1", + "norecentposts": "Nema nedavnih objava", + "norecenttopics": "Nema nedavnih tema", + "recentposts": "Posljednji postovi", + "recentips": "Posljednje prijavljeni IPovi", + "moderator_tools": "Moderatorski alati", + "away": "Odustan", + "dnd": "Ne smetaj", + "invisible": "Nevidljiv", + "offline": "Odjavljen", + "email": "Email", + "language": "Jezik", + "guest": "Gost", + "guests": "Gosti", + "updated.title": "Forum je nadograđen", + "updated.message": "Ovaj forum je upravo nadograđen na posljednju verziju. Klikni ovdje za ponovno učitavanje stranice.", + "privacy": "Privatnost", + "follow": "Prati", + "unfollow": "Prestani pratiti", + "delete_all": "Obriši sve", + "map": "Mapa", + "sessions": "Prijavljene sesije", + "ip_address": "IP adresa", + "enter_page_number": "Unesi broj stranice", + "upload_file": "Učitaj datoteku", + "upload": "Učitavanje", + "allowed-file-types": "Dozvoljeni tipovi datoteke su %1", + "unsaved-changes": "Imate nespremljenih promjena. Jeste li sigurni da želite napustiti stranicu?", + "reconnecting-message": "Izgleda da je veza na %1 prekinuta, molimo pričekajte dok se pokušamo ponovo spojiti.", + "play": "Pokreni", + "cookies.message": "Ova stranica koristi kolačiće kako bi osigurala najbolje korisničko iskustvo.", + "cookies.accept": "Shvaćam!", + "cookies.learn_more": "Saznaj više", + "edited": "Uređeno" +} \ No newline at end of file diff --git a/public/language/hr/groups.json b/public/language/hr/groups.json new file mode 100644 index 0000000000..a27c0cc6c9 --- /dev/null +++ b/public/language/hr/groups.json @@ -0,0 +1,58 @@ +{ + "groups": "Grupe", + "view_group": "Pogledaj grupu", + "owner": "Vlasnik grupe", + "new_group": "Napravi novu grupu", + "no_groups_found": "Nema grupa za pregled", + "pending.accept": "Prihvaćam", + "pending.reject": "Odbij", + "pending.accept_all": "Prihvati sve", + "pending.reject_all": "Odbij sve", + "pending.none": "Trenutno nema korisnika na čekanju", + "invited.none": "Trenutno nema pozvanih članova", + "invited.uninvite": "Povuci pozivnicu", + "invited.search": "Pretraži korisnike za poziv u grupu", + "invited.notification_title": "Pozvani ste da se pridružite%1", + "request.notification_title": "Zahtjev za pristup grupi od %1", + "request.notification_text": "%1 je poslao zahtjev da postane član %2", + "cover-save": "Spremi", + "cover-saving": "Spremanje", + "details.title": "Detalji Grupe", + "details.members": "Popis članova", + "details.pending": "Korisnici na čekanju", + "details.invited": "Pozvani korisnici", + "details.has_no_posts": "Članovi ove grupe nisu objavljivali.", + "details.latest_posts": "Zadnje objave", + "details.private": "Privatno", + "details.disableJoinRequests": "Onemogući zahtjeve za pristup", + "details.grant": "Dozvoli/Ukini vlasništvo", + "details.kick": "Izbaci", + "details.kick_confirm": "Jeste li sigurni da želite izbaciti ovog člana iz grupe?", + "details.owner_options": "\"Administracija grupe", + "details.group_name": "Ime grupe", + "details.member_count": "Broj članova", + "details.creation_date": "Kreirano", + "details.description": "Opis", + "details.badge_preview": "Prikaz značke", + "details.change_icon": "Promjeni ikonu", + "details.change_colour": "Promjeni boju", + "details.badge_text": "Tekst značke", + "details.userTitleEnabled": "Pokaži značku", + "details.private_help": "Ako je uključeno, ulazak korisnika u grupu zahtjeva odobrenje vlasnika grupe", + "details.hidden": "Sakriveno", + "details.hidden_help": "Ako je uključeno, ova grupa neće biti na popisu grupa i korisnici će morati biti pozvani ručno", + "details.delete_group": "Obriši ovu grupu", + "details.private_system_help": "Privatne grupe su isključene na sistemskoj razini", + "event.updated": "Detalji grupe su promjenjeni", + "event.deleted": "Grupa \\\"%1\\\" je obrisana", + "membership.accept-invitation": "Prihvati pozivnicu", + "membership.invitation-pending": "Pozivnica na čekanju", + "membership.join-group": "Priključi se u grupu", + "membership.leave-group": "Izađi iz grupe", + "membership.reject": "Odbij", + "new-group.group_name": "Ime grupe:", + "upload-group-cover": "Promjeni naslovnicu grupe", + "bulk-invite-instructions": "Unesi popis korisnika sa zarezima između korisničkih imena za poziv u ovu grupu", + "bulk-invite": "Masovni poziv", + "remove_group_cover_confirm": "Jeste li sigurni da želite obrisati sliku naslovnice?" +} \ No newline at end of file diff --git a/public/language/hr/language.json b/public/language/hr/language.json new file mode 100644 index 0000000000..bdc95e799c --- /dev/null +++ b/public/language/hr/language.json @@ -0,0 +1,5 @@ +{ + "name": "Hrvatski", + "code": "hr", + "dir": "ltr" +} \ No newline at end of file diff --git a/public/language/hr/login.json b/public/language/hr/login.json new file mode 100644 index 0000000000..53c6fe313b --- /dev/null +++ b/public/language/hr/login.json @@ -0,0 +1,12 @@ +{ + "username-email": "Korisničko ime / Email", + "username": "Korisničko ime", + "email": "Email", + "remember_me": "Zapamti me?", + "forgot_password": "Zaboravljena lozinka?", + "alternative_logins": "Alternativne prijave", + "failed_login_attempt": "Neuspješna prijava", + "login_successful": "Uspješno ste prijavljeni!", + "dont_have_account": "Nemate korisnički račun?", + "logged-out-due-to-inactivity": "Odjavljeni ste iz administratorske kontrolne ploče zbog neaktivnosti." +} \ No newline at end of file diff --git a/public/language/hr/modules.json b/public/language/hr/modules.json new file mode 100644 index 0000000000..90a85d3450 --- /dev/null +++ b/public/language/hr/modules.json @@ -0,0 +1,49 @@ +{ + "chat.chatting_with": "Razgovaraj sa ", + "chat.placeholder": "Upišite poruku ovdje, ENTER za slanje", + "chat.send": "Pošalji", + "chat.no_active": "Nemate aktivnih razgovora.", + "chat.user_typing": "%1 piše poruku ...", + "chat.user_has_messaged_you": "%1 vam je poslao poruku.", + "chat.see_all": "Pogledaj sve razgovore", + "chat.mark_all_read": "Označi sve razgovore kao pročitane", + "chat.no-messages": "Odaberite primatelja da vidite povijest razgovora", + "chat.no-users-in-room": "Nema korisnika u ovoj sobi", + "chat.recent-chats": "Nedavni razgovori", + "chat.contacts": "Kontakti", + "chat.message-history": "Povijest razgovora", + "chat.pop-out": "Pop out razgovor", + "chat.minimize": "Smanji", + "chat.maximize": "Povećaj", + "chat.seven_days": "7 Dana", + "chat.thirty_days": "30 Dana", + "chat.three_months": "3 Mjeseca", + "chat.delete_message_confirm": "Sigurni ste da želite izbrisati ovu poruku?", + "chat.add-users-to-room": "Dodaj korisnike u sobu", + "chat.confirm-chat-with-dnd-user": "Korisnik ne želi biti ometan. Jeste li sigurno da mu želite poslati poruku?", + "composer.compose": "Sastavi", + "composer.show_preview": "Prikaz", + "composer.hide_preview": "Sakrij prikaz", + "composer.user_said_in": "%1 je rekao u %2:", + "composer.user_said": "%1 je rekao:", + "composer.discard": "Sigurni ste da želite odbaciti ovu objavu?", + "composer.submit_and_lock": "Objavi i zaključaj", + "composer.toggle_dropdown": "Promjeni padajuće", + "composer.uploading": "Šaljem %1", + "composer.formatting.bold": "Bold", + "composer.formatting.italic": "Italic", + "composer.formatting.list": "Popis", + "composer.formatting.strikethrough": "Precrtano", + "composer.formatting.link": "Poveznica", + "composer.formatting.picture": "Slika", + "composer.upload-picture": "Učitaj sliku", + "composer.upload-file": "Učitaj datoteku", + "composer.zen_mode": "Zen", + "composer.select_category": "Odaberi kategoriju", + "bootbox.ok": "OK", + "bootbox.cancel": "Odbaci", + "bootbox.confirm": "Potvrdi", + "cover.dragging_title": "Pozicija naslovne slike", + "cover.dragging_message": "Povucite sliku na željenu poziciju i spremite \\\"Save\\\"", + "cover.saved": "Spremljeno" +} \ No newline at end of file diff --git a/public/language/hr/notifications.json b/public/language/hr/notifications.json new file mode 100644 index 0000000000..ba6c3cd24e --- /dev/null +++ b/public/language/hr/notifications.json @@ -0,0 +1,48 @@ +{ + "title": "Obavijesti", + "no_notifs": "Nema novih obavijesti", + "see_all": "Pogledaj sve obavijesti", + "mark_all_read": "Označi sve obavijesti kao pročitane", + "back_to_home": "Povratak na %1", + "outgoing_link": "Odlazna poveznica", + "outgoing_link_message": "Napuštate %1", + "continue_to": "Nastavite na %1", + "return_to": "Vratite se na %1", + "new_notification": "Nova obavijest", + "you_have_unread_notifications": "Nepročitane obavijesti.", + "all": "Sve", + "topics": "Teme", + "replies": "Odgovori", + "chat": "Razgovori", + "follows": "Pratitelji", + "upvote": "Glasači za", + "new-flags": "Nove zastave", + "my-flags": "Zastave označene na mene", + "bans": "Blokirani", + "new_message_from": "Poruka od %1", + "upvoted_your_post_in": "%1 je glasao za u %2.", + "upvoted_your_post_in_dual": "%1 i %2 Glasalo je za Vašu objavu in %3.", + "upvoted_your_post_in_multiple": "%1 i %2 ostalih glasalo je za Vašu objavu %3.", + "moved_your_post": "%1 je premjestio Vašu objavu u %2", + "moved_your_topic": "%1 je premjestio %2", + "user_flagged_post_in": "%1 je označio objavu u %2", + "user_flagged_post_in_dual": "%1 i %2 označio objavu u %3", + "user_flagged_post_in_multiple": "%1 i %2 ostalih označio objavu u %3", + "user_flagged_user": "%1 označio je profil (%2)", + "user_flagged_user_dual": "%1 i %2su označili profil (%3)", + "user_flagged_user_multiple": "%1 i %2 ostalih su označili korisnički profil (%3)", + "user_posted_to": "%1 i %2 su odgovorili na: %3", + "user_posted_to_dual": "%1 i %2 ostalih su odgovorili na objavu u: %3", + "user_posted_to_multiple": "%1 i %2 drugih su odgovorili na: %3", + "user_posted_topic": "%1 je otvorio novu temu: %2", + "user_started_following_you": "%1 Vas sada prati.", + "user_started_following_you_dual": "%1 i %2 vas sada prate.", + "user_started_following_you_multiple": "%1 i %2 ostalih vas sada prate.", + "new_register": "%1 je poslao zahtjev za registraciju.", + "new_register_multiple": "%1 registracija čeka odobrenje.", + "flag_assigned_to_you": "Zastava%1 je dodijeljena vama.", + "email-confirmed": "Email potvrđen", + "email-confirmed-message": "Hvala na potvrdi emaila. Vaš račun je sada aktivan.", + "email-confirm-error-message": "Nastao je problem pri potvrdi Vaše email adrese. Provjerite kod ili zatražite novi.", + "email-confirm-sent": "Provjera korisničkog emaila poslana." +} \ No newline at end of file diff --git a/public/language/hr/pages.json b/public/language/hr/pages.json new file mode 100644 index 0000000000..76cd7ae518 --- /dev/null +++ b/public/language/hr/pages.json @@ -0,0 +1,52 @@ +{ + "home": "Naslovna", + "unread": "Nepročitane teme", + "popular-day": "Popularne teme danas", + "popular-week": "Popularne teme ovaj tjedan", + "popular-month": "Popularne teme ovaj mjesec", + "popular-alltime": "Najpopularnije teme ", + "recent": "Nedavne teme", + "flagged-content": "Označene objave", + "ip-blacklist": "IP crna lista", + "users/online": "Online korisnici", + "users/latest": "Posljednji korisnici", + "users/sort-posts": "Korisnici s najviše objava", + "users/sort-reputation": "Korisnici s najvećom reputacijom", + "users/banned": "Blokirani korisnici", + "users/most-flags": "Najviše označeni korisnici", + "users/search": "Pretraga korisnika", + "notifications": "Obavijesti", + "tags": "Oznake", + "tag": "Teme označene pod \\\"%1\\\"", + "register": "Registrirajte se", + "registration-complete": "Registracija uspješna", + "login": "Prijavite se na Vaš račun", + "reset": "Promijenite lozinku", + "categories": "Kategorije", + "groups": "Grupe", + "group": "%1 grupa", + "chats": "Razgovori", + "chat": "Razgovor s %1", + "flags": "Zastave", + "flag-details": "Detalji zastave %1", + "account/edit": "Uređivanje \\\"%1\\\"", + "account/edit/password": "Uređivanje lozinke \\\"%1\\", + "account/edit/username": "Uređivanje korisnika \\\"%1\\\"", + "account/edit/email": "Uređivanje email \\\"%1\\\"", + "account/info": "Informacija o računu", + "account/following": "Ljudi %1 prati", + "account/followers": "Ljudi koji prate %1", + "account/posts": "Objavio %1", + "account/topics": "Teme od %1", + "account/groups": "%1 grupe", + "account/bookmarks": "%1 zabilježene objave", + "account/settings": "Korisničke postavke", + "account/watched": "Teme prati %1", + "account/upvoted": "%1 glasao za", + "account/downvoted": "%1 glasao protiv", + "account/best": "Najbolje objave od %1", + "confirm": "Email potvrđen!", + "maintenance.text": "%1 Održavanje u toku. Posjetite nas uskoro.", + "maintenance.messageIntro": "Poruka administratora:", + "throttled.text": "%1: Preopterećenje sustava. Pričekajte nekoliko trenutaka." +} \ No newline at end of file diff --git a/public/language/hr/recent.json b/public/language/hr/recent.json new file mode 100644 index 0000000000..ac247883e2 --- /dev/null +++ b/public/language/hr/recent.json @@ -0,0 +1,19 @@ +{ + "title": "Nedavno", + "day": "Dan", + "week": "Tjedan", + "month": "Mjesec", + "year": "Godina", + "alltime": "Sve vrijeme", + "no_recent_topics": "Nema nedavnih tema.", + "no_popular_topics": "Nema popularnih tema.", + "there-is-a-new-topic": "Nova tema.", + "there-is-a-new-topic-and-a-new-post": "Nova tema i nova objava.", + "there-is-a-new-topic-and-new-posts": "Nova tema i %1 nova objava", + "there-are-new-topics": "%1 nova tema", + "there-are-new-topics-and-a-new-post": "%1 nova tema i nova objava", + "there-are-new-topics-and-new-posts": "%1 nova tema i %2 nova objava", + "there-is-a-new-post": "Nova objava.", + "there-are-new-posts": "%1 nova objava.", + "click-here-to-reload": "Klikni ovdje za ponovno učitavanje." +} \ No newline at end of file diff --git a/public/language/hr/register.json b/public/language/hr/register.json new file mode 100644 index 0000000000..a518352cf3 --- /dev/null +++ b/public/language/hr/register.json @@ -0,0 +1,23 @@ +{ + "register": "Registracija", + "cancel_registration": "Obustavi registraciju", + "help.email": "Vaš email će biti skriven od javnosti.", + "help.username_restrictions": "Unikatno korisničko ime između %1 i %2 znaka. Ostali Vas mogu spomenuti sa @username.", + "help.minimum_password_length": "Dužina lozinke mora biti %1 znakova.", + "email_address": "Email adresa", + "email_address_placeholder": "Unesite email adresu", + "username": "Korisničko ime", + "username_placeholder": "Unesite korisničko ime", + "password": "Lozinka", + "password_placeholder": "Unesite lozinku", + "confirm_password": "Potvrdite lozinku", + "confirm_password_placeholder": "Potvrdite lozinku", + "register_now_button": "Registrirajte se", + "alternative_registration": "Alternativna registracija:", + "terms_of_use": "Uvjeti korištenja", + "agree_to_terms_of_use": "Prihvaćam uvjete korištenja", + "terms_of_use_error": "Morate prihvatiti uvjete korištenja", + "registration-added-to-queue": "Vaša registracija je dodana u listu zahtjeva za registraciju. Biti ćete obaviješteni kad Vas administrator prihvati.", + "interstitial.intro": "Treba nam još par dodatnih informacija prije nego kreirate novi račun.", + "interstitial.errors-found": "Nismo uspijeli dovršiti registraciju:" +} \ No newline at end of file diff --git a/public/language/hr/reset_password.json b/public/language/hr/reset_password.json new file mode 100644 index 0000000000..a815beb932 --- /dev/null +++ b/public/language/hr/reset_password.json @@ -0,0 +1,17 @@ +{ + "reset_password": "Resetiranje lozinke", + "update_password": "Promjeni lozinku", + "password_changed.title": "Lozinka promijenjena", + "password_changed.message": "

Lozink uspješno promijenjena, prijavite se ponovno!.", + "wrong_reset_code.title": "Netočan kod za resetiranje", + "wrong_reset_code.message": "Netočan kod za resetiranje. Probaj ponovno ili zatraži novi kod.", + "new_password": "Nova lozinka", + "repeat_password": "Potvrdi lozinku", + "enter_email": "Unesite Vašu email adresu i poslati ćemo Vam email sa uputstvima kako resetirati lozinku.", + "enter_email_address": "Unesite email adresu", + "password_reset_sent": "Poslan je zahtjev za resetiranje lozinke", + "invalid_email": "Netočan email / email ne postoji!", + "password_too_short": "Lozinka koju ste unijeli je prekratka, izaberite drugu lozinku.", + "passwords_do_not_match": "Lozinke se ne podudaraju!", + "password_expired": "Vaša lozinka je istekla, izaberite novu lozinku" +} \ No newline at end of file diff --git a/public/language/hr/search.json b/public/language/hr/search.json new file mode 100644 index 0000000000..a2e5fddedd --- /dev/null +++ b/public/language/hr/search.json @@ -0,0 +1,42 @@ +{ + "results_matching": "%1 rezultat odgovara \"%2\", (%3 sekunde)", + "no-matches": "Nema rezultata", + "advanced-search": "Napredna pretraga", + "in": "U", + "titles": "Naslovi", + "titles-posts": "Naslovi i objave", + "posted-by": "Objavio", + "in-categories": "U kategoriji", + "search-child-categories": "Pretraži podkategorije", + "has-tags": "Ima oznake", + "reply-count": "Broj odgovora", + "at-least": "Najmanje", + "at-most": "Najviše", + "relevance": "Relevantno", + "post-time": "Vrijeme objave", + "newer-than": "Novije od", + "older-than": "Starije od", + "any-date": "Bilo kada", + "yesterday": "Jučer", + "one-week": "Tjedan", + "two-weeks": "Dva tjedna", + "one-month": "Mjesec", + "three-months": "Tri mjeseca", + "six-months": "Šest mjeseci", + "one-year": "Godina", + "sort-by": "Sortiraj po", + "last-reply-time": "Vrijeme zadnje odgovora", + "topic-title": "Naslov teme", + "number-of-replies": "Broj odgovora", + "number-of-views": "Broj pogleda", + "topic-start-date": "Početak teme", + "username": "Korisničko ime", + "category": "Kategorija", + "descending": "U silaznom redu", + "ascending": "Po uzlaznom redu", + "save-preferences": "Spremi postavke", + "clear-preferences": "Očisti postavke", + "search-preferences-saved": "Postavke pretraživanja spremljene", + "search-preferences-cleared": "Postavke pretraživanja očišćene ", + "show-results-as": "Prikaži rezultate kao" +} \ No newline at end of file diff --git a/public/language/hr/success.json b/public/language/hr/success.json new file mode 100644 index 0000000000..55b303fba9 --- /dev/null +++ b/public/language/hr/success.json @@ -0,0 +1,6 @@ +{ + "success": "Uspijeh", + "topic-post": "Uspješna objava", + "authentication-successful": "Autentifikacija uspješna", + "settings-saved": "Postavke spremljene!" +} \ No newline at end of file diff --git a/public/language/hr/tags.json b/public/language/hr/tags.json new file mode 100644 index 0000000000..ba0b9b5495 --- /dev/null +++ b/public/language/hr/tags.json @@ -0,0 +1,7 @@ +{ + "no_tag_topics": "Nema tema sa ovom oznakom", + "tags": "Oznake", + "enter_tags_here": "Unesite oznake, između %1 i %2 znaka.", + "enter_tags_here_short": "Unestie oznake ...", + "no_tags": "Još nema oznaka." +} \ No newline at end of file diff --git a/public/language/hr/topic.json b/public/language/hr/topic.json new file mode 100644 index 0000000000..45b25f3e6c --- /dev/null +++ b/public/language/hr/topic.json @@ -0,0 +1,119 @@ +{ + "topic": "Tema", + "topic_id": "ID Teme", + "topic_id_placeholder": "Unesite ID teme", + "no_topics_found": "Tema nije pronađena!", + "no_posts_found": "Objave nisu pronađene!", + "post_is_deleted": "Ova objava je obrisana!", + "topic_is_deleted": "Ova tema je obrisana!", + "profile": "Profil", + "posted_by": "Objavio %1", + "posted_by_guest": "Objavio gost", + "chat": "Razgovor", + "notify_me": "Budi obavješten o novim odgovorima na ovu temu", + "quote": "Citat", + "reply": "Odgovor", + "replies_to_this_post": "%1 je odgovorio", + "last_reply_time": "Zadnji odgovor", + "reply-as-topic": "Odgovori kao temu", + "guest-login-reply": "Prijavi se za objavu", + "edit": "Uredi", + "delete": "Obriši", + "purge": "Očisti sve", + "restore": "Obnovi", + "move": "Premjesti", + "fork": "Dupliraj", + "link": "Poveznica", + "share": "Podijeli", + "tools": "Alati", + "locked": "Zaključano", + "pinned": "Zakačeno", + "moved": "Premješteno", + "bookmark_instructions": "Klikni ovdje za povratak na zadnji pročitani post.", + "flag_title": "Označi ovaj post za zastavom za moderaciju", + "deleted_message": "Ova tema je obrisana. Samo korisnici sa privilegijom upravljanja tema je mogu vidjeti.", + "following_topic.message": "Od sada ćete primati obavijesti kada netko objavi objavu u ovoj temi.", + "not_following_topic.message": "Od sada ćete vidjeti ovu temu u popisu nepročitanih tema,ali nećete dobivati obavijesti kada netko objavi objavu u temi.", + "ignoring_topic.message": "Od sada više nećete vidjeti ovu temu u popisu nepročitanih tema.Bit će te obaviješteni kada ste spomenuti ili je netko glasao za vašu objavu.", + "login_to_subscribe": "Registriraj se ili prijavi kako bi se mogao pretplatit na ovu temu.", + "markAsUnreadForAll.success": "Tema označena kao nepročitana za sve.", + "mark_unread": "Označi kao nepročitano", + "mark_unread.success": "Tema označena kao nepročitana", + "watch": "Prati", + "unwatch": "Prestani pratiti", + "watch.title": "Budi obaviješten o novim objavama u ovoj temi", + "unwatch.title": "Prestani pratiti ovu temu", + "share_this_post": "Podijeli ovu objavu", + "watching": "Prati", + "not-watching": "Ne pratiš", + "ignoring": "Ignoriraš", + "watching.description": "Obavijesti me o novim odgovorima .
Prikaži temu u nepročitanim ako kategorija nije ignorirana.", + "ignoring.description": "Nemoj slati obavijesti o novim odgovorima.
Ne prikazuj temu u nepročitanom.", + "thread_tools.title": "Alati teme", + "thread_tools.markAsUnreadForAll": "Označi kao nepročitano za sve", + "thread_tools.pin": "Zakači temu", + "thread_tools.unpin": "Otkači temu", + "thread_tools.lock": "Zaključaj temu", + "thread_tools.unlock": "Odključaj temu", + "thread_tools.move": "Premjesti temu", + "thread_tools.move_all": "Premjesti sve", + "thread_tools.fork": "Dupliraj temu", + "thread_tools.delete": "Obriši temu", + "thread_tools.delete-posts": "Obriši objavu", + "thread_tools.delete_confirm": "Sigurni ste da želite obrisati ovu temu?", + "thread_tools.restore": "Povrati temu", + "thread_tools.restore_confirm": "Sigurni ste da želite povratiti ovu temu?", + "thread_tools.purge": "Odbaci temu", + "thread_tools.purge_confirm": "Sigurni ste da želite odbaciti ovu temu?", + "topic_move_success": "Tema je uspješno pomaknuta u %1", + "post_delete_confirm": "Sigurni ste da želite obrisati ovu objavu?", + "post_restore_confirm": "Sigurni ste da želite povratiti ovu objavu?", + "post_purge_confirm": "Sigurni ste da želite odbaciti ovu objavu?", + "load_categories": "Učitavam kategorije", + "disabled_categories_note": "Onemogućene kategorije su izbljeđene", + "confirm_move": "Pomakni", + "confirm_fork": "Dupliraj", + "bookmark": "Zabilježi", + "bookmarks": "Zabilješke", + "bookmarks.has_no_bookmarks": "Nemate zabiježenih objava.", + "loading_more_posts": "Učitavam više objava", + "move_topic": "Pomakni temu", + "move_topics": "Pomakni teme", + "move_post": "Pomakni objavu", + "post_moved": "Objava pomaknuta!", + "fork_topic": "Dupliraj temu", + "topic_will_be_moved_to": "Ova tem će biti pomaknuta u kategoriju", + "fork_topic_instruction": "Označi objave koje želite duplirati", + "fork_no_pids": "Objave nisu odabrane!", + "fork_pid_count": "%1 objava odabrana", + "fork_success": "Uspješno duplirana tema. Kliknite ovdje za dupliranu temu.", + "delete_posts_instruction": "Označite objave koje želite obrisati/odbaciti", + "composer.title_placeholder": "Unesite naslov teme ovdje ...", + "composer.handle_placeholder": "Ime", + "composer.discard": "Odbaci", + "composer.submit": "Podnesi", + "composer.replying_to": "Odgovori na %1", + "composer.new_topic": "Nova tema", + "composer.uploading": "slanje...", + "composer.thumb_url_label": "Zaljepite URL slike za temu", + "composer.thumb_title": "Dodajte slike ovoj temi", + "composer.thumb_url_placeholder": "http://example.com/thumb.png", + "composer.thumb_file_label": "Ili učitajte datoteku", + "composer.thumb_remove": "Očisti polja", + "composer.drag_and_drop_images": "Dovuci i pusti sliku ovdje", + "more_users_and_guests": "%1 korisnik i %2 gosta", + "more_users": "%1 korisnik", + "more_guests": "%1 gost", + "users_and_others": "%1 i %2 druga", + "sort_by": "Sortitaj po", + "oldest_to_newest": "Starije prema Novom", + "newest_to_oldest": "Novije prema Starom", + "most_votes": "Najviše glasova", + "most_posts": "Najviše objava", + "stale.title": "Otvori novu temu?", + "stale.warning": "Tema na koju odgovarate je stara. Želite li otvoriti novu temu i postaviti referencu u vašem odgovoru?", + "stale.create": "Otvori novu temu", + "stale.reply_anyway": "Odgovori na ovu temu svejedno", + "link_back": "Re: [%1](%2)" +} \ No newline at end of file diff --git a/public/language/hr/unread.json b/public/language/hr/unread.json new file mode 100644 index 0000000000..2d8bd2128a --- /dev/null +++ b/public/language/hr/unread.json @@ -0,0 +1,13 @@ +{ + "title": "Nepročitano", + "no_unread_topics": "Sve teme su pročitane", + "load_more": "Učitaj više", + "mark_as_read": "Označi kao pročitano", + "selected": "Odabrano", + "all": "Sve", + "all_categories": "Sve kategorije", + "topics_marked_as_read.success": "Teme označene kao pročitane!", + "all-topics": "Sve teme", + "new-topics": "Nove teme", + "watched-topics": "Praćene teme" +} \ No newline at end of file diff --git a/public/language/hr/uploads.json b/public/language/hr/uploads.json new file mode 100644 index 0000000000..6a3655ad9b --- /dev/null +++ b/public/language/hr/uploads.json @@ -0,0 +1,6 @@ +{ + "uploading-file": "Učitavam datoteku ...", + "select-file-to-upload": "Izaberite datoteku!", + "upload-success": "Prijenos datoteka uspješan!", + "maximum-file-size": "Maksimum %1 kb" +} \ No newline at end of file diff --git a/public/language/hr/user.json b/public/language/hr/user.json new file mode 100644 index 0000000000..20ad62ec30 --- /dev/null +++ b/public/language/hr/user.json @@ -0,0 +1,136 @@ +{ + "banned": "Blokiran", + "offline": "Nije na mreži", + "username": "Korisničko ime", + "joindate": "Datum prijave", + "postcount": "Broj objava", + "email": "Email", + "confirm_email": "Potvrdi email", + "account_info": "Informacije o računu", + "ban_account": "Blokiraj račun", + "ban_account_confirm": "Da li zaista želite blokirati ovog korisnika", + "unban_account": "Odblokiraj račun", + "delete_account": "Obriši račun", + "delete_account_confirm": "Jeste li sigurni da želite obrisati vaš račun?
Ova radnja je nepovratna i nećete više moći pristupiti vašim podacima

Upišite svoje korisničko ime za potvrdu brisanja računa.", + "delete_this_account_confirm": "Jeste li sigurni da želite obrisati vaš račun?
Ova radnja je nepovratna i nećete više moći pristupiti vašim podacima

", + "account-deleted": "Račun obrisan", + "fullname": "Puno ime", + "website": "Web stranica", + "location": "Lokacija", + "age": "Dob", + "joined": "Priključio", + "lastonline": "Viđen na mreži", + "profile": "Profil", + "profile_views": "Pregled profila", + "reputation": "Reputacija", + "bookmarks": "Zabilješke", + "watched": "Gledano", + "followers": "Pratitelji", + "following": "Prati", + "aboutme": "O meni", + "signature": "Potpis", + "birthday": "Rođendan", + "chat": "Razgovor", + "chat_with": "Nastavi razgovor sa %1!", + "new_chat_with": "Pokreni novi razgovor sa %1", + "flag-profile": "Označi profil", + "follow": "Prati", + "unfollow": "Prestani pratiti", + "more": "Više", + "profile_update_success": "Profil je uspješno promijenjen!", + "change_picture": "Promjeni sliku", + "change_username": "Promjeni korisničko ime", + "change_email": "Promjeni email", + "edit": "Uredi", + "edit-profile": "Uredi profil", + "default_picture": "Zadana ikona", + "uploaded_picture": "Učitaj sliku", + "upload_new_picture": "Učitaj novu sliku", + "upload_new_picture_from_url": "Učitaj sliku iz URL", + "current_password": "Sadašnja lozinka", + "change_password": "Promjeni lozinku", + "change_password_error": "Netočna lozinka!", + "change_password_error_wrong_current": "Vaša trenutačna lozinka nije točna!", + "change_password_error_length": "Lozinka prekratka!", + "change_password_error_match": "Lozinke se moraju podudarati!", + "change_password_error_privileges": "Nemate pravo mijenjati ovu lozinku.", + "change_password_success": "Vaša lozinka je promijenjena!", + "confirm_password": "Potvrdi lozinku", + "password": "Lozinka", + "username_taken_workaround": "Korisničko ime koje ste izabrali je već zauzeto. Zbog toga smo ga malo promjenili. Sada je vaše korisničko ime%1", + "password_same_as_username": "Vaša lozinka je ista kao i vaše korisničko ime, molimo upišite drugu lozinku.", + "password_same_as_email": "Vaša lozinka je ista kao vaš email, molimo upišite drugu lozinku.", + "weak_password": "Slaba lozinka", + "upload_picture": "Učitaj sliku", + "upload_a_picture": "Učitaj sliku", + "remove_uploaded_picture": "Ukloni učitanu sliku", + "upload_cover_picture": "Učitaj naslovnu sliku", + "remove_cover_picture_confirm": "Jeste li sigurno da želite ukloniti naslovnu sliku", + "crop_picture": "Skratite sliku", + "upload_cropped_picture": "Skrati i učitaj", + "settings": "Postavke", + "show_email": "Prikaži email", + "show_fullname": "Prikaži puno ime", + "restrict_chats": "Dopusti poruke o korisnika koje pratim", + "digest_label": "Pretplati se na izvještaje", + "digest_description": "Pretplati se na email izvještaje od ovog foruma (nove obavjesti i teme) prema zadanom rasporedu", + "digest_off": "Isključi", + "digest_daily": "Dnevno", + "digest_weekly": "Tjedno", + "digest_monthly": "Mjesečno", + "send_chat_notifications": "Pošalji email ako stigne nova poruka i nisam na mreži", + "send_post_notifications": "Pošalji email kada se objavi odgovor na teme koje sam pretplaćen", + "settings-require-reload": "Neke promjene zahtjevaju osvježenje. Kliknite ovdje za osvježavanje stranice.", + "has_no_follower": "Ovaj korisnik nema pratitelja :(.", + "follows_no_one": "Ovaj korisnik nikog ne prati :(", + "has_no_posts": "Ovaj korisnik nema objava.", + "has_no_topics": "Ovaj korisnik nema objavljenih tema.", + "has_no_watched_topics": "Ovaj korisnik ne prati teme.", + "has_no_upvoted_posts": "Ovaj korisnik nije glasao za na objavama.", + "has_no_downvoted_posts": "Ovaj korisnik nije glasao protiv na objavama.", + "has_no_voted_posts": "Ovaj korisnik nema glasanih objava", + "email_hidden": "Email sakriven", + "hidden": "Sakriven", + "paginate_description": "Numeriraj teme i objave umjesto scrollanja", + "topics_per_page": "Teme po stranici", + "posts_per_page": "Objave po stranici", + "notification_sounds": "Sviraj zvuk kada primim obavijest", + "notifications_and_sounds": "Obavijesti i zvukovi", + "incoming-message-sound": "Zvuk nadolazećih poruka", + "outgoing-message-sound": "Zvuk odlazećih poruka", + "notification-sound": "Zvuk obavijesti", + "no-sound": "Bez zvuka", + "browsing": "Postavke pretraživanja", + "open_links_in_new_tab": "Otvori odlazne poveznice u novom tabu", + "enable_topic_searching": "Omogući pretragu unutar tema", + "topic_search_help": "Ako uključeno,pretraga unutar tema će zamijeniti pretragu ključnih riječi vašeg pretraživača kojemu je omogućeno pretraživanje samo onoga što je na ekranu,za razliku od ove opcije koja omogućava pretragu na cijeloj temi", + "delay_image_loading": "Odgodi učitavanje slika.", + "image_load_delay_help": "Ako omogućeno,slike u ovoj temi se neće učitavati dok nisu u potpunosti unutar ruba ekrana.", + "scroll_to_my_post": "Nakon objavljivanja,prikaži objavu", + "follow_topics_you_reply_to": "Prati teme na koje objavljuješ", + "follow_topics_you_create": "Prati teme koje si napravio", + "grouptitle": "Ime Grupe", + "no-group-title": "Nema imena grupe", + "select-skin": "Izaberi izgled", + "select-homepage": "Izaberi naslovnu", + "homepage": "Naslovna", + "homepage_description": "Izaberi stranicu ", + "custom_route": "Uobičajena putanja naslovnice", + "custom_route_help": "Izaberi ime putanje ovdje", + "sso.title": "Jednokratne usluge prijave", + "sso.associated": "Povezano sa", + "sso.not-associated": "Klikni ovdje za povezivanje sa", + "info.latest-flags": "Zadnja zastava", + "info.no-flags": "Nema objava sa zastavama", + "info.ban-history": "Povijest nedavno blokiranih", + "info.no-ban-history": "Ovaj korisnik nikad nije bio blokiran", + "info.banned-until": "Blokiran do %1!", + "info.banned-permanently": "Trajno blokiran", + "info.banned-reason-label": "Razlog", + "info.banned-no-reason": "Razlog nije dan.", + "info.username-history": "Povijest korisničkog imena", + "info.email-history": "Povijest emaila", + "info.moderation-note": "Poruka moderiranja", + "info.moderation-note.success": "Poruka moderiranja spremljena", + "info.moderation-note.add": "Dodaj bilješku" +} \ No newline at end of file diff --git a/public/language/hr/users.json b/public/language/hr/users.json new file mode 100644 index 0000000000..cafe2c0412 --- /dev/null +++ b/public/language/hr/users.json @@ -0,0 +1,21 @@ +{ + "latest_users": "Posljednji korisnici", + "top_posters": "Najviše objava", + "most_reputation": "Najveća reputacija", + "most_flags": "Najviše zastava", + "search": "Pretraga", + "enter_username": "Unesi korisničko ime za pretragu", + "load_more": "Učitaj više", + "users-found-search-took": "%1user(s) pronađeni! Pretraga je trajala %2 sekundi.", + "filter-by": "Filtriraj po", + "online-only": "Samo na mreži", + "invite": "Pozovi", + "invitation-email-sent": "Pozivnica poslana %1", + "user_list": "Popis korisnika", + "recent_topics": "Zadnje teme", + "popular_topics": "Popularne teme", + "unread_topics": "Nepročitane teme", + "categories": "Kategorije", + "tags": "Tagovi", + "no-users-found": "Korisnici nisu pronađeni!" +} \ No newline at end of file From 064a8aa6d73ac4e015871b9dece009f581c0deea Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 11 May 2017 11:41:28 -0400 Subject: [PATCH 054/263] Allowing custom date range to be supplied in pageviews graph, closes #5541 --- .../en-GB/admin/general/dashboard.json | 11 +++- public/less/admin/general/dashboard.less | 2 - public/src/admin/general/dashboard.js | 66 +++++++++++++++++-- public/src/utils.js | 4 +- src/socket.io/admin.js | 10 +-- src/views/admin/general/dashboard.tpl | 28 ++++---- .../admin/partials/pageviews-range-select.tpl | 20 ++++++ 7 files changed, 115 insertions(+), 26 deletions(-) create mode 100644 src/views/admin/partials/pageviews-range-select.tpl diff --git a/public/language/en-GB/admin/general/dashboard.json b/public/language/en-GB/admin/general/dashboard.json index 02046bd17a..437e8e2461 100644 --- a/public/language/en-GB/admin/general/dashboard.json +++ b/public/language/en-GB/admin/general/dashboard.json @@ -5,9 +5,14 @@ "users": "Users", "posts": "Posts", "topics": "Topics", - "page-views-last-month": "Page views Last Month", - "page-views-this-month": "Page views This Month", - "page-views-last-day": "Page views in last 24 hours", + "page-views-last-month": "Last Month", + "page-views-this-month": "This Month", + "page-views-last-day": "Last 24 hours", + "page-views-custom": "Custom Date Range", + "page-views-custom-start": "Range Start", + "page-views-custom-end": "Range End", + "page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is YYYY-MM-DD", + "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", "stats.day": "Day", "stats.week": "Week", diff --git a/public/less/admin/general/dashboard.less b/public/less/admin/general/dashboard.less index 12e542e32a..6bbb49fc86 100644 --- a/public/less/admin/general/dashboard.less +++ b/public/less/admin/general/dashboard.less @@ -130,8 +130,6 @@ } .pageview-stats { - width:33%; - strong { font-size: 22px; } diff --git a/public/src/admin/general/dashboard.js b/public/src/admin/general/dashboard.js index 71ad4e3738..6b88483ac0 100644 --- a/public/src/admin/general/dashboard.js +++ b/public/src/admin/general/dashboard.js @@ -294,7 +294,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s $(window).on('resize', adjustPieCharts); adjustPieCharts(); - $('[data-action="updateGraph"]').on('click', function () { + $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () { var until; switch ($(this).attr('data-until')) { case 'last-month': @@ -305,6 +305,60 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s updateTrafficGraph($(this).attr('data-units'), until); $('[data-action="updateGraph"]').removeClass('active'); $(this).addClass('active'); + + require(['translator'], function (translator) { + translator.translate('[[admin/general/dashboard:page-views-custom]]', function (translated) { + $('[data-action="updateGraph"][data-units="custom"]').text(translated); + }); + }); + }); + $('[data-action="updateGraph"][data-units="custom"]').on('click', function () { + var targetEl = $(this); + + templates.parse('admin/partials/pageviews-range-select', {}, function (html) { + var modal = bootbox.dialog({ + title: '[[admin/general/dashboard:page-views-custom]]', + message: html, + buttons: { + submit: { + label: '[[global:search]]', + className: 'btn-primary', + callback: submit, + }, + }, + }); + + function submit() { + // NEED TO ADD VALIDATION HERE FOR YYYY-MM-DD + var formData = modal.find('form').serializeObject(); + var validRegexp = /\d{4}-\d{2}-\d{2}/; + + // Input validation + if (!formData.startRange && !formData.endRange) { + // No range? Assume last 30 days + updateTrafficGraph('days'); + $('[data-action="updateGraph"]').removeClass('active'); + $('[data-action="updateGraph"][data-units="days"]').addClass('active'); + return; + } else if (!validRegexp.test(formData.startRange) || !validRegexp.test(formData.endRange)) { + // Invalid Input + modal.find('.alert-danger').removeClass('hidden'); + return false; + } + + var until = new Date(formData.endRange); + until.setDate(until.getDate() + 1); + until = until.getTime(); + var amount = (until - new Date(formData.startRange).getTime()) / (1000 * 60 * 60 * 24); + + updateTrafficGraph('days', until, amount); + $('[data-action="updateGraph"]').removeClass('active'); + targetEl.addClass('active'); + + // Update "custom range" label + targetEl.html(formData.startRange + ' – ' + formData.endRange); + } + }); }); socket.emit('admin.rooms.getAll', Admin.updateRoomUsage); @@ -325,7 +379,9 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s }); } - function updateTrafficGraph(units, until) { + function updateTrafficGraph(units, until, amount) { + // until and amount are optional + if (!app.isFocused) { return; } @@ -334,6 +390,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s graph: 'traffic', units: units || 'hours', until: until, + amount: amount, }, function (err, data) { if (err) { return app.alertError(err.message); @@ -345,7 +402,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s graphData.traffic = data; if (units === 'days') { - graphs.traffic.data.xLabels = utils.getDaysArray(until); + graphs.traffic.data.xLabels = utils.getDaysArray(until, amount); } else { graphs.traffic.data.xLabels = utils.getHoursArray(); @@ -364,6 +421,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s graphs.traffic.update(); currentGraph.units = units; currentGraph.until = until; + currentGraph.amount = amount; }); } @@ -450,7 +508,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.roomInterval); intervals.graphs = setInterval(function () { - updateTrafficGraph(currentGraph.units, currentGraph.until); + updateTrafficGraph(currentGraph.units, currentGraph.until, currentGraph.amount); }, realtime ? DEFAULTS.realtimeInterval : DEFAULTS.graphInterval); } diff --git a/public/src/utils.js b/public/src/utils.js index 7264bfcf8f..3a73c5fdd6 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -274,13 +274,13 @@ return labels.reverse(); }, - getDaysArray: function (from) { + getDaysArray: function (from, amount) { var currentDay = new Date(from || Date.now()).getTime(); var months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; var labels = []; var tmpDate; - for (var x = 29; x >= 0; x -= 1) { + for (var x = (amount || 30) - 1; x >= 0; x -= 1) { tmpDate = new Date(currentDay - (1000 * 60 * 60 * 24 * x)); labels.push(months[tmpDate.getMonth()] + ' ' + tmpDate.getDate()); } diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 1b4d9ebada..2cdaa0161b 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -211,10 +211,12 @@ SocketAdmin.analytics.get = function (socket, data, callback) { } // Default returns views from past 24 hours, by hour - if (data.units === 'days') { - data.amount = 30; - } else { - data.amount = 24; + if (!data.amount) { + if (data.units === 'days') { + data.amount = 30; + } else { + data.amount = 24; + } } if (data.graph === 'traffic') { diff --git a/src/views/admin/general/dashboard.tpl b/src/views/admin/general/dashboard.tpl index d904637fe4..884fe5f2a8 100644 --- a/src/views/admin/general/dashboard.tpl +++ b/src/views/admin/general/dashboard.tpl @@ -11,17 +11,23 @@


- - - diff --git a/src/views/admin/partials/pageviews-range-select.tpl b/src/views/admin/partials/pageviews-range-select.tpl new file mode 100644 index 0000000000..e63166d3a5 --- /dev/null +++ b/src/views/admin/partials/pageviews-range-select.tpl @@ -0,0 +1,20 @@ +
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+

[[admin/general/dashboard:page-views-custom-help]]

+
+ \ No newline at end of file From 572d7a1e8cb711a5e9684347d8fe17b75de3ef02 Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Thu, 11 May 2017 11:25:13 -0500 Subject: [PATCH 055/263] Fix uploads of files with no extension failing when there is no restriction on file extensions. --- src/controllers/uploads.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controllers/uploads.js b/src/controllers/uploads.js index 3ef09c89cc..39db4bc00b 100644 --- a/src/controllers/uploads.js +++ b/src/controllers/uploads.js @@ -211,7 +211,7 @@ uploadsController.uploadFile = function (uid, uploadedFile, callback) { var allowed = file.allowedExtensions(); var extension = path.extname(uploadedFile.name).toLowerCase(); - if (!extension || extension === '.' || (allowed.length > 0 && allowed.indexOf(extension) === -1)) { + if (allowed.length > 0 && (!extension || extension === '.' || allowed.indexOf(extension) === -1)) { return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]')); } From d06b19a69a8896266a3346810f3fd0f57ec3cc00 Mon Sep 17 00:00:00 2001 From: psychobunny Date: Thu, 11 May 2017 16:06:59 -0400 Subject: [PATCH 056/263] up vanilla --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b4a6a96933..549303aebe 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "4.0.0", "nodebb-theme-persona": "5.0.0", - "nodebb-theme-vanilla": "6.0.0", + "nodebb-theme-vanilla": "6.0.1", "nodebb-widget-essentials": "3.0.0", "nodemailer": "2.6.4", "nodemailer-sendmail-transport": "1.0.0", From 51a85031f737ca2ac27d6fce8e3336a5a4bb35d5 Mon Sep 17 00:00:00 2001 From: Baris Usakli Date: Thu, 11 May 2017 16:31:10 -0400 Subject: [PATCH 057/263] utils tests --- package.json | 1 + public/src/utils.js | 4 +- test/utils.js | 199 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 549303aebe..39e8f74fc7 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "grunt": "~1.0.0", "grunt-contrib-watch": "^1.0.0", "istanbul": "^0.4.2", + "jsdom": "9.12.0", "mocha": "~3.2.0", "mocha-lcov-reporter": "^1.2.0", "xmlhttprequest": "1.8.0", diff --git a/public/src/utils.js b/public/src/utils.js index 3a73c5fdd6..fe655c2e3b 100644 --- a/public/src/utils.js +++ b/public/src/utils.js @@ -346,9 +346,7 @@ }, urlToLocation: function (url) { - var a = document.createElement('a'); - a.href = url; - return a; + return $('')[0]; }, // return boolean if string 'true' or string 'false', or if a parsable string which is a number diff --git a/test/utils.js b/test/utils.js index 9c8c2bfb12..2366b57d06 100644 --- a/test/utils.js +++ b/test/utils.js @@ -2,10 +2,26 @@ var assert = require('assert'); +var jsdom = require('jsdom'); var utils = require('./../public/src/utils.js'); describe('Utility Methods', function () { + // https://gist.github.com/robballou/9ee108758dc5e0e2d028 + // create some jsdom magic to allow jQuery to work + var doc = jsdom.jsdom(''); + var window = doc.defaultView; + global.jQuery = require('jquery')(window); + global.$ = global.jQuery; + var $ = global.$; + global.window = doc.defaultView; + + it('should preserve case if requested', function (done) { + var slug = utils.slugify('UPPER CASE', true); + assert.equal(slug, 'UPPER-CASE'); + done(); + }); + describe('username validation', function () { it('accepts latin-1 characters', function () { var username = "John\"'-. Doeäâèéë1234"; @@ -120,6 +136,13 @@ describe('Utility Methods', function () { done(); }); + it('should make numbers human readable on elements', function (done) { + var el = $('
'); + utils.makeNumbersHumanReadable(el); + assert.equal(el.html(), '100.0k'); + done(); + }); + it('should add commas to numbers', function (done) { assert.equal(utils.addCommas('100'), '100'); done(); @@ -135,6 +158,166 @@ describe('Utility Methods', function () { done(); }); + it('should add commas to elements', function (done) { + var el = $('
1000000
'); + utils.addCommasToNumbers(el); + assert.equal(el.html(), '1,000,000'); + done(); + }); + + it('should return passed in value if invalid', function (done) { + var bigInt = -111111111111111111; + var result = utils.toISOString(bigInt); + assert.equal(bigInt, result); + done(); + }); + + it('should return false if browser is not android', function (done) { + global.navigator = { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.96 Safari/537.36', + }; + assert.equal(utils.isAndroidBrowser(), false); + done(); + }); + + it('should return true if browser is android', function (done) { + global.navigator = { + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Android /58.0.3029.96 Safari/537.36', + }; + assert.equal(utils.isAndroidBrowser(), true); + done(); + }); + + it('should return false if not touch device', function (done) { + global.document = global.document || {}; + global.document.documentElement = {}; + assert(!utils.isTouchDevice()); + done(); + }); + + it('should return true if touch device', function (done) { + global.document.documentElement = { + ontouchstart: 1, + }; + assert(utils.isTouchDevice()); + done(); + }); + + it('should check if element is in viewport', function (done) { + var el = $('
some text
'); + assert(utils.isElementInViewport(el)); + done(); + }); + + it('should get empty object for url params', function (done) { + var params = utils.params(); + assert.equal(Object.keys(params), 0); + done(); + }); + + it('should get url params', function (done) { + var params = utils.params({ url: 'http://nodebb.org?foo=1&bar=test&herp=2' }); + assert.equal(params.foo, 1); + assert.equal(params.bar, 'test'); + assert.equal(params.herp, 2); + done(); + }); + + it('should get a single param', function (done) { + assert.equal(utils.param('somekey'), undefined); + done(); + }); + + + describe('toType', function () { + it('should return param as is if not string', function (done) { + assert.equal(123, utils.toType(123)); + done(); + }); + + it('should convert return string numbers as numbers', function (done) { + assert.equal(123, utils.toType('123')); + done(); + }); + + it('should convert string "false" to boolean false', function (done) { + assert.strictEqual(false, utils.toType('false')); + done(); + }); + + it('should convert string "true" to boolean true', function (done) { + assert.strictEqual(true, utils.toType('true')); + done(); + }); + + it('should parse json', function (done) { + var data = utils.toType('{"a":"1"}'); + assert.equal(data.a, '1'); + done(); + }); + + it('should return string as is if its not json,true,false or number', function (done) { + var regularStr = 'this is a regular string'; + assert.equal(regularStr, utils.toType(regularStr)); + done(); + }); + }); + + describe('utils.props', function () { + var data = {}; + + it('should set nested data', function (done) { + assert.equal(10, utils.props(data, 'a.b.c.d', 10)); + done(); + }); + + it('should return nested object', function (done) { + var obj = utils.props(data, 'a.b.c'); + assert.equal(obj.d, 10); + done(); + }); + + it('should returned undefined without throwing', function (done) { + assert.equal(utils.props(data, 'a.b.c.foo.bar'), undefined); + done(); + }); + + it('should return undefined if second param is null', function (done) { + assert.equal(utils.props(undefined, null), undefined); + done(); + }); + }); + + describe('isInternalURI', function () { + var target = { host: '', protocol: 'https' }; + var reference = { host: '', protocol: 'https' }; + + it('should return true if they match', function (done) { + assert(utils.isInternalURI(target, reference, '')); + done(); + }); + + it('should return true if they match', function (done) { + target.host = 'nodebb.org'; + reference.host = 'nodebb.org'; + assert(utils.isInternalURI(target, reference, '')); + done(); + }); + + it('should handle relative path', function (done) { + target.pathname = '/forum'; + assert(utils.isInternalURI(target, reference, '/forum')); + done(); + }); + + it('should return false if they do not match', function (done) { + target.pathname = ''; + reference.host = 'designcreateplay.com'; + assert(!utils.isInternalURI(target, reference)); + done(); + }); + }); + it('escape html', function (done) { var escaped = utils.escapeHTML('&<>'); assert.equal(escaped, '&<>'); @@ -178,4 +361,20 @@ describe('Utility Methods', function () { assert.strictEqual(utils.rtrim('\t thing \t'), '\t thing'); done(); }); + + it('should walk directory', function (done) { + utils.walk(__dirname, function (err, data) { + assert.ifError(err); + assert(Array.isArray(data)); + done(); + }); + }); + + it('should profile function', function (done) { + var st = process.hrtime(); + setTimeout(function () { + process.profile('it took', st); + done(); + }, 500); + }); }); From 816809ce3c63ac021dcea502a228d59295857695 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Thu, 11 May 2017 16:53:30 -0400 Subject: [PATCH 058/263] fixes #5606 --- .../en-GB/admin/general/dashboard.json | 4 +-- public/src/admin/general/dashboard.js | 21 +++++++--------- src/analytics.js | 25 ++++++++----------- src/socket.io/admin.js | 4 +-- src/views/admin/general/dashboard.tpl | 8 +++--- 5 files changed, 28 insertions(+), 34 deletions(-) diff --git a/public/language/en-GB/admin/general/dashboard.json b/public/language/en-GB/admin/general/dashboard.json index 437e8e2461..61f4421fb5 100644 --- a/public/language/en-GB/admin/general/dashboard.json +++ b/public/language/en-GB/admin/general/dashboard.json @@ -5,8 +5,8 @@ "users": "Users", "posts": "Posts", "topics": "Topics", - "page-views-last-month": "Last Month", - "page-views-this-month": "This Month", + "page-views-seven": "Last 7 Days", + "page-views-thirty": "Last 30 Days", "page-views-last-day": "Last 24 hours", "page-views-custom": "Custom Date Range", "page-views-custom-start": "Range Start", diff --git a/public/src/admin/general/dashboard.js b/public/src/admin/general/dashboard.js index 6b88483ac0..054ed54e51 100644 --- a/public/src/admin/general/dashboard.js +++ b/public/src/admin/general/dashboard.js @@ -295,14 +295,11 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s adjustPieCharts(); $('[data-action="updateGraph"]:not([data-units="custom"])').on('click', function () { - var until; - switch ($(this).attr('data-until')) { - case 'last-month': - var lastMonth = new Date(); - lastMonth.setDate(lastMonth.getDate() - 30); - until = lastMonth.getTime(); - } - updateTrafficGraph($(this).attr('data-units'), until); + var until = new Date(); + var amount = $(this).attr('data-amount'); + until.setHours(0, 0, 0, 0); + until = until.getTime(); + updateTrafficGraph($(this).attr('data-units'), until, amount); $('[data-action="updateGraph"]').removeClass('active'); $(this).addClass('active'); @@ -406,11 +403,11 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator'], function (s } else { graphs.traffic.data.xLabels = utils.getHoursArray(); - $('#pageViewsThisMonth').html(data.monthlyPageViews.thisMonth); - $('#pageViewsLastMonth').html(data.monthlyPageViews.lastMonth); + $('#pageViewsThirty').html(data.summary.thirty); + $('#pageViewsSeven').html(data.summary.seven); $('#pageViewsPastDay').html(data.pastDay); - utils.addCommasToNumbers($('#pageViewsThisMonth')); - utils.addCommasToNumbers($('#pageViewsLastMonth')); + utils.addCommasToNumbers($('#pageViewsThirty')); + utils.addCommasToNumbers($('#pageViewsSeven')); utils.addCommasToNumbers($('#pageViewsPastDay')); } diff --git a/src/analytics.js b/src/analytics.js index 8e9871580b..8a7392f3ca 100644 --- a/src/analytics.js +++ b/src/analytics.js @@ -168,21 +168,18 @@ Analytics.getUnwrittenPageviews = function () { return pageViews; }; -Analytics.getMonthlyPageViews = function (callback) { - var thisMonth = new Date(); - var lastMonth = new Date(); - thisMonth.setMonth(thisMonth.getMonth(), 1); - thisMonth.setHours(0, 0, 0, 0); - lastMonth.setMonth(thisMonth.getMonth() - 1, 1); - lastMonth.setHours(0, 0, 0, 0); - - var values = [thisMonth.getTime(), lastMonth.getTime()]; +Analytics.getSummary = function (callback) { + var today = new Date(); + today.setHours(0, 0, 0, 0); - db.sortedSetScores('analytics:pageviews:month', values, function (err, scores) { - if (err) { - return callback(err); - } - callback(null, { thisMonth: scores[0] || 0, lastMonth: scores[1] || 0 }); + async.parallel({ + seven: async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews', today, 7), + thirty: async.apply(Analytics.getDailyStatsForSet, 'analytics:pageviews', today, 30), + }, function (err, scores) { + callback(null, { + seven: scores.seven.reduce(function (sum, cur) { return sum += cur; }, 0), + thirty: scores.thirty.reduce(function (sum, cur) { return sum += cur; }, 0), + }); }); }; diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 2cdaa0161b..f1bc4f2871 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -235,8 +235,8 @@ SocketAdmin.analytics.get = function (socket, data, callback) { analytics.getHourlyStatsForSet('analytics:pageviews', data.until || Date.now(), data.amount, next); } }, - monthlyPageViews: function (next) { - analytics.getMonthlyPageViews(next); + summary: function (next) { + analytics.getSummary(next); }, }, function (err, data) { data.pastDay = data.pageviews.reduce(function (a, b) { return parseInt(a, 10) + parseInt(b, 10); }); diff --git a/src/views/admin/general/dashboard.tpl b/src/views/admin/general/dashboard.tpl index 884fe5f2a8..46f7e236dd 100644 --- a/src/views/admin/general/dashboard.tpl +++ b/src/views/admin/general/dashboard.tpl @@ -13,12 +13,12 @@
+
+ + +

+ [[admin/settings/user:invite-expiration-help]] +

+
From 6daeadb7e9d2a9f27182cc5c333216fdf20f5290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 12 May 2017 16:32:00 -0400 Subject: [PATCH 069/263] remove async.parallel --- src/user/invite.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/user/invite.js b/src/user/invite.js index 0a32c45031..3e6fdef359 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -60,17 +60,10 @@ module.exports = function (User) { if (exists) { return next(new Error('[[error:email-taken]]')); } - - async.parallel([ - function (next) { - db.setAdd('invitation:uid:' + uid, email, next); - }, - function (next) { - db.setAdd('invitation:uids', uid, next); - }, - ], function (err) { - next(err); - }); + db.setAdd('invitation:uid:' + uid, email, next); + }, + function (next) { + db.setAdd('invitation:uids', uid, next); }, function (next) { db.set('invitation:email:' + email, token, next); From 01e2263c012dc0e61d78424655c72c30a637345a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 12 May 2017 17:53:23 -0400 Subject: [PATCH 070/263] more tests --- src/controllers/helpers.js | 17 ++-- src/groups/membership.js | 8 +- src/middleware/admin.js | 22 ++--- src/middleware/index.js | 67 +------------- src/middleware/user.js | 181 +++++++++++++++++++++++-------------- test/controllers.js | 93 +++++++++++++++++-- test/helpers/index.js | 3 +- 7 files changed, 225 insertions(+), 166 deletions(-) diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 5571bfbf62..9ee0ebc0d9 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -10,8 +10,9 @@ var privileges = require('../privileges'); var categories = require('../categories'); var plugins = require('../plugins'); var meta = require('../meta'); +var middleware = require('../middleware'); -var helpers = {}; +var helpers = module.exports; helpers.notAllowed = function (req, res, error) { plugins.fireHook('filter:helpers.notAllowed', { @@ -31,11 +32,13 @@ helpers.notAllowed = function (req, res, error) { title: '[[global:403.title]]', }); } else { - res.status(403).render('403', { - path: req.path, - loggedIn: !!req.uid, - error: error, - title: '[[global:403.title]]', + middleware.buildHeader(req, res, function () { + res.status(403).render('403', { + path: req.path, + loggedIn: !!req.uid, + error: error, + title: '[[global:403.title]]', + }); }); } } else if (res.locals.isAPI) { @@ -175,5 +178,3 @@ function recursive(category, categoriesData, level) { recursive(child, categoriesData, '    ' + level); }); } - -module.exports = helpers; diff --git a/src/groups/membership.js b/src/groups/membership.js index 6337db1d8a..2660d716e1 100644 --- a/src/groups/membership.js +++ b/src/groups/membership.js @@ -323,12 +323,12 @@ module.exports = function (Groups) { Groups.isMember = function (uid, groupName, callback) { if (!uid || parseInt(uid, 10) <= 0) { - return callback(null, false); + return setImmediate(callback, null, false); } var cacheKey = uid + ':' + groupName; if (cache.has(cacheKey)) { - return process.nextTick(callback, null, cache.get(cacheKey)); + return setImmediate(callback, null, cache.get(cacheKey)); } async.waterfall([ @@ -344,7 +344,7 @@ module.exports = function (Groups) { Groups.isMembers = function (uids, groupName, callback) { function getFromCache(next) { - process.nextTick(next, null, uids.map(function (uid) { + setImmediate(next, null, uids.map(function (uid) { return cache.get(uid + ':' + groupName); })); } @@ -377,7 +377,7 @@ module.exports = function (Groups) { Groups.isMemberOfGroups = function (uid, groups, callback) { function getFromCache(next) { - process.nextTick(next, null, groups.map(function (groupName) { + setImmediate(next, null, groups.map(function (groupName) { return cache.get(uid + ':' + groupName); })); } diff --git a/src/middleware/admin.js b/src/middleware/admin.js index 3e36b74db9..f40e273317 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -16,17 +16,17 @@ module.exports = function (middleware) { middleware.admin.isAdmin = function (req, res, next) { winston.warn('[middleware.admin.isAdmin] deprecation warning, no need to use this from plugins!'); - if (!req.user) { - return controllers.helpers.notAllowed(req, res); - } - - user.isAdministrator(req.user.uid, function (err, isAdmin) { - if (err || isAdmin) { - return next(err); - } - - controllers.helpers.notAllowed(req, res); - }); + async.waterfall([ + function (next) { + user.isAdministrator(req.uid, next); + }, + function (isAdmin, next) { + if (!isAdmin) { + return controllers.helpers.notAllowed(req, res); + } + next(); + }, + ], next); }; middleware.admin.buildHeader = function (req, res, next) { diff --git a/src/middleware/index.js b/src/middleware/index.js index d4cdb0bfd2..06b336d97f 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -21,7 +21,7 @@ var controllers = { helpers: require('../controllers/helpers'), }; -var middleware = {}; +var middleware = module.exports; middleware.applyCSRF = csrf(); @@ -34,68 +34,6 @@ require('./maintenance')(middleware); require('./user')(middleware); require('./headers')(middleware); -middleware.authenticate = function (req, res, next) { - if (req.user) { - return next(); - } else if (plugins.hasListeners('action:middleware.authenticate')) { - return plugins.fireHook('action:middleware.authenticate', { - req: req, - res: res, - next: next, - }); - } - - controllers.helpers.notAllowed(req, res); -}; - -middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) { - /* - The "self" part of this middleware hinges on you having used - middleware.exposeUid prior to invoking this middleware. - */ - async.waterfall([ - function (next) { - if (!req.uid) { - return setImmediate(next, null, false); - } - - if (req.uid === parseInt(res.locals.uid, 10)) { - return setImmediate(next, null, true); - } - user.isAdminOrGlobalMod(req.uid, next); - }, - function (isAdminOrGlobalMod, next) { - if (!isAdminOrGlobalMod) { - return controllers.helpers.notAllowed(req, res); - } - next(); - }, - ], next); -}; - -middleware.ensureSelfOrPrivileged = function (req, res, next) { - /* - The "self" part of this middleware hinges on you having used - middleware.exposeUid prior to invoking this middleware. - */ - if (req.user) { - if (parseInt(req.user.uid, 10) === parseInt(res.locals.uid, 10)) { - return next(); - } - - user.isPrivileged(req.uid, function (err, ok) { - if (err) { - return next(err); - } else if (ok) { - return next(); - } - controllers.helpers.notAllowed(req, res); - }); - } else { - controllers.helpers.notAllowed(req, res); - } -}; - middleware.pageView = function (req, res, next) { analytics.pageView({ ip: req.ip, @@ -226,6 +164,3 @@ middleware.processTimeagoLocales = function (req, res) { }); } }; - - -module.exports = middleware; diff --git a/src/middleware/user.js b/src/middleware/user.js index 85b5d6e808..6b0abaf5b0 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -6,14 +6,65 @@ var nconf = require('nconf'); var meta = require('../meta'); var user = require('../user'); var privileges = require('../privileges'); +var plugins = require('../plugins'); var controllers = { helpers: require('../controllers/helpers'), }; module.exports = function (middleware) { + middleware.authenticate = function (req, res, next) { + if (req.uid) { + return next(); + } + + if (plugins.hasListeners('action:middleware.authenticate')) { + return plugins.fireHook('action:middleware.authenticate', { + req: req, + res: res, + next: next, + }); + } + + controllers.helpers.notAllowed(req, res); + }; + + middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) { + ensureSelfOrMethod(user.isAdminOrGlobalMod, req, res, next); + }; + + middleware.ensureSelfOrPrivileged = function (req, res, next) { + ensureSelfOrMethod(user.isPrivileged, req, res, next); + }; + + function ensureSelfOrMethod(method, req, res, next) { + /* + The "self" part of this middleware hinges on you having used + middleware.exposeUid prior to invoking this middleware. + */ + async.waterfall([ + function (next) { + if (!req.uid) { + return setImmediate(next, null, false); + } + + if (req.uid === parseInt(res.locals.uid, 10)) { + return setImmediate(next, null, true); + } + + method(req.uid, next); + }, + function (allowed, next) { + if (!allowed) { + return controllers.helpers.notAllowed(req, res); + } + next(); + }, + ], next); + } + middleware.checkGlobalPrivacySettings = function (req, res, next) { - if (!req.user && !!parseInt(meta.config.privateUserInfo, 10)) { + if (!req.uid && !!parseInt(meta.config.privateUserInfo, 10)) { return middleware.authenticate(req, res, next); } @@ -44,28 +95,28 @@ module.exports = function (middleware) { next(null, false); } }, - ], function (err, allowed) { - if (err || allowed) { - return next(err); - } - controllers.helpers.notAllowed(req, res); - }); + function (allowed) { + if (allowed) { + return next(); + } + controllers.helpers.notAllowed(req, res); + }, + ], next); }; middleware.redirectToAccountIfLoggedIn = function (req, res, next) { - if (req.session.forceLogin) { + if (req.session.forceLogin || !req.uid) { return next(); } - if (!req.user) { - return next(); - } - user.getUserField(req.user.uid, 'userslug', function (err, userslug) { - if (err) { - return next(err); - } - controllers.helpers.redirect(res, '/user/' + userslug); - }); + async.waterfall([ + function (next) { + user.getUserField(req.uid, 'userslug', next); + }, + function (userslug) { + controllers.helpers.redirect(res, '/user/' + userslug); + }, + ], next); }; middleware.redirectUidToUserslug = function (req, res, next) { @@ -73,71 +124,61 @@ module.exports = function (middleware) { if (!uid) { return next(); } - user.getUserField(uid, 'userslug', function (err, userslug) { - if (err || !userslug) { - return next(err); - } - - var path = req.path.replace(/^\/api/, '') + async.waterfall([ + function (next) { + user.getUserField(uid, 'userslug', next); + }, + function (userslug) { + if (!userslug) { + return next(); + } + var path = req.path.replace(/^\/api/, '') .replace('uid', 'user') .replace(uid, function () { return userslug; }); - controllers.helpers.redirect(res, path); - }); + controllers.helpers.redirect(res, path); + }, + ], next); }; middleware.isAdmin = function (req, res, next) { - if (!req.uid) { - return controllers.helpers.notAllowed(req, res); - } - - user.isAdministrator(req.uid, function (err, isAdmin) { - if (err) { - return next(err); - } - - if (isAdmin) { - user.hasPassword(req.uid, function (err, hasPassword) { - if (err) { - return next(err); - } - - if (!hasPassword) { - return next(); - } - - var loginTime = req.session.meta ? req.session.meta.datetime : 0; - if (loginTime && parseInt(loginTime, 10) > Date.now() - 3600000) { - var timeLeft = parseInt(loginTime, 10) - (Date.now() - 3600000); - if (timeLeft < 300000) { - req.session.meta.datetime += 300000; - } - - return next(); - } + async.waterfall([ + function (next) { + user.isAdministrator(req.uid, next); + }, + function (isAdmin, next) { + if (!isAdmin) { + return controllers.helpers.notAllowed(req, res); + } + user.hasPassword(req.uid, next); + }, + function (hasPassword, next) { + if (!hasPassword) { + return next(); + } - req.session.returnTo = req.path.replace(/^\/api/, ''); - req.session.forceLogin = 1; - if (res.locals.isAPI) { - res.status(401).json({}); - } else { - res.redirect(nconf.get('relative_path') + '/login'); + var loginTime = req.session.meta ? req.session.meta.datetime : 0; + if (loginTime && parseInt(loginTime, 10) > Date.now() - 3600000) { + var timeLeft = parseInt(loginTime, 10) - (Date.now() - 3600000); + if (timeLeft < 300000) { + req.session.meta.datetime += 300000; } - }); - return; - } - if (res.locals.isAPI) { - return controllers.helpers.notAllowed(req, res); - } + return next(); + } - middleware.buildHeader(req, res, function () { - controllers.helpers.notAllowed(req, res); - }); - }); + req.session.returnTo = req.path.replace(/^\/api/, ''); + req.session.forceLogin = 1; + if (res.locals.isAPI) { + res.status(401).json({}); + } else { + res.redirect(nconf.get('relative_path') + '/login'); + } + }, + ], next); }; middleware.requireUser = function (req, res, next) { - if (req.user) { + if (req.uid) { return next(); } diff --git a/test/controllers.js b/test/controllers.js index 956e5a8daf..c353effbcb 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -9,6 +9,7 @@ var db = require('./mocks/databasemock'); var categories = require('../src/categories'); var topics = require('../src/topics'); var user = require('../src/user'); +var groups = require('../src/groups'); var meta = require('../src/meta'); var translator = require('../src/translator'); @@ -19,6 +20,7 @@ describe('Controllers', function () { var fooUid; before(function (done) { + groups.resetCache(); async.series({ category: function (next) { categories.create({ @@ -355,7 +357,7 @@ describe('Controllers', function () { }); it('should load stylesheet.css', function (done) { - request(nconf.get('url') + '/stylesheet.css', function (err, res, body) { + request(nconf.get('url') + '/assets/stylesheet.css', function (err, res, body) { assert.ifError(err); assert.equal(res.statusCode, 200); assert(body); @@ -364,7 +366,7 @@ describe('Controllers', function () { }); it('should load admin.css', function (done) { - request(nconf.get('url') + '/admin.css', function (err, res, body) { + request(nconf.get('url') + '/assets/admin.css', function (err, res, body) { assert.ifError(err); assert.equal(res.statusCode, 200); assert(body); @@ -374,7 +376,7 @@ describe('Controllers', function () { it('should load nodebb.min.js', function (done) { - request(nconf.get('url') + '/nodebb.min.js', function (err, res, body) { + request(nconf.get('url') + '/assets/nodebb.min.js', function (err, res, body) { assert.ifError(err); assert.equal(res.statusCode, 200); assert(body); @@ -383,7 +385,7 @@ describe('Controllers', function () { }); it('should load acp.min.js', function (done) { - request(nconf.get('url') + '/acp.min.js', function (err, res, body) { + request(nconf.get('url') + '/assets/acp.min.js', function (err, res, body) { assert.ifError(err); assert.equal(res.statusCode, 200); assert(body); @@ -491,7 +493,6 @@ describe('Controllers', function () { }); it('should load group details page', function (done) { - var groups = require('../src/groups'); groups.create({ name: 'group-details', description: 'Foobar!', @@ -800,6 +801,57 @@ describe('Controllers', function () { }); }); + it('should redirect to account page with logged in user', function (done) { + request(nconf.get('url') + '/api/login', { jar: jar, json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 308); + assert.equal(body, '/user/foo'); + done(); + }); + }); + + it('should 404 if uid is not a number', function (done) { + request(nconf.get('url') + '/api/uid/test', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + done(); + }); + }); + + it('should redirect to userslug', function (done) { + request(nconf.get('url') + '/api/uid/' + fooUid, { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 308); + assert.equal(body, '/user/foo'); + done(); + }); + }); + + it('should 404 if user does not exist', function (done) { + request(nconf.get('url') + '/api/uid/123123', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + done(); + }); + }); + + it('should 401 if user is not logged in', function (done) { + request(nconf.get('url') + '/api/admin', { json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 401); + done(); + }); + }); + + it('should 403 if user is not admin', function (done) { + request(nconf.get('url') + '/api/admin', { jar: jar, json: true }, function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 403); + done(); + }); + }); + + it('should load /user/foo/posts', function (done) { request(nconf.get('url') + '/api/user/foo/posts', function (err, res, body) { assert.ifError(err); @@ -947,6 +999,35 @@ describe('Controllers', function () { done(); }); }); + + it('should return 401 if privateUserInfo is turned on', function (done) { + meta.config.privateUserInfo = 1; + request(nconf.get('url') + '/api/user/foo', { json: true }, function (err, res, body) { + meta.config.privateUserInfo = 0; + assert.ifError(err); + assert.equal(res.statusCode, 401); + assert.equal(body, 'not-authorized'); + done(); + }); + }); + + it('should return false if user can not edit user', function (done) { + user.create({ username: 'regularJoe', password: 'barbar' }, function (err) { + assert.ifError(err); + helpers.loginUser('regularJoe', 'barbar', function (err, jar) { + assert.ifError(err); + request(nconf.get('url') + '/api/user/foo/info', { jar: jar, json: true }, function (err, res) { + assert.ifError(err); + assert.equal(res.statusCode, 403); + request(nconf.get('url') + '/api/user/foo/edit', { jar: jar, json: true }, function (err, res) { + assert.ifError(err); + assert.equal(res.statusCode, 403); + done(); + }); + }); + }); + }); + }); }); describe('account follow page', function () { @@ -1120,7 +1201,7 @@ describe('Controllers', function () { }, }); - request(nconf.get('url') + '/users', { }, function (err, res, body) { + request(nconf.get('url') + '/users', { }, function (err, res) { plugins.loadedHooks['filter:router.page'] = []; assert.ifError(err); assert.equal(res.statusCode, 403); diff --git a/test/helpers/index.js b/test/helpers/index.js index 2b999246f7..e850ba23e2 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -3,6 +3,7 @@ var request = require('request'); var nconf = require('nconf'); var fs = require('fs'); +var winston = require('winston'); var myXhr = require('../mocks/newXhr'); var utils = require('../../public/src/utils'); @@ -105,7 +106,7 @@ helpers.uploadFile = function (uploadEndPoint, filePath, body, jar, csrf_token, return callback(err); } if (res.statusCode !== 200) { - console.log(body); + winston.error(body); } callback(null, res, body); }); From 6049c46f15e0f9b8cf7bae25477e8f2c1766c111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 12 May 2017 19:18:11 -0400 Subject: [PATCH 071/263] meta dependecy tests --- src/meta/dependencies.js | 82 +++++++++++++++++++++++----------------- test/meta.js | 50 ++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 34 deletions(-) diff --git a/src/meta/dependencies.js b/src/meta/dependencies.js index 034061f636..3a16f0e9e5 100644 --- a/src/meta/dependencies.js +++ b/src/meta/dependencies.js @@ -11,46 +11,15 @@ var pkg = require('../../package.json'); module.exports = function (Meta) { Meta.dependencies = {}; + var depsMissing = false; + var depsOutdated = false; Meta.dependencies.check = function (callback) { var modules = Object.keys(pkg.dependencies); - var depsOutdated = false; - var depsMissing = false; winston.verbose('Checking dependencies for outdated modules'); - async.each(modules, function (module, next) { - fs.readFile(path.join(__dirname, '../../node_modules/', module, 'package.json'), { - encoding: 'utf-8', - }, function (err, pkgData) { - if (err) { - // If a bundled plugin/theme is not present, skip the dep check (#3384) - if (err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) { - winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.'); - return next(); - } - return next(err); - } - - try { - pkgData = JSON.parse(pkgData); - } catch (e) { - process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n'); - depsMissing = true; - return next(); - } - - var ok = !semver.validRange(pkg.dependencies[module]) || semver.satisfies(pkgData.version, pkg.dependencies[module]); - - if (ok || (pkgData._resolved && pkgData._resolved.indexOf('//github.com') !== -1)) { - next(); - } else { - process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n'); - depsOutdated = true; - next(); - } - }); - }, function (err) { + async.each(modules, Meta.dependencies.checkModule, function (err) { if (err) { return callback(err); } @@ -64,4 +33,49 @@ module.exports = function (Meta) { } }); }; + + Meta.dependencies.checkModule = function (moduleName, callback) { + fs.readFile(path.join(__dirname, '../../node_modules/', moduleName, 'package.json'), { + encoding: 'utf-8', + }, function (err, pkgData) { + if (err) { + // If a bundled plugin/theme is not present, skip the dep check (#3384) + if (err.code === 'ENOENT' && (moduleName === 'nodebb-rewards-essentials' || moduleName.startsWith('nodebb-plugin') || moduleName.startsWith('nodebb-theme'))) { + winston.warn('[meta/dependencies] Bundled plugin ' + moduleName + ' not found, skipping dependency check.'); + return callback(null, true); + } + return callback(err); + } + + pkgData = Meta.dependencies.parseModuleData(moduleName, pkgData); + + var satisfies = Meta.dependencies.doesSatisfy(pkgData, pkg.dependencies[moduleName]); + callback(null, satisfies); + }); + }; + + Meta.dependencies.parseModuleData = function (moduleName, pkgData) { + try { + pkgData = JSON.parse(pkgData); + } catch (e) { + winston.warn('[' + 'missing'.red + '] ' + moduleName.bold + ' is a required dependency but could not be found\n'); + depsMissing = true; + return null; + } + return pkgData; + }; + + Meta.dependencies.doesSatisfy = function (moduleData, packageJSONVersion) { + if (!moduleData) { + return false; + } + var versionOk = !semver.validRange(packageJSONVersion) || semver.satisfies(moduleData.version, packageJSONVersion); + var githubRepo = moduleData._resolved && moduleData._resolved.indexOf('//github.com') !== -1; + var satisfies = versionOk || githubRepo; + if (!satisfies) { + winston.warn('[' + 'outdated'.yellow + '] ' + moduleData.name.bold + ' installed v' + moduleData.version + ', package.json requires ' + packageJSONVersion + '\n'); + depsOutdated = true; + } + return satisfies; + }; }; diff --git a/test/meta.js b/test/meta.js index 9c2f6d89b3..8b32e90070 100644 --- a/test/meta.js +++ b/test/meta.js @@ -211,6 +211,56 @@ describe('meta', function () { }); }); + describe('dependencies', function () { + it('should return ENOENT if module is not found', function (done) { + meta.dependencies.checkModule('some-module-that-does-not-exist', function (err) { + assert.equal(err.code, 'ENOENT'); + done(); + }); + }); + + it('should not error if module is a nodebb-plugin-*', function (done) { + meta.dependencies.checkModule('nodebb-plugin-somePlugin', function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should not error if module is nodebb-theme-*', function (done) { + meta.dependencies.checkModule('nodebb-theme-someTheme', function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should parse json package data', function (done) { + var pkgData = meta.dependencies.parseModuleData('nodebb-plugin-test', '{"a": 1}'); + assert.equal(pkgData.a, 1); + done(); + }); + + it('should return null data with invalid json', function (done) { + var pkgData = meta.dependencies.parseModuleData('nodebb-plugin-test', 'asdasd'); + assert.strictEqual(pkgData, null); + done(); + }); + + it('should return false if moduleData is falsy', function (done) { + assert(!meta.dependencies.doesSatisfy(null, '1.0.0')); + done(); + }); + + it('should return false if moduleData doesnt not satisfy package.json', function (done) { + assert(!meta.dependencies.doesSatisfy({ name: 'nodebb-plugin-test', version: '0.9.0' }, '1.0.0')); + done(); + }); + + it('should return true if _resolved is from github', function (done) { + assert(meta.dependencies.doesSatisfy({ name: 'nodebb-plugin-test', _resolved: 'https://github.com/some/repo', version: '0.9.0' }, '1.0.0')); + done(); + }); + }); + after(function (done) { db.emptydb(done); From 376c71adda42335c326f9d2a7d7cd3532c6f17e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 12 May 2017 19:51:33 -0400 Subject: [PATCH 072/263] group search tests --- src/groups/search.js | 5 +--- test/groups.js | 56 +++++++++++++++++++++++++++++++------------- 2 files changed, 41 insertions(+), 20 deletions(-) diff --git a/src/groups/search.js b/src/groups/search.js index 62df79696c..5552eb932c 100644 --- a/src/groups/search.js +++ b/src/groups/search.js @@ -88,10 +88,7 @@ module.exports = function (Groups) { if (!data.query) { Groups.getOwnersAndMembers(data.groupName, data.uid, 0, 19, function (err, users) { - if (err) { - return callback(err); - } - callback(null, { users: users }); + callback(err, { users: users }); }); return; } diff --git a/test/groups.js b/test/groups.js index 6b52787efa..abc858e9dc 100644 --- a/test/groups.js +++ b/test/groups.js @@ -57,6 +57,10 @@ describe('Groups', function () { // Also create a hidden group Groups.join('Hidden', 'Test', next); }, + function (next) { + // create another group that starts with test for search/sort + Groups.create({ name: 'Test2', description: 'Foobar!' }, next); + }, ], function (err, results) { assert.ifError(err); testUid = results[3]; @@ -69,7 +73,7 @@ describe('Groups', function () { it('should list the groups present', function (done) { Groups.getGroupsFromSet('groups:createtime', 0, 0, -1, function (err, groups) { assert.ifError(err); - assert.equal(groups.length, 6); + assert.equal(groups.length, 7); done(); }); }); @@ -98,10 +102,18 @@ describe('Groups', function () { describe('.search()', function () { var socketGroups = require('../src/socket.io/groups'); + it('should return empty array if query is falsy', function (done) { + Groups.search(null, {}, function (err, groups) { + assert.ifError(err); + assert.equal(0, groups.length); + done(); + }); + }); + it('should return the groups when search query is empty', function (done) { socketGroups.search({ uid: adminUid }, { query: '' }, function (err, groups) { assert.ifError(err); - assert.equal(3, groups.length); + assert.equal(4, groups.length); done(); }); }); @@ -109,7 +121,7 @@ describe('Groups', function () { it('should return the "Test" group when searched for', function (done) { socketGroups.search({ uid: adminUid }, { query: 'test' }, function (err, groups) { assert.ifError(err); - assert.equal(1, groups.length); + assert.equal(2, groups.length); assert.strictEqual('Test', groups[0].name); done(); }); @@ -118,7 +130,7 @@ describe('Groups', function () { it('should return the "Test" group when searched for and sort by member count', function (done) { Groups.search('test', { filterHidden: true, sort: 'count' }, function (err, groups) { assert.ifError(err); - assert.equal(1, groups.length); + assert.equal(2, groups.length); assert.strictEqual('Test', groups[0].name); done(); }); @@ -127,25 +139,37 @@ describe('Groups', function () { it('should return the "Test" group when searched for and sort by creation time', function (done) { Groups.search('test', { filterHidden: true, sort: 'date' }, function (err, groups) { assert.ifError(err); - assert.equal(1, groups.length); - assert.strictEqual('Test', groups[0].name); + assert.equal(2, groups.length); + assert.strictEqual('Test', groups[1].name); done(); }); }); it('should return all users if no query', function (done) { - User.create({ - username: 'newuser', - email: 'newuser@b.com', - }, function (err, uid) { + function createAndJoinGroup(username, email, callback) { + async.waterfall([ + function (next) { + User.create({ username: username, email: email }, next); + }, + function (uid, next) { + Groups.join('Test', uid, next); + }, + ], callback); + } + async.series([ + function (next) { + createAndJoinGroup('newuser', 'newuser@b.com', next); + }, + function (next) { + createAndJoinGroup('bob', 'bob@b.com', next); + }, + ], function (err) { assert.ifError(err); - Groups.join('Test', uid, function (err) { + + socketGroups.searchMembers({ uid: adminUid }, { groupName: 'Test', query: '' }, function (err, data) { assert.ifError(err); - socketGroups.searchMembers({ uid: adminUid }, { groupName: 'Test', query: '' }, function (err, data) { - assert.ifError(err); - assert.equal(data.users.length, 2); - done(); - }); + assert.equal(data.users.length, 3); + done(); }); }); }); From 5e3946a6be0649ba7188f7805eeca21fe7e4b390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 12 May 2017 20:50:01 -0400 Subject: [PATCH 073/263] teaser tests --- src/topics/teaser.js | 32 +++++++------ test/topics.js | 112 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 14 deletions(-) diff --git a/src/topics/teaser.js b/src/topics/teaser.js index f864e3e23d..d62dc2fe72 100644 --- a/src/topics/teaser.js +++ b/src/topics/teaser.js @@ -1,9 +1,9 @@ - 'use strict'; var async = require('async'); var S = require('string'); +var winston = require('winston'); var meta = require('../meta'); var user = require('../user'); @@ -14,6 +14,7 @@ var utils = require('../utils'); module.exports = function (Topics) { Topics.getTeasers = function (topics, uid, callback) { if (typeof uid === 'function') { + winston.warn('[Topics.getTeasers] this usage is deprecated please provide uid'); callback = uid; uid = 0; } @@ -108,6 +109,7 @@ module.exports = function (Topics) { Topics.getTeasersByTids = function (tids, uid, callback) { if (typeof uid === 'function') { + winston.warn('[Topics.getTeasersByTids] this usage is deprecated please provide uid'); callback = uid; uid = 0; } @@ -116,7 +118,7 @@ module.exports = function (Topics) { } async.waterfall([ function (next) { - Topics.getTopicsFields(tids, ['tid', 'postcount', 'teaserPid'], next); + Topics.getTopicsFields(tids, ['tid', 'postcount', 'teaserPid', 'mainPid'], next); }, function (topics, next) { Topics.getTeasers(topics, uid, next); @@ -126,6 +128,7 @@ module.exports = function (Topics) { Topics.getTeaser = function (tid, uid, callback) { if (typeof uid === 'function') { + winston.warn('[Topics.getTeaser] this usage is deprecated please provide uid'); callback = uid; uid = 0; } @@ -135,17 +138,18 @@ module.exports = function (Topics) { }; Topics.updateTeaser = function (tid, callback) { - Topics.getLatestUndeletedReply(tid, function (err, pid) { - if (err) { - return callback(err); - } - - pid = pid || null; - if (pid) { - Topics.setTopicField(tid, 'teaserPid', pid, callback); - } else { - Topics.deleteTopicField(tid, 'teaserPid', callback); - } - }); + async.waterfall([ + function (next) { + Topics.getLatestUndeletedReply(tid, next); + }, + function (pid, next) { + pid = pid || null; + if (pid) { + Topics.setTopicField(tid, 'teaserPid', pid, next); + } else { + Topics.deleteTopicField(tid, 'teaserPid', next); + } + }, + ], callback); }; }; diff --git a/test/topics.js b/test/topics.js index 6782f1041d..c362ca0824 100644 --- a/test/topics.js +++ b/test/topics.js @@ -8,6 +8,7 @@ var nconf = require('nconf'); var db = require('./mocks/databasemock'); var topics = require('../src/topics'); var categories = require('../src/categories'); +var meta = require('../src/meta'); var User = require('../src/user'); var groups = require('../src/groups'); var helpers = require('./helpers'); @@ -1524,6 +1525,117 @@ describe('Topic\'s', function () { }); }); + describe('teasers', function () { + var topic1; + var topic2; + before(function (done) { + async.series([ + function (next) { + topics.post({ uid: adminUid, title: 'topic 1', content: 'content 1', cid: categoryObj.cid }, next); + }, + function (next) { + topics.post({ uid: adminUid, title: 'topic 2', content: 'content 2', cid: categoryObj.cid }, next); + }, + ], function (err, results) { + assert.ifError(err); + topic1 = results[0]; + topic2 = results[1]; + done(); + }); + }); + + after(function (done) { + meta.config.teaserPost = ''; + done(); + }); + + + it('should return empty array if first param is empty', function (done) { + topics.getTeasers([], function (err, teasers) { + assert.ifError(err); + assert.equal(0, teasers.length); + done(); + }); + }); + + it('should get teasers with 2 params', function (done) { + topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) { + assert.ifError(err); + assert.deepEqual([undefined, undefined], teasers); + done(); + }); + }); + + it('should get teasers with first posts', function (done) { + meta.config.teaserPost = 'first'; + topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) { + assert.ifError(err); + assert.equal(2, teasers.length); + assert(teasers[0]); + assert(teasers[1]); + assert(teasers[0].tid, topic1.topicData.tid); + assert(teasers[0].content, 'content 1'); + assert(teasers[0].user.username, 'admin'); + done(); + }); + }); + + it('should get teasers even if one topic is falsy', function (done) { + topics.getTeasers([null, topic2.topicData], function (err, teasers) { + assert.ifError(err); + assert.equal(2, teasers.length); + assert.equal(undefined, teasers[0]); + assert(teasers[1]); + assert(teasers[1].tid, topic2.topicData.tid); + assert(teasers[1].content, 'content 2'); + assert(teasers[1].user.username, 'admin'); + done(); + }); + }); + + it('should get teasers with first posts', function (done) { + meta.config.teaserPost = 'last-post'; + topics.reply({ uid: adminUid, content: 'reply 1 content', tid: topic1.topicData.tid }, function (err, result) { + assert.ifError(err); + topic1.topicData.teaserPid = result.pid; + topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) { + assert.ifError(err); + assert(teasers[0]); + assert(teasers[1]); + assert(teasers[0].tid, topic1.topicData.tid); + assert(teasers[0].content, 'reply 1 content'); + done(); + }); + }); + }); + + it('should get teasers by tids', function (done) { + topics.getTeasersByTids([topic2.topicData.tid, topic1.topicData.tid], function (err, teasers) { + assert.ifError(err); + assert(2, teasers.length); + assert.equal(teasers[1].content, 'reply 1 content'); + done(); + }); + }); + + it('should return empty array ', function (done) { + topics.getTeasersByTids([], function (err, teasers) { + assert.ifError(err); + assert.equal(0, teasers.length); + done(); + }); + }); + + it('should get teaser by tid', function (done) { + topics.getTeaser(topic2.topicData.tid, function (err, teaser) { + assert.ifError(err); + assert(teaser); + assert.equal(teaser.content, 'content 2'); + done(); + }); + }); + }); + after(function (done) { db.emptydb(done); }); From 64732af1308735ffe004fda0a67e579684d52d3d Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Sat, 13 May 2017 09:23:06 +0000 Subject: [PATCH 074/263] Latest translations and fallbacks --- public/language/uk/admin/general/dashboard.json | 12 ++++++------ public/language/uk/admin/manage/users.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/language/uk/admin/general/dashboard.json b/public/language/uk/admin/general/dashboard.json index 61528f5fb0..24778fe52e 100644 --- a/public/language/uk/admin/general/dashboard.json +++ b/public/language/uk/admin/general/dashboard.json @@ -5,12 +5,12 @@ "users": "Користувачі", "posts": "Пости", "topics": "Теми", - "page-views-seven": "Last 7 Days", - "page-views-thirty": "Last 30 Days", - "page-views-last-day": "Last 24 hours", - "page-views-custom": "Custom Date Range", - "page-views-custom-start": "Range Start", - "page-views-custom-end": "Range End", + "page-views-seven": "Останні 7 Днів", + "page-views-thirty": "Останні 30 Днів", + "page-views-last-day": "Останні 24 Години", + "page-views-custom": "Заданий Період", + "page-views-custom-start": "Початок Періоду", + "page-views-custom-end": "Кінець Періоду", "page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is YYYY-MM-DD", "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", diff --git a/public/language/uk/admin/manage/users.json b/public/language/uk/admin/manage/users.json index 9de068acca..e8559022b6 100644 --- a/public/language/uk/admin/manage/users.json +++ b/public/language/uk/admin/manage/users.json @@ -18,7 +18,7 @@ "new": "Новий користувач", "pills.latest": "Останні користувачі", - "pills.unvalidated": "Не підтвердженні", + "pills.unvalidated": "Не підтверджені", "pills.no-posts": "Без постів", "pills.top-posters": "Кращі автори", "pills.top-rep": "Найбільша репутація", From 22db07938c69e31d93633d83aeb761d8247f346f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 13 May 2017 13:45:48 -0400 Subject: [PATCH 075/263] up composer --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 424eeeb0f3..f97ac95349 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "morgan": "^1.3.2", "mousetrap": "^1.5.3", "nconf": "~0.8.2", - "nodebb-plugin-composer-default": "4.4.7", + "nodebb-plugin-composer-default": "4.4.8", "nodebb-plugin-dbsearch": "2.0.2", "nodebb-plugin-emoji-extended": "1.1.1", "nodebb-plugin-emoji-one": "1.1.5", From 6b05f6c5e93f31259cecc7c63987e677ba54c1df Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Sat, 13 May 2017 18:46:11 -0600 Subject: [PATCH 076/263] Add hook `filter:topics.getUnreadTids` Allows for filtering topics from unread count and unread page --- src/topics/unread.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/topics/unread.js b/src/topics/unread.js index ef2f35f22c..ac26a277f9 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -10,6 +10,7 @@ var categories = require('../categories'); var privileges = require('../privileges'); var meta = require('../meta'); var utils = require('../utils'); +var plugins = require('../plugins'); module.exports = function (Topics) { Topics.getTotalUnread = function (uid, filter, callback) { @@ -132,6 +133,17 @@ module.exports = function (Topics) { filterTopics(uid, tids, params.cid, params.filter, next); }, + function (tids, next) { + plugins.fireHook('filter:topics.getUnreadTids', { + uid: uid, + tids: tids, + cid: params.cid, + filter: params.filter, + }, next); + }, + function (results, next) { + next(null, results.tids); + }, ], callback); }; From 8920523e064e7216ccea7aa2590af712ab0b9e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 13 May 2017 21:45:50 -0400 Subject: [PATCH 077/263] more tests --- src/flags.js | 2 - src/notifications.js | 121 +++++++++++++++++++++--------------------- src/user/info.js | 91 ++++++++++++++----------------- src/user/profile.js | 46 ++++++++-------- test/notifications.js | 4 ++ test/user.js | 41 ++++++++++---- 6 files changed, 162 insertions(+), 143 deletions(-) diff --git a/src/flags.js b/src/flags.js index 458e2bdf56..03c810794b 100644 --- a/src/flags.js +++ b/src/flags.js @@ -685,5 +685,3 @@ Flags.notify = function (flagObj, uid, callback) { break; } }; - -module.exports = Flags; diff --git a/src/notifications.js b/src/notifications.js index fef4a4a4d2..931e0ad293 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -331,82 +331,83 @@ Notifications.markReadMultiple = function (nids, uid, callback) { db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next); }, - ], function (err, notificationData) { - if (err) { - return callback(err); - } - - // Filter out notifications that didn't exist - notificationData = notificationData.filter(function (notification) { - return notification && notification.nid; - }); + function (notificationData, next) { + // Filter out notifications that didn't exist + notificationData = notificationData.filter(function (notification) { + return notification && notification.nid; + }); - // Extract nid - nids = notificationData.map(function (notification) { - return notification.nid; - }); + // Extract nid + nids = notificationData.map(function (notification) { + return notification.nid; + }); - var datetimes = notificationData.map(function (notification) { - return (notification && notification.datetime) || Date.now(); - }); + var datetimes = notificationData.map(function (notification) { + return (notification && notification.datetime) || Date.now(); + }); - async.parallel([ - function (next) { - db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); - }, - function (next) { - db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next); - }, - ], function (err) { - callback(err); - }); + async.parallel([ + function (next) { + db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); + }, + function (next) { + db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next); + }, + ], next); + }, + ], function (err) { + callback(err); }); }; Notifications.markAllRead = function (uid, callback) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) { - if (err) { - return callback(err); - } - - if (!Array.isArray(nids) || !nids.length) { - return callback(); - } + async.waterfall([ + function (next) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next); + }, + function (nids, next) { + if (!Array.isArray(nids) || !nids.length) { + return next(); + } - Notifications.markReadMultiple(nids, uid, callback); - }); + Notifications.markReadMultiple(nids, uid, next); + }, + ], callback); }; -Notifications.prune = function () { +Notifications.prune = function (callback) { + callback = callback || function () {}; var week = 604800000; var cutoffTime = Date.now() - week; - db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) { - if (err) { - return winston.error(err.message); - } - - if (!Array.isArray(nids) || !nids.length) { - return; - } + async.waterfall([ + function (next) { + db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, next); + }, + function (nids, next) { + if (!Array.isArray(nids) || !nids.length) { + return callback(); + } - var keys = nids.map(function (nid) { - return 'notifications:' + nid; - }); + var keys = nids.map(function (nid) { + return 'notifications:' + nid; + }); - async.parallel([ - function (next) { - db.sortedSetRemove('notifications', nids, next); - }, - function (next) { - db.deleteAll(keys, next); - }, - ], function (err) { - if (err) { - return winston.error('Encountered error pruning notifications: ' + err.message); - } - }); + async.parallel([ + function (next) { + db.sortedSetRemove('notifications', nids, next); + }, + function (next) { + db.deleteAll(keys, next); + }, + ], next); + }, + ], function (err) { + if (err) { + winston.error('Encountered error pruning notifications: ' + err.message); + } + callback(err); }); }; diff --git a/src/user/info.js b/src/user/info.js index 755c7cff8c..056b675ed3 100644 --- a/src/user/info.js +++ b/src/user/info.js @@ -30,21 +30,15 @@ module.exports = function (User) { }, function (_reason, next) { reason = _reason && _reason.length ? _reason[0] : ''; - next(); + next(null, { + uid: uid, + timestamp: timestamp, + expiry: parseInt(expiry, 10), + expiry_readable: new Date(parseInt(expiry, 10)).toString(), + reason: validator.escape(String(reason)), + }); }, - ], function (err) { - if (err) { - return callback(err); - } - - callback(null, { - uid: uid, - timestamp: timestamp, - expiry: parseInt(expiry, 10), - expiry_readable: new Date(parseInt(expiry, 10)).toString(), - reason: validator.escape(String(reason)), - }); - }); + ], callback); }; User.getModerationHistory = function (uid, callback) { @@ -59,48 +53,46 @@ module.exports = function (User) { function (data, next) { getFlagMetadata(data, next); }, - ], function (err, data) { - if (err) { - return callback(err); - } - formatBanData(data); - callback(null, data); - }); + function (data, next) { + formatBanData(data); + next(null, data); + }, + ], callback); }; User.getHistory = function (set, callback) { - db.getSortedSetRevRangeWithScores(set, 0, -1, function (err, data) { - if (err) { - return callback(err); - } - callback(null, data.map(function (set) { - set.timestamp = set.score; - set.timestampISO = utils.toISOString(set.score); - set.value = validator.escape(String(set.value.split(':')[0])); - delete set.score; - return set; - })); - }); + async.waterfall([ + function (next) { + db.getSortedSetRevRangeWithScores(set, 0, -1, next); + }, + function (data, next) { + next(null, data.map(function (set) { + set.timestamp = set.score; + set.timestampISO = utils.toISOString(set.score); + set.value = validator.escape(String(set.value.split(':')[0])); + delete set.score; + return set; + })); + }, + ], callback); }; function getFlagMetadata(data, callback) { var pids = data.flags.map(function (flagObj) { return parseInt(flagObj.value, 10); }); + async.waterfall([ + function (next) { + posts.getPostsFields(pids, ['tid'], next); + }, + function (postData, next) { + var tids = postData.map(function (post) { + return post.tid; + }); - posts.getPostsFields(pids, ['tid'], function (err, postData) { - if (err) { - return callback(err); - } - - var tids = postData.map(function (post) { - return post.tid; - }); - - topics.getTopicsFields(tids, ['title'], function (err, topicData) { - if (err) { - return callback(err); - } + topics.getTopicsFields(tids, ['title'], next); + }, + function (topicData, next) { data.flags = data.flags.map(function (flagObj, idx) { flagObj.pid = flagObj.value; flagObj.timestamp = flagObj.score; @@ -112,10 +104,9 @@ module.exports = function (User) { return _.extend(flagObj, topicData[idx]); }); - - callback(null, data); - }); - }); + next(null, data); + }, + ], callback); } function formatBanData(data) { diff --git a/src/user/profile.js b/src/user/profile.js index 8347d6de74..91956e9bf4 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -195,27 +195,31 @@ module.exports = function (User) { return callback(); } - User.getUserFields(uid, ['username', 'userslug'], function (err, userData) { - if (err) { - return callback(err); - } - - async.parallel([ - function (next) { - updateUidMapping('username', uid, newUsername, userData.username, next); - }, - function (next) { - var newUserslug = utils.slugify(newUsername); - updateUidMapping('userslug', uid, newUserslug, userData.userslug, next); - }, - function (next) { - async.series([ - async.apply(db.sortedSetRemove, 'username:sorted', userData.username.toLowerCase() + ':' + uid), - async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid), - async.apply(db.sortedSetAdd, 'user:' + uid + ':usernames', Date.now(), newUsername + ':' + Date.now()), - ], next); - }, - ], callback); + async.waterfall([ + function (next) { + User.getUserFields(uid, ['username', 'userslug'], next); + }, + function (userData, next) { + async.parallel([ + function (next) { + updateUidMapping('username', uid, newUsername, userData.username, next); + }, + function (next) { + var newUserslug = utils.slugify(newUsername); + updateUidMapping('userslug', uid, newUserslug, userData.userslug, next); + }, + function (next) { + var now = Date.now(); + async.series([ + async.apply(db.sortedSetRemove, 'username:sorted', userData.username.toLowerCase() + ':' + uid), + async.apply(db.sortedSetAdd, 'username:sorted', 0, newUsername.toLowerCase() + ':' + uid), + async.apply(db.sortedSetAdd, 'user:' + uid + ':usernames', now, newUsername + ':' + now), + ], next); + }, + ], next); + }, + ], function (err) { + callback(err); }); } diff --git a/test/notifications.js b/test/notifications.js index d65b4c0bb5..0100c78737 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -251,6 +251,10 @@ describe('Notifications', function () { }); }); + it('should prune notifications', function (done) { + notifications.prune(done); + }); + after(function (done) { db.emptydb(done); diff --git a/test/user.js b/test/user.js index 943c348872..bfef9a1178 100644 --- a/test/user.js +++ b/test/user.js @@ -95,7 +95,7 @@ describe('User', function () { assert.ifError(err); assert.strictEqual(username, 'Jane Doe 9'); - done(); + next(); }); }, ], done); @@ -299,7 +299,7 @@ describe('User', function () { }); it('.send() should create a new reset code and reset password', function (done) { - User.reset.send('reset@me.com', function (err, code) { + User.reset.send('reset@me.com', function (err) { if (err) { console.log(err); } @@ -751,7 +751,28 @@ describe('User', function () { }); }); - describe('.getModerationHistory', function () { + describe('user info', function () { + it('should return error if there is no ban reason', function (done) { + User.getLatestBanInfo(123, function (err) { + assert.equal(err.message, 'no-ban-info'); + done(); + }); + }); + + + it('should get history from set', function (done) { + var now = Date.now(); + db.sortedSetAdd('user:' + testUid + ':usernames', now, 'derp:' + now, function (err) { + assert.ifError(err); + User.getHistory('user:' + testUid + ':usernames', function (err, data) { + assert.ifError(err); + assert.equal(data[0].value, 'derp'); + assert.equal(data[0].timestamp, now); + done(); + }); + }); + }); + it('should return the correct ban reason', function (done) { async.series([ function (next) { @@ -960,15 +981,15 @@ describe('User', function () { assert.ifError(err); socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'this is a test user' }, function (err) { assert.ifError(err); - db.getSortedSetRevRange('uid:' + testUid + ':moderation:notes', 0, 0, function (err, notes) { + socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'second moderation note' }, function (err) { assert.ifError(err); - notes = notes.map(function (noteData) { - return JSON.parse(noteData); + User.getModerationNotes(testUid, 0, -1, function (err, notes) { + assert.ifError(err); + assert.equal(notes[0].note, 'second moderation note'); + assert.equal(notes[0].uid, adminUid); + assert(notes[0].timestamp); + done(); }); - assert.equal(notes[0].note, 'this is a test user'); - assert.equal(notes[0].uid, adminUid); - assert(notes[0].timestamp); - done(); }); }); }); From 6fdad4b0023c80cf838b5c93369a01ba0845d07c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 13 May 2017 22:12:52 -0400 Subject: [PATCH 078/263] fix test on redis --- src/socket.io/user.js | 6 ++---- test/user.js | 43 ++++++++++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/socket.io/user.js b/src/socket.io/user.js index a2e4413c9a..9777c46f05 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -15,7 +15,7 @@ var db = require('../database'); var userController = require('../controllers/user'); var privileges = require('../privileges'); -var SocketUser = {}; +var SocketUser = module.exports; require('./user/profile')(SocketUser); require('./user/search')(SocketUser); @@ -327,7 +327,7 @@ SocketUser.setModerationNote = function (socket, data, callback) { }, function (allowed, next) { if (allowed) { - return next(null, allowed); + return setImmediate(next, null, allowed); } user.isModeratorOfAnyCategory(socket.uid, next); @@ -346,5 +346,3 @@ SocketUser.setModerationNote = function (socket, data, callback) { }, ], callback); }; - -module.exports = SocketUser; diff --git a/test/user.js b/test/user.js index bfef9a1178..1cdcce717b 100644 --- a/test/user.js +++ b/test/user.js @@ -975,24 +975,33 @@ describe('User', function () { }); it('should set moderation note', function (done) { - User.create({ username: 'noteadmin' }, function (err, adminUid) { + var adminUid; + async.waterfall([ + function (next) { + User.create({ username: 'noteadmin' }, next); + }, + function (_adminUid, next) { + adminUid = _adminUid; + groups.join('administrators', adminUid, next); + }, + function (next) { + socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'this is a test user' }, next); + }, + function (next) { + setTimeout(next, 50); + }, + function (next) { + socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'second moderation note' }, next); + }, + function (next) { + User.getModerationNotes(testUid, 0, -1, next); + }, + ], function (err, notes) { assert.ifError(err); - groups.join('administrators', adminUid, function (err) { - assert.ifError(err); - socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'this is a test user' }, function (err) { - assert.ifError(err); - socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'second moderation note' }, function (err) { - assert.ifError(err); - User.getModerationNotes(testUid, 0, -1, function (err, notes) { - assert.ifError(err); - assert.equal(notes[0].note, 'second moderation note'); - assert.equal(notes[0].uid, adminUid); - assert(notes[0].timestamp); - done(); - }); - }); - }); - }); + assert.equal(notes[0].note, 'second moderation note'); + assert.equal(notes[0].uid, adminUid); + assert(notes[0].timestamp); + done(); }); }); }); From 33023ea419c4ac36a6dff1441df7956cc3e34e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 13 May 2017 22:15:31 -0400 Subject: [PATCH 079/263] create a notif before pruning --- test/notifications.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/notifications.js b/test/notifications.js index 0100c78737..c5a22a6d2c 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -252,7 +252,14 @@ describe('Notifications', function () { }); it('should prune notifications', function (done) { - notifications.prune(done); + notifications.create({ + bodyShort: 'bodyShort', + nid: 'tobedeleted', + path: '/notification/path', + }, function (err) { + assert.ifError(err); + notifications.prune(done); + }); }); From a28b76861d8c2956d23c47adf009b9cb2b26ff0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 13 May 2017 22:29:22 -0400 Subject: [PATCH 080/263] perma and temp ban test --- test/user.js | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/test/user.js b/test/user.js index 1cdcce717b..9eae6f7f09 100644 --- a/test/user.js +++ b/test/user.js @@ -798,6 +798,35 @@ describe('User', function () { }); }); }); + + it('should ban user permanently', function (done) { + User.ban(testUid, function (err) { + assert.ifError(err); + User.isBanned(testUid, function (err, isBanned) { + assert.ifError(err); + assert.equal(isBanned, true); + User.unban(testUid, done); + }); + }); + }); + + it('should ban user temporarily', function (done) { + User.ban(testUid, Date.now() + 2000, function (err) { + assert.ifError(err); + + User.isBanned(testUid, function (err, isBanned) { + assert.ifError(err); + assert.equal(isBanned, true); + setTimeout(function () { + User.isBanned(testUid, function (err, isBanned) { + assert.ifError(err); + assert.equal(isBanned, false); + User.unban(testUid, done); + }); + }, 3000); + }); + }); + }); }); describe('digests', function () { From ed9a429ca9f7f7ade93c0140b75c22b21dfab6f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 13 May 2017 22:45:13 -0400 Subject: [PATCH 081/263] test invalid expiry --- src/user/bans.js | 16 ++++++++-------- test/user.js | 7 +++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/user/bans.js b/src/user/bans.js index 1795fc64e9..ad51c7c07c 100644 --- a/src/user/bans.js +++ b/src/user/bans.js @@ -84,13 +84,13 @@ module.exports = function (User) { }; User.getBannedReason = function (uid, callback) { - // Grabs the latest ban reason - db.getSortedSetRevRange('banned:' + uid + ':reasons', 0, 0, function (err, reasons) { - if (err) { - return callback(err); - } - - callback(null, reasons.length ? reasons[0] : ''); - }); + async.waterfall([ + function (next) { + db.getSortedSetRevRange('banned:' + uid + ':reasons', 0, 0, next); + }, + function (reasons, next) { + next(null, reasons.length ? reasons[0] : ''); + }, + ], callback); }; }; diff --git a/test/user.js b/test/user.js index 9eae6f7f09..87bcf39e70 100644 --- a/test/user.js +++ b/test/user.js @@ -827,6 +827,13 @@ describe('User', function () { }); }); }); + + it('should error if until is NaN', function (done) { + User.ban(testUid, 'asd', function (err) { + assert.equal(err.message, '[[error:ban-expiry-missing]]'); + done(); + }); + }); }); describe('digests', function () { From f3008391d68868a35e257039277290d5cc077b60 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Sun, 14 May 2017 09:22:50 +0000 Subject: [PATCH 082/263] Latest translations and fallbacks --- public/language/uk/admin/general/dashboard.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/language/uk/admin/general/dashboard.json b/public/language/uk/admin/general/dashboard.json index 24778fe52e..9e90a211ea 100644 --- a/public/language/uk/admin/general/dashboard.json +++ b/public/language/uk/admin/general/dashboard.json @@ -11,8 +11,8 @@ "page-views-custom": "Заданий Період", "page-views-custom-start": "Початок Періоду", "page-views-custom-end": "Кінець Періоду", - "page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is YYYY-MM-DD", - "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + "page-views-custom-help": "Вкажіть календарний період, за який ви хочете побачити переглянуті сторінки. Якщо ви не можете використати селектор дат, допустимий формат дати YYYY-MM-DD", + "page-views-custom-error": "Будь-ласка вкажіть календарний період у форматі YYYY-MM-DD", "stats.day": "День", "stats.week": "Тиждень", From f7c31a6fa1cf40c38a3e6e7c08fdde83079cecd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Mon, 15 May 2017 09:24:44 -0400 Subject: [PATCH 083/263] up vanilla --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f97ac95349..dff73ec5d5 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "4.0.0", "nodebb-theme-persona": "5.0.0", - "nodebb-theme-vanilla": "6.0.1", + "nodebb-theme-vanilla": "6.0.2", "nodebb-widget-essentials": "3.0.0", "nodemailer": "2.6.4", "nodemailer-sendmail-transport": "1.0.0", From d3d58b77d96c46e31847067d077bc7d57851d08c Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 15 May 2017 11:43:23 -0400 Subject: [PATCH 084/263] added test for custom date range ACP, #5541 --- test/socket.io.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/socket.io.js b/test/socket.io.js index 820a845c82..38e64fbd25 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -346,6 +346,18 @@ describe('socket.io', function () { }); }); + it('should allow a custom date range for traffic graph analytics', function (done) { + io.emit('admin.analytics.get', { graph: 'traffic', units: 'days', amount: '7' }, function (err, data) { + assert.ifError(err); + assert(data); + assert(data.pageviews); + assert(data.uniqueVisitors); + assert.strictEqual(7, data.pageviews.length); + assert.strictEqual(7, data.uniqueVisitors.length); + done(); + }); + }); + it('should return error', function (done) { socketAdmin.before({ uid: 10 }, 'someMethod', {}, function (err) { assert.equal(err.message, '[[error:no-privileges]]'); From 5f1eb70d0164a86e627f782260ed3da724343cea Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Mon, 15 May 2017 12:19:47 -0400 Subject: [PATCH 085/263] fixes #5680 --- src/user/info.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/user/info.js b/src/user/info.js index 056b675ed3..20228aa161 100644 --- a/src/user/info.js +++ b/src/user/info.js @@ -51,7 +51,29 @@ module.exports = function (User) { }, next); }, function (data, next) { - getFlagMetadata(data, next); + // Get pids from flag objects + var keys = data.flags.map(function (flagObj) { + return 'flag:' + flagObj.value; + }); + db.getObjectsFields(keys, ['type', 'targetId'], function (err, payload) { + if (err) { + return next(err); + } + + // Only pass on flag ids from posts + data.flags = payload.reduce(function (memo, cur, idx) { + if (cur.type === 'post') { + memo.push({ + value: parseInt(cur.targetId, 10), + score: data.flags[idx].score, + }); + } + + return memo; + }, []); + + getFlagMetadata(data, next); + }); }, function (data, next) { formatBanData(data); From e49f33317d596220b1168bcc72672b9016d0167a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 16 May 2017 17:14:50 -0400 Subject: [PATCH 086/263] digest cleanup and tests --- src/user/digest.js | 208 ++++++++++++++++++++++----------------------- src/user/jobs.js | 61 +++++++------ test/user.js | 24 ++++++ 3 files changed, 156 insertions(+), 137 deletions(-) diff --git a/src/user/digest.js b/src/user/digest.js index bf11e23a65..6381a8e262 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -12,121 +12,117 @@ var plugins = require('../plugins'); var emailer = require('../emailer'); var utils = require('../utils'); -(function (Digest) { - Digest.execute = function (interval, callback) { - callback = callback || function () {}; +var Digest = module.exports; - var digestsDisabled = parseInt(meta.config.disableEmailSubscriptions, 10) === 1; - if (digestsDisabled) { - winston.info('[user/jobs] Did not send digests (' + interval + ') because subscription system is disabled.'); - return callback(); - } +Digest.execute = function (interval, callback) { + callback = callback || function () {}; - if (!interval) { - // interval is one of: day, week, month, or year - interval = 'day'; - } - var subscribers; - async.waterfall([ - function (next) { - async.parallel({ - topics: async.apply(topics.getLatestTopics, 0, 0, 9, interval), - subscribers: async.apply(Digest.getSubscribers, interval), - }, next); - }, - function (data, next) { - subscribers = data.subscribers; - if (!data.subscribers.length) { - return callback(); + var digestsDisabled = parseInt(meta.config.disableEmailSubscriptions, 10) === 1; + if (digestsDisabled) { + winston.info('[user/jobs] Did not send digests (' + interval + ') because subscription system is disabled.'); + return callback(); + } + + var subscribers; + async.waterfall([ + function (next) { + async.parallel({ + topics: async.apply(topics.getLatestTopics, 0, 0, 9, interval), + subscribers: async.apply(Digest.getSubscribers, interval), + }, next); + }, + function (data, next) { + subscribers = data.subscribers; + if (!data.subscribers.length) { + return callback(); + } + + // Fix relative paths in topic data + data.topics.topics = data.topics.topics.map(function (topicObj) { + var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; + if (user && user.picture && utils.isRelativeUrl(user.picture)) { + user.picture = nconf.get('base_url') + user.picture; } - // Fix relative paths in topic data - data.topics.topics = data.topics.topics.map(function (topicObj) { - var user = topicObj.hasOwnProperty('teaser') && topicObj.teaser !== undefined ? topicObj.teaser.user : topicObj.user; - if (user && user.picture && utils.isRelativeUrl(user.picture)) { - user.picture = nconf.get('base_url') + user.picture; - } + return topicObj; + }); - return topicObj; - }); + data.interval = interval; + Digest.send(data, next); + }, + ], function (err) { + if (err) { + winston.error('[user/jobs] Could not send digests (' + interval + '): ' + err.message); + } else { + winston.info('[user/jobs] Digest (' + interval + ') scheduling completed. ' + subscribers.length + ' email(s) sent.'); + } - data.interval = interval; - Digest.send(data, next); - }, - ], function (err) { - if (err) { - winston.error('[user/jobs] Could not send digests (' + interval + '): ' + err.message); - } else { - winston.info('[user/jobs] Digest (' + interval + ') scheduling completed. ' + subscribers.length + ' email(s) sent.'); - } + callback(err); + }); +}; - callback(err); - }); - }; +Digest.getSubscribers = function (interval, callback) { + async.waterfall([ + function (next) { + db.getSortedSetRange('digest:' + interval + ':uids', 0, -1, next); + }, + function (subscribers, next) { + plugins.fireHook('filter:digest.subscribers', { + interval: interval, + subscribers: subscribers, + }, next); + }, + function (results, next) { + next(null, results.subscribers); + }, + ], callback); +}; - Digest.getSubscribers = function (interval, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRange('digest:' + interval + ':uids', 0, -1, next); - }, - function (subscribers, next) { - plugins.fireHook('filter:digest.subscribers', { - interval: interval, - subscribers: subscribers, - }, next); - }, - function (results, next) { - next(null, results.subscribers); - }, - ], callback); - }; +Digest.send = function (data, callback) { + if (!data || !data.subscribers || !data.subscribers.length) { + return callback(); + } + var now = new Date(); - Digest.send = function (data, callback) { - if (!data || !data.subscribers || !data.subscribers.length) { - return callback(); - } - var now = new Date(); + async.waterfall([ + function (next) { + user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], next); + }, + function (users, next) { + async.eachLimit(users, 100, function (userObj, next) { + async.waterfall([ + function (next) { + user.notifications.getDailyUnread(userObj.uid, next); + }, + function (notifications, next) { + notifications = notifications.filter(Boolean); + // If there are no notifications and no new topics, don't bother sending a digest + if (!notifications.length && !data.topics.topics.length) { + return next(); + } - async.waterfall([ - function (next) { - user.getUsersFields(data.subscribers, ['uid', 'username', 'userslug', 'lastonline'], next); - }, - function (users, next) { - async.eachLimit(users, 100, function (userObj, next) { - async.waterfall([ - function (next) { - user.notifications.getDailyUnread(userObj.uid, next); - }, - function (notifications, next) { - notifications = notifications.filter(Boolean); - // If there are no notifications and no new topics, don't bother sending a digest - if (!notifications.length && !data.topics.topics.length) { - return next(); + notifications.forEach(function (notification) { + if (notification.image && !notification.image.startsWith('http')) { + notification.image = nconf.get('url') + notification.image; } + }); - notifications.forEach(function (notification) { - if (notification.image && !notification.image.startsWith('http')) { - notification.image = nconf.get('url') + notification.image; - } - }); - - emailer.send('digest', userObj.uid, { - 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.topics, - interval: data.interval, - }); - next(); - }, - ], next); - }, next); - }, - ], function (err) { - callback(err); - }); - }; -}(module.exports)); + emailer.send('digest', userObj.uid, { + 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.topics, + interval: data.interval, + }); + next(); + }, + ], next); + }, next); + }, + ], function (err) { + callback(err); + }); +}; diff --git a/src/user/jobs.js b/src/user/jobs.js index 1eeeb5650e..7950e3fdcb 100644 --- a/src/user/jobs.js +++ b/src/user/jobs.js @@ -10,7 +10,7 @@ var jobs = {}; module.exports = function (User) { User.startJobs = function (callback) { winston.verbose('[user/jobs] (Re-)starting user jobs...'); - var terminated = 0; + var started = 0; var digestHour = parseInt(meta.config.digestHour, 10); @@ -21,37 +21,12 @@ module.exports = function (User) { digestHour = 0; } - // Terminate any active cron jobs - for (var jobId in jobs) { - if (jobs.hasOwnProperty(jobId)) { - winston.verbose('[user/jobs] Terminating job (' + jobId + ')'); - jobs[jobId].stop(); - delete jobs[jobId]; - terminated += 1; - } - } - winston.verbose('[user/jobs] ' + terminated + ' jobs terminated'); - - jobs['digest.daily'] = new cronJob('0 ' + digestHour + ' * * *', function () { - winston.verbose('[user/jobs] Digest job (daily) started.'); - User.digest.execute('day'); - }, null, true); - winston.verbose('[user/jobs] Starting job (digest.daily)'); - started += 1; - - jobs['digest.weekly'] = new cronJob('0 ' + digestHour + ' * * 0', function () { - winston.verbose('[user/jobs] Digest job (weekly) started.'); - User.digest.execute('week'); - }, null, true); - winston.verbose('[user/jobs] Starting job (digest.weekly)'); - started += 1; + User.stopJobs(); - jobs['digest.monthly'] = new cronJob('0 ' + digestHour + ' 1 * *', function () { - winston.verbose('[user/jobs] Digest job (monthly) started.'); - User.digest.execute('month'); - }, null, true); - winston.verbose('[user/jobs] Starting job (digest.monthly)'); - started += 1; + startDigestJob('digest.daily', '0 ' + digestHour + ' * * *', 'day'); + startDigestJob('digest.weekly', '0 ' + digestHour + ' * * 0', 'week'); + startDigestJob('digest.monthly', '0 ' + digestHour + ' 1 * *', 'month'); + started += 3; jobs['reset.clean'] = new cronJob('0 0 * * *', User.reset.clean, null, true); winston.verbose('[user/jobs] Starting job (reset.clean)'); @@ -63,5 +38,29 @@ module.exports = function (User) { callback(); } }; + + function startDigestJob(name, cronString, term) { + jobs[name] = new cronJob(cronString, function () { + winston.verbose('[user/jobs] Digest job (' + name + ') started.'); + User.digest.execute(term); + }, null, true); + winston.verbose('[user/jobs] Starting job (' + name + ')'); + } + + User.stopJobs = function () { + var terminated = 0; + // Terminate any active cron jobs + for (var jobId in jobs) { + if (jobs.hasOwnProperty(jobId)) { + winston.verbose('[user/jobs] Terminating job (' + jobId + ')'); + jobs[jobId].stop(); + delete jobs[jobId]; + terminated += 1; + } + } + if (terminated > 0) { + winston.verbose('[user/jobs] ' + terminated + ' jobs terminated'); + } + }; }; diff --git a/test/user.js b/test/user.js index 87bcf39e70..f7d9986336 100644 --- a/test/user.js +++ b/test/user.js @@ -1301,6 +1301,30 @@ describe('User', function () { }); }); + describe('user jobs', function () { + it('should start user jobs', function (done) { + User.startJobs(function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should stop user jobs', function (done) { + User.stopJobs(); + done(); + }); + + it('should send digetst', function (done) { + db.sortedSetAdd('digest:day:uids', [Date.now(), Date.now()], [1, 2], function (err) { + assert.ifError(err); + User.digest.execute('day', function (err) { + assert.ifError(err); + done(); + }); + }); + }); + }); + after(function (done) { db.emptydb(done); From 12f73b7d6f69f6b8418542ba964174740289aad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 16 May 2017 19:24:42 -0400 Subject: [PATCH 087/263] closes #5682 --- src/controllers/unread.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/controllers/unread.js b/src/controllers/unread.js index 5e00cf5c96..07d97ede0e 100644 --- a/src/controllers/unread.js +++ b/src/controllers/unread.js @@ -8,9 +8,10 @@ var validator = require('validator'); var pagination = require('../pagination'); var user = require('../user'); var topics = require('../topics'); +var plugins = require('../plugins'); var helpers = require('./helpers'); -var unreadController = {}; +var unreadController = module.exports; var validFilter = { '': true, new: true, watched: true }; @@ -19,13 +20,17 @@ unreadController.get = function (req, res, next) { var results; var cid = req.query.cid; var filter = req.params.filter || ''; - - if (!validFilter[filter]) { - return next(); - } var settings; + async.waterfall([ function (next) { + plugins.fireHook('filter:unread.getValidFilters', { filters: validFilter }, next); + }, + function (data, _next) { + if (!data.filters[filter]) { + return next(); + } + async.parallel({ watchedCategories: function (next) { helpers.getWatchedCategories(req.uid, cid, next); @@ -33,7 +38,7 @@ unreadController.get = function (req, res, next) { settings: function (next) { user.getSettings(req.uid, next); }, - }, next); + }, _next); }, function (_results, next) { results = _results; @@ -113,5 +118,3 @@ unreadController.unreadTotal = function (req, res, next) { res.json(data); }); }; - -module.exports = unreadController; From 11fc79051af7dc69ccc794a727c90537b0b3654f Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Wed, 17 May 2017 09:23:36 +0000 Subject: [PATCH 088/263] Latest translations and fallbacks --- public/language/th/language.json | 2 +- public/language/th/user.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/public/language/th/language.json b/public/language/th/language.json index 8af024bba9..16f46a1385 100644 --- a/public/language/th/language.json +++ b/public/language/th/language.json @@ -1,5 +1,5 @@ { - "name": "ภาษาเขียน", + "name": "ภาษาไทย", "code": "th", "dir": "ltr" } \ No newline at end of file diff --git a/public/language/th/user.json b/public/language/th/user.json index 345583ac68..538c59c7f7 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -4,8 +4,8 @@ "username": "ชื่อผู้ใช้", "joindate": "วันที่เข้าร่วม", "postcount": "จำนวนโพสต์", - "email": "อีเมล", - "confirm_email": "ยืนยันอีเมล", + "email": "อีเมล์", + "confirm_email": "ยืนยันอีเมล์", "account_info": "ข้อมูลบัญชี", "ban_account": "แบนบัญชี", "ban_account_confirm": "คุณต้องการแบนผู้ใช้นี้หรือไม่?", @@ -60,7 +60,7 @@ "username_taken_workaround": "ชื้อผู้ใช้นี้ถูกใช้แล้ว เราทำการแก้ไขชื่อผู้ใช้ของคุณเล็กน้อยเป็น %1", "password_same_as_username": "คุณใช้รหัสผ่านเดียวกับชื่อผู้ใช้ กรุณาเปลี่ยนรหัสผ่านใหม่", "password_same_as_email": "คุณใช้รหัสผ่านเดียวกับอีเมล กรุณาเปลี่ยนรหัสผ่านใหม่", - "weak_password": "Weak password.", + "weak_password": "พาสเวิร์ดเดาได้ง่าย", "upload_picture": "อัพโหลดรูป", "upload_a_picture": "อัพโหลดรูป", "remove_uploaded_picture": "ลบภาพที่อัพโหลดไว้", @@ -103,11 +103,11 @@ "browsing": "เปิดดูการตั้งค่า", "open_links_in_new_tab": "เปิดลิงค์ในแท็บใหม่", "enable_topic_searching": "เปิดใช้การค้นหาแบบ In-Topic", - "topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen", + "topic_search_help": "หากเปิดใช้งาน, \"การค้นหาภายในกระทู้\" จะแทนที่ระบบ \"การค้นหาจากค่าเริ่มต้นของเบราเซอร์\" และจะทำให้คุณค้นหาข้อมูลต่างๆภายในกระทู้ได้ แทนที่จะเป็นการหาแค่สิ่งที่แสดงบนหน้าจอเท่านั้น", "delay_image_loading": "Delay Image Loading", - "image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view", + "image_load_delay_help": "หากเปิดใช้งาน, รูปภาพในกระทู้จะไม่โหลดจนกว่าจะมีการเลื่อนไปดู", "scroll_to_my_post": "After posting a reply, show the new post", - "follow_topics_you_reply_to": "Watch topics that you reply to", + "follow_topics_you_reply_to": "ดูกระทู้ที่คุณตอบ", "follow_topics_you_create": "Watch topics you create", "grouptitle": "Group Title", "no-group-title": "No group title", From 1f8ef629e6e8c0c8be6c9fc119c6a2e826fbad46 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 17 May 2017 15:59:55 -0600 Subject: [PATCH 089/263] Extract in-place translation from bootbox wrapper for use anywhere --- public/src/modules/translator.js | 66 +++++++++++++++++++++++++++++++- public/vendor/bootbox/wrapper.js | 61 ++++------------------------- 2 files changed, 72 insertions(+), 55 deletions(-) diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index afd64317ef..5296e1eef4 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -304,7 +304,7 @@ * Load translation file (or use a cached version), and optionally return the translation of a certain key * @param {string} namespace - The file name of the translation namespace * @param {string} [key] - The key of the specific translation to getJSON - * @returns {Promise|Promise} + * @returns {Promise<{ [key: string]: string }>|Promise} */ Translator.prototype.getTranslation = function getTranslation(namespace, key) { var translation; @@ -324,6 +324,70 @@ return translation; }; + /** + * @param {Node} node + * @returns {Node[]} + */ + function descendantTextNodes(node) { + var textNodes = []; + + function helper(node) { + if (node.nodeType === 3) { + textNodes.push(node); + } else { + for (var i = 0, c = node.childNodes, l = c.length; i < l; i += 1) { + helper(c[i]); + } + } + } + + helper(node); + return textNodes; + } + + /** + * Recursively translate a DOM element in place + * @param {Element} element - Root element to translate + * @param {string[]} [attributes] - Array of node attributes to translate + * @returns {Promise} + */ + Translator.prototype.translateInPlace = function translateInPlace(element, attributes) { + attributes = attributes || ['placeholder', 'title']; + + var nodes = descendantTextNodes(element); + var text = nodes.map(function (node) { + return node.nodeValue; + }).join(' || '); + + var attrNodes = attributes.reduce(function (prev, attr) { + var tuples = Array.prototype.map.call(element.querySelectorAll('[' + attr + '*="[["]'), function (el) { + return [attr, el]; + }); + return prev.concat(tuples); + }, []); + var attrText = attrNodes.map(function (node) { + return node[1].getAttribute(node[0]); + }).join(' || '); + + return Promise.all([ + this.translate(text), + this.translate(attrText), + ]).then(function (ref) { + var translated = ref[0]; + var translatedAttrs = ref[1]; + if (translated) { + translated.split(' || ').forEach(function (html, i) { + $(nodes[i]).replaceWith(html); + }); + } + if (translatedAttrs) { + translatedAttrs.split(' || ').forEach(function (text, i) { + attrNodes[i][1].setAttribute(attrNodes[i][0], text); + }); + } + }); + }; + /** * Get the language of the current environment, falling back to defaults * @returns {string} diff --git a/public/vendor/bootbox/wrapper.js b/public/vendor/bootbox/wrapper.js index 505b42788f..35efdd8305 100644 --- a/public/vendor/bootbox/wrapper.js +++ b/public/vendor/bootbox/wrapper.js @@ -1,67 +1,20 @@ /* global bootbox */ require(['translator'], function (shim) { - "use strict"; - - function descendantTextNodes(node) { - var textNodes = []; - - function helper(node) { - if (node.nodeType === 3) { - textNodes.push(node); - } else { - for (var i = 0, c = node.childNodes, l = c.length; i < l; i += 1) { - helper(c[i]); - } - } - } - - helper(node); - return textNodes; - } + 'use strict'; var translator = shim.Translator.create(); var dialog = bootbox.dialog; var attrsToTranslate = ['placeholder', 'title', 'value']; bootbox.dialog = function (options) { - var show, $elem, nodes, text, attrNodes, attrText; - - show = options.show !== false; + var show = options.show !== false; options.show = false; - $elem = dialog.call(bootbox, options); + var $elem = dialog.call(bootbox, options); + var element = $elem[0]; - if (/\[\[.+\]\]/.test($elem[0].outerHTML)) { - nodes = descendantTextNodes($elem[0]); - text = nodes.map(function (node) { - return node.nodeValue; - }).join(' || '); - - attrNodes = attrsToTranslate.reduce(function (prev, attr) { - return prev.concat(nodes.map.call($elem.find('[' + attr + '*="[["]'), function (el) { - return [attr, el]; - })); - }, []); - attrText = attrNodes.map(function (node) { - return node[1].getAttribute(node[0]); - }).join(' || '); - - Promise.all([ - translator.translate(text), - translator.translate(attrText), - ]).then(function (ref) { - var translated = ref[0]; - var translatedAttrs = ref[1]; - if (translated) { - translated.split(' || ').forEach(function (html, i) { - $(nodes[i]).replaceWith(html); - }); - } - if (translatedAttrs) { - translatedAttrs.split(' || ').forEach(function (text, i) { - attrNodes[i][1].setAttribute(attrNodes[i][0], text); - }); - } + if (/\[\[.+\]\]/.test(element.outerHTML)) { + translator.translateInPlace(element, attrsToTranslate).then(function () { if (show) { $elem.modal('show'); } @@ -84,7 +37,7 @@ require(['translator'], function (shim) { CANCEL: translations[1], CONFIRM: translations[2], }); - + bootbox.setLocale(lang); }); }); From 7ccfb5cdc02faf1158def95c51adedde383df8ee Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 17 May 2017 16:33:31 -0600 Subject: [PATCH 090/263] Build refactor - Target aliases and bundles - More concurrency - Use `--series` to force series processing - Minifier self-hosted threading - meta/css delegates to minifier --- package.json | 3 +- public/less/admin/admin.less | 2 +- src/meta/build.js | 283 +++++++++++++++++++------------ src/meta/css.js | 163 +++++++++--------- src/meta/js.js | 314 +++++++++++++++-------------------- src/meta/minifier.js | 285 ++++++++++++++++++++++++------- 6 files changed, 612 insertions(+), 438 deletions(-) diff --git a/package.json b/package.json index dff73ec5d5..791c5f36b8 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,8 @@ "coveralls": "istanbul cover _mocha --report lcovonly -- -R dot && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" }, "dependencies": { - "async": "2.4.0", "ace-builds": "^1.2.6", + "async": "2.4.0", "autoprefixer": "7.0.1", "bcryptjs": "2.4.3", "body-parser": "^1.9.0", @@ -45,6 +45,7 @@ "jquery": "^3.1.0", "json-2-csv": "^2.0.22", "less": "^2.0.0", + "lodash.padstart": "^4.6.1", "logrotate-stream": "^0.2.3", "lru-cache": "4.0.2", "mime": "^1.3.4", diff --git a/public/less/admin/admin.less b/public/less/admin/admin.less index a37682ab83..8f713028ed 100644 --- a/public/less/admin/admin.less +++ b/public/less/admin/admin.less @@ -1,4 +1,4 @@ -@import "../../node_modules/bootstrap/less/bootstrap"; +@import "bootstrap/less/bootstrap"; @import "./paper/variables"; @import "./paper/bootswatch"; @import "./mixins"; diff --git a/src/meta/build.js b/src/meta/build.js index 9ba5ec89bf..e88bbb17e9 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -2,149 +2,212 @@ var async = require('async'); var winston = require('winston'); +var os = require('os'); +var nconf = require('nconf'); +var padstart = require('lodash.padstart'); -var buildStart; +var cacheBuster = require('./cacheBuster'); +var meta; -var valid = ['js', 'clientCSS', 'acpCSS', 'tpl', 'lang', 'sound']; +function step(target, callback) { + var startTime = Date.now(); + winston.info('[build] ' + target + ' build started'); -exports.buildAll = function (callback) { - exports.build(valid.join(','), callback); -}; + return function (err) { + if (err) { + winston.error('[build] ' + target + ' build failed'); + return callback(err); + } -exports.build = function build(targets, callback) { - buildStart = Date.now(); + var time = (Date.now() - startTime) / 1000; - var db = require('../database'); - var meta = require('../meta'); - var plugins = require('../plugins'); + winston.info('[build] ' + target + ' build completed in ' + time + 'sec'); + callback(); + }; +} + +var targetHandlers = { + 'plugin static dirs': function (parallel, callback) { + meta.js.linkStatics(callback); + }, + 'requirejs modules': function (parallel, callback) { + meta.js.buildModules(parallel, callback); + }, + 'client js bundle': function (parallel, callback) { + meta.js.buildBundle('client', parallel, callback); + }, + 'admin js bundle': function (parallel, callback) { + meta.js.buildBundle('admin', parallel, callback); + }, + javascript: [ + 'plugin static dirs', + 'requirejs modules', + 'client js bundle', + 'admin js bundle', + ], + 'client side styles': function (parallel, callback) { + meta.css.buildBundle('client', parallel, callback); + }, + 'admin control panel styles': function (parallel, callback) { + meta.css.buildBundle('admin', parallel, callback); + }, + styles: [ + 'client side styles', + 'admin control panel styles', + ], + templates: function (parallel, callback) { + meta.templates.compile(callback); + }, + languages: function (parallel, callback) { + meta.languages.build(callback); + }, + sounds: function (parallel, callback) { + meta.sounds.build(callback); + }, +}; +var aliases = { + 'plugin static dirs': ['staticdirs'], + 'requirejs modules': ['rjs', 'modules'], + 'client js bundle': ['clientjs', 'clientscript', 'clientscripts'], + 'admin js bundle': ['adminjs', 'adminscript', 'adminscripts'], + javascript: ['js'], + 'client side styles': [ + 'clientcss', 'clientless', 'clientstyles', 'clientstyle', + ], + 'admin control panel styles': [ + 'admincss', 'adminless', 'adminstyles', 'adminstyle', 'acpcss', 'acpless', 'acpstyles', 'acpstyle', + ], + styles: ['css', 'less', 'style'], + templates: ['tpl'], + languages: ['lang', 'i18n'], + sounds: ['sound'], +}; - targets = (targets === true ? valid : targets.split(',').filter(function (target) { - return valid.indexOf(target) !== -1; - })); +aliases = Object.keys(aliases).reduce(function (prev, key) { + var arr = aliases[key]; + arr.forEach(function (alias) { + prev[alias] = key; + }); + prev[key] = key; + return prev; +}, {}); - if (!targets) { - winston.error('[build] No valid build targets found. Aborting.'); - return process.exit(0); - } +function beforeBuild(callback) { + var db = require('../database'); + var plugins = require('../plugins'); + meta = require('../meta'); async.series([ - async.apply(db.init), - async.apply(meta.themes.setupPaths), - async.apply(plugins.prepareForBuild), + db.init, + meta.themes.setupPaths, + plugins.prepareForBuild, ], function (err) { if (err) { winston.error('[build] Encountered error preparing for build: ' + err.message); - return process.exit(1); + return callback(err); } - exports.buildTargets(targets, callback); + callback(); }); -}; +} -exports.buildTargets = function (targets, callback) { - var cacheBuster = require('./cacheBuster'); - var meta = require('../meta'); - var numCpus = require('os').cpus().length; - var parallel = targets.length > 1 && numCpus > 1; +var allTargets = Object.keys(targetHandlers).filter(function (name) { + return typeof targetHandlers[name] === 'function'; +}); +function buildTargets(targets, parallel, callback) { + var all = parallel ? async.each : async.eachSeries; - buildStart = buildStart || Date.now(); + var length = Math.max.apply(Math, targets.map(function (name) { + return name.length; + })); - var step = function (startTime, target, next, err) { - if (err) { - winston.error('Build failed: ' + err.stack); - process.exit(1); - } - winston.info('[build] ' + target + ' => Completed in ' + ((Date.now() - startTime) / 1000) + 's'); - next(); - }; + all(targets, function (target, next) { + targetHandlers[target](parallel, step(padstart(target, length) + ' ', next)); + }, callback); +} - if (parallel) { - winston.verbose('[build] Utilising multiple cores/processes'); - } else { - winston.verbose('[build] Utilising single-core'); +function build(targets, callback) { + if (targets === true) { + targets = allTargets; + } else if (!Array.isArray(targets)) { + targets = targets.split(','); } - async[parallel ? 'parallel' : 'series']([ + targets = targets + // get full target name + .map(function (target) { + target = target.toLowerCase().replace(/-/g, ''); + if (!aliases[target]) { + winston.warn('[build] Unknown target: ' + target); + return false; + } + + return aliases[target]; + }) + // filter nonexistent targets + .filter(Boolean) + // map multitargets to their sets + .reduce(function (prev, target) { + if (Array.isArray(targetHandlers[target])) { + return prev.concat(targetHandlers[target]); + } + + return prev.concat(target); + }, []) + // unique + .filter(function (target, i, arr) { + return arr.indexOf(target) === i; + }); + + if (typeof callback !== 'function') { + callback = function (err) { + if (err) { + winston.error(err); + process.exit(1); + } else { + process.exit(0); + } + }; + } + + if (!targets) { + winston.info('[build] No valid targets supplied. Aborting.'); + callback(); + } + + var startTime; + var totalTime; + async.series([ + beforeBuild, function (next) { - if (targets.indexOf('js') !== -1) { - winston.info('[build] Building javascript'); - var startTime = Date.now(); - async.series([ - meta.js.buildModules, - meta.js.linkStatics, - async.apply(meta.js.minify, 'nodebb.min.js'), - async.apply(meta.js.minify, 'acp.min.js'), - ], step.bind(this, startTime, 'js', next)); + var parallel = os.cpus().length > 1 && !nconf.get('series'); + if (parallel) { + winston.info('[build] Building in parallel mode'); } else { - setImmediate(next); + winston.info('[build] Building in series mode'); } + + startTime = Date.now(); + buildTargets(targets, parallel, next); }, function (next) { - async.eachSeries(targets, function (target, next) { - var startTime; - switch (target) { - case 'js': - setImmediate(next); - break; - case 'clientCSS': - winston.info('[build] Building client-side CSS'); - startTime = Date.now(); - meta.css.minify('client', step.bind(this, startTime, target, next)); - break; - - case 'acpCSS': - winston.info('[build] Building admin control panel CSS'); - startTime = Date.now(); - meta.css.minify('admin', step.bind(this, startTime, target, next)); - break; - - case 'tpl': - winston.info('[build] Building templates'); - startTime = Date.now(); - meta.templates.compile(step.bind(this, startTime, target, next)); - break; - - case 'lang': - winston.info('[build] Building language files'); - startTime = Date.now(); - meta.languages.build(step.bind(this, startTime, target, next)); - break; - - case 'sound': - winston.info('[build] Linking sound files'); - startTime = Date.now(); - meta.sounds.build(step.bind(this, startTime, target, next)); - break; - - default: - winston.warn('[build] Unknown build target: \'' + target + '\''); - setImmediate(next); - break; - } - }, next); + totalTime = (Date.now() - startTime) / 1000; + cacheBuster.write(next); }, ], function (err) { if (err) { winston.error('[build] Encountered error during build step: ' + err.message); - return process.exit(1); + return callback(err); } - cacheBuster.write(function (err) { - if (err) { - winston.error('[build] Failed to write `cache-buster.conf`: ' + err.message); - return process.exit(1); - } - - var time = (Date.now() - buildStart) / 1000; + winston.info('[build] Asset compilation successful. Completed in ' + totalTime + 'sec.'); + callback(); + }); +} - winston.info('[build] Asset compilation successful. Completed in ' + time + 's.'); +exports.build = build; - if (typeof callback === 'function') { - callback(); - } else { - process.exit(0); - } - }); - }); +exports.buildAll = function (callback) { + build(allTargets, callback); }; diff --git a/src/meta/css.js b/src/meta/css.js index 88ad3348e8..127191437c 100644 --- a/src/meta/css.js +++ b/src/meta/css.js @@ -4,15 +4,12 @@ var winston = require('winston'); var nconf = require('nconf'); var fs = require('fs'); var path = require('path'); -var less = require('less'); var async = require('async'); -var autoprefixer = require('autoprefixer'); -var postcss = require('postcss'); -var clean = require('postcss-clean'); var plugins = require('../plugins'); var db = require('../database'); var file = require('../file'); +var minifier = require('./minifier'); module.exports = function (Meta) { Meta.css = {}; @@ -49,50 +46,19 @@ module.exports = function (Meta) { }, }; - Meta.css.minify = function (target, callback) { - callback = callback || function () {}; - - winston.verbose('[meta/css] Minifying LESS/CSS'); - db.getObjectFields('config', ['theme:type', 'theme:id'], function (err, themeData) { - if (err) { - return callback(err); - } - - var themeId = (themeData['theme:id'] || 'nodebb-theme-persona'); - var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')); - var paths = [ - baseThemePath, - path.join(__dirname, '../../node_modules'), - path.join(__dirname, '../../public/vendor/fontawesome/less'), - ]; - var source = ''; - - var lessFiles = filterMissingFiles(plugins.lessFiles); - var cssFiles = filterMissingFiles(plugins.cssFiles); - - async.waterfall([ - function (next) { - getStyleSource(cssFiles, '\n@import (inline) ".', '.css', next); - }, - function (src, next) { - source += src; - getStyleSource(lessFiles, '\n@import ".', '.less', next); - }, - function (src, next) { - source += src; - next(); - }, - ], function (err) { - if (err) { - return callback(err); + function filterMissingFiles(filepaths, callback) { + async.filter(filepaths, function (filepath, next) { + file.exists(path.join(__dirname, '../../node_modules', filepath), function (err, exists) { + if (!exists) { + winston.warn('[meta/css] File not found! ' + filepath); } - minify(buildImports[target](source), paths, target, callback); + next(err, exists); }); - }); - }; + }, callback); + } - function getStyleSource(files, prefix, extension, callback) { + function getImports(files, prefix, extension, callback) { var pluginDirectories = []; var source = ''; @@ -121,55 +87,82 @@ module.exports = function (Meta) { }); } - Meta.css.commitToFile = function (target, source, callback) { - var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css'; + function getBundleMetadata(target, callback) { + var paths = [ + path.join(__dirname, '../../node_modules'), + path.join(__dirname, '../../public/vendor/fontawesome/less'), + ]; - fs.writeFile(path.join(__dirname, '../../build/public/' + filename), source, function (err) { - if (!err) { - winston.verbose('[meta/css] ' + target + ' CSS committed to disk.'); - } else { - winston.error('[meta/css] ' + err.message); - process.exit(1); - } + async.waterfall([ + function (next) { + if (target !== 'client') { + return next(null, null); + } - callback(); - }); - }; + db.getObjectFields('config', ['theme:type', 'theme:id'], next); + }, + function (themeData, next) { + if (target === 'client') { + var themeId = (themeData['theme:id'] || 'nodebb-theme-persona'); + var baseThemePath = path.join(nconf.get('themes_path'), (themeData['theme:type'] && themeData['theme:type'] === 'local' ? themeId : 'nodebb-theme-vanilla')); + paths.unshift(baseThemePath); + } - function minify(source, paths, target, callback) { - callback = callback || function () {}; - less.render(source, { - paths: paths, - }, function (err, lessOutput) { + async.parallel({ + less: function (cb) { + async.waterfall([ + function (next) { + filterMissingFiles(plugins.lessFiles, next); + }, + function (lessFiles, next) { + getImports(lessFiles, '\n@import ".', '.less', next); + }, + ], cb); + }, + css: function (cb) { + async.waterfall([ + function (next) { + filterMissingFiles(plugins.cssFiles, next); + }, + function (cssFiles, next) { + getImports(cssFiles, '\n@import (inline) ".', '.css', next); + }, + ], cb); + }, + }, next); + }, + function (result, next) { + var cssImports = result.css; + var lessImports = result.less; + + var imports = cssImports + '\n' + lessImports; + imports = buildImports[target](imports); + + next(null, imports); + }, + ], function (err, imports) { if (err) { - winston.error('[meta/css] Could not minify LESS/CSS: ' + err.message); return callback(err); } - postcss(global.env === 'development' ? [autoprefixer] : [ - autoprefixer, - clean({ - processImportFrom: ['local'], - }), - ]).process(lessOutput.css).then(function (result) { - result.warnings().forEach(function (warn) { - winston.verbose(warn.toString()); - }); - - return Meta.css.commitToFile(target, result.css, function () { - callback(null, result.css); - }); - }); + callback(null, { paths: paths, imports: imports }); }); } - function filterMissingFiles(files) { - return files.filter(function (filePath) { - var exists = file.existsSync(path.join(__dirname, '../../node_modules', filePath)); - if (!exists) { - winston.warn('[meta/css] File not found! ' + filePath); - } - return exists; - }); - } + Meta.css.buildBundle = function (target, fork, callback) { + async.waterfall([ + function (next) { + getBundleMetadata(target, next); + }, + function (data, next) { + var minify = global.env !== 'development'; + minifier.css.bundle(data.imports, data.paths, minify, fork, next); + }, + function (bundle, next) { + var filename = (target === 'client' ? 'stylesheet' : 'admin') + '.css'; + + fs.writeFile(path.join(__dirname, '../../build/public', filename), bundle.code, next); + }, + ], callback); + }; }; diff --git a/src/meta/js.js b/src/meta/js.js index f654d45644..e7b22939fc 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -1,136 +1,128 @@ 'use strict'; -var winston = require('winston'); -var fork = require('child_process').fork; var path = require('path'); var async = require('async'); var fs = require('fs'); var mkdirp = require('mkdirp'); var rimraf = require('rimraf'); -var uglifyjs = require('uglify-js'); var file = require('../file'); var plugins = require('../plugins'); - -var minifierPath = path.join(__dirname, 'minifier.js'); +var minifier = require('./minifier'); module.exports = function (Meta) { - Meta.js = { - target: {}, - scripts: { - base: [ - 'node_modules/jquery/dist/jquery.js', - 'node_modules/socket.io-client/dist/socket.io.js', - 'public/vendor/jquery/timeago/jquery.timeago.js', - 'public/vendor/jquery/js/jquery.form.min.js', - 'public/vendor/visibility/visibility.min.js', - 'node_modules/bootstrap/dist/js/bootstrap.js', - 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js', - 'public/vendor/jquery/textcomplete/jquery.textcomplete.js', - 'public/vendor/requirejs/require.js', - 'public/src/require-config.js', - 'public/vendor/bootbox/bootbox.js', - 'public/vendor/bootbox/wrapper.js', - 'public/vendor/tinycon/tinycon.js', - 'public/vendor/xregexp/xregexp.js', - 'public/vendor/xregexp/unicode/unicode-base.js', - 'node_modules/templates.js/lib/templates.js', - 'public/src/utils.js', - 'public/src/sockets.js', - 'public/src/app.js', - 'public/src/ajaxify.js', - 'public/src/overrides.js', - 'public/src/widgets.js', - 'node_modules/promise-polyfill/promise.js', - ], - - // files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load - rjs: [ - 'public/src/client/footer.js', - 'public/src/client/chats.js', - 'public/src/client/infinitescroll.js', - 'public/src/client/pagination.js', - 'public/src/client/recent.js', - 'public/src/client/unread.js', - 'public/src/client/topic.js', - 'public/src/client/topic/events.js', - 'public/src/client/topic/fork.js', - 'public/src/client/topic/move.js', - 'public/src/client/topic/posts.js', - 'public/src/client/topic/images.js', - 'public/src/client/topic/postTools.js', - 'public/src/client/topic/threadTools.js', - 'public/src/client/categories.js', - 'public/src/client/category.js', - 'public/src/client/category/tools.js', - - 'public/src/modules/translator.js', - 'public/src/modules/notifications.js', - 'public/src/modules/chat.js', - 'public/src/modules/components.js', - 'public/src/modules/sort.js', - 'public/src/modules/navigator.js', - 'public/src/modules/topicSelect.js', - 'public/src/modules/share.js', - 'public/src/modules/search.js', - 'public/src/modules/alerts.js', - 'public/src/modules/taskbar.js', - 'public/src/modules/helpers.js', - 'public/src/modules/string.js', - 'public/src/modules/flags.js', - 'public/src/modules/storage.js', - ], - - // modules listed below are built (/src/modules) so they can be defined anonymously - modules: { - 'Chart.js': 'node_modules/chart.js/dist/Chart.min.js', - 'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js', - 'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js', - 'jqueryui.js': 'public/vendor/jquery/js/jquery-ui.js', - 'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js', - ace: 'node_modules/ace-builds/src-min', - }, + Meta.js = {}; + + Meta.js.scripts = { + base: [ + 'node_modules/jquery/dist/jquery.js', + 'node_modules/socket.io-client/dist/socket.io.js', + 'public/vendor/jquery/timeago/jquery.timeago.js', + 'public/vendor/jquery/js/jquery.form.min.js', + 'public/vendor/visibility/visibility.min.js', + 'node_modules/bootstrap/dist/js/bootstrap.js', + 'public/vendor/jquery/bootstrap-tagsinput/bootstrap-tagsinput.min.js', + 'public/vendor/jquery/textcomplete/jquery.textcomplete.js', + 'public/vendor/requirejs/require.js', + 'public/src/require-config.js', + 'public/vendor/bootbox/bootbox.js', + 'public/vendor/bootbox/wrapper.js', + 'public/vendor/tinycon/tinycon.js', + 'public/vendor/xregexp/xregexp.js', + 'public/vendor/xregexp/unicode/unicode-base.js', + 'node_modules/templates.js/lib/templates.js', + 'public/src/utils.js', + 'public/src/sockets.js', + 'public/src/app.js', + 'public/src/ajaxify.js', + 'public/src/overrides.js', + 'public/src/widgets.js', + 'node_modules/promise-polyfill/promise.js', + ], + + // files listed below are only available client-side, or are bundled in to reduce # of network requests on cold load + rjs: [ + 'public/src/client/footer.js', + 'public/src/client/chats.js', + 'public/src/client/infinitescroll.js', + 'public/src/client/pagination.js', + 'public/src/client/recent.js', + 'public/src/client/unread.js', + 'public/src/client/topic.js', + 'public/src/client/topic/events.js', + 'public/src/client/topic/fork.js', + 'public/src/client/topic/move.js', + 'public/src/client/topic/posts.js', + 'public/src/client/topic/images.js', + 'public/src/client/topic/postTools.js', + 'public/src/client/topic/threadTools.js', + 'public/src/client/categories.js', + 'public/src/client/category.js', + 'public/src/client/category/tools.js', + + 'public/src/modules/translator.js', + 'public/src/modules/notifications.js', + 'public/src/modules/chat.js', + 'public/src/modules/components.js', + 'public/src/modules/sort.js', + 'public/src/modules/navigator.js', + 'public/src/modules/topicSelect.js', + 'public/src/modules/share.js', + 'public/src/modules/search.js', + 'public/src/modules/alerts.js', + 'public/src/modules/taskbar.js', + 'public/src/modules/helpers.js', + 'public/src/modules/string.js', + 'public/src/modules/flags.js', + 'public/src/modules/storage.js', + ], + + // modules listed below are built (/src/modules) so they can be defined anonymously + modules: { + 'Chart.js': 'node_modules/chart.js/dist/Chart.min.js', + 'mousetrap.js': 'node_modules/mousetrap/mousetrap.min.js', + 'cropper.js': 'node_modules/cropperjs/dist/cropper.min.js', + 'jqueryui.js': 'public/vendor/jquery/js/jquery-ui.js', + 'zxcvbn.js': 'node_modules/zxcvbn/dist/zxcvbn.js', + ace: 'node_modules/ace-builds/src-min', }, }; - function minifyModules(modules, callback) { + function minifyModules(modules, fork, callback) { + // for it to never fork + // otherwise it spawns way too many processes + // maybe eventually we can pool modules + // and pass the pools to the minifer + // to reduce the total number of threads + fork = false; + async.eachLimit(modules, 500, function (mod, next) { var srcPath = mod.srcPath; var destPath = mod.destPath; - var minified; - async.parallel([ - function (cb) { + async.parallel({ + dirped: function (cb) { mkdirp(path.dirname(destPath), cb); }, - function (cb) { + minified: function (cb) { fs.readFile(srcPath, function (err, buffer) { if (err) { return cb(err); } if (srcPath.endsWith('.min.js') || path.dirname(srcPath).endsWith('min')) { - minified = { code: buffer.toString() }; - return cb(); - } - - try { - minified = uglifyjs.minify(buffer.toString(), { - fromString: true, - compress: false, - }); - } catch (e) { - return cb(e); + return cb(null, { code: buffer.toString() }); } - cb(); + minifier.js.minify(buffer.toString(), fork, cb); }); }, - ], function (err) { + }, function (err, results) { if (err) { return next(err); } + var minified = results.minified; fs.writeFile(destPath, minified.code, next); }); }, callback); @@ -233,7 +225,7 @@ module.exports = function (Meta) { }); } - Meta.js.buildModules = function (callback) { + Meta.js.buildModules = function (fork, callback) { async.waterfall([ clearModules, function (next) { @@ -244,7 +236,7 @@ module.exports = function (Meta) { getModuleList(next); }, function (modules, next) { - minifyModules(modules, next); + minifyModules(modules, fork, next); }, ], callback); }; @@ -269,52 +261,13 @@ module.exports = function (Meta) { }); }; - Meta.js.minify = function (target, callback) { - winston.verbose('[meta/js] Minifying ' + target); - - var forkProcessParams = setupDebugging(); - var minifier = fork(minifierPath, [], forkProcessParams); - Meta.js.minifierProc = minifier; - - Meta.js.target[target] = {}; - - Meta.js.prepare(target, function (err) { - if (err) { - return callback(err); - } - minifier.send({ - action: 'js', - minify: global.env !== 'development', - scripts: Meta.js.target[target].scripts, - }); - }); - - minifier.on('message', function (message) { - switch (message.type) { - case 'end': - Meta.js.target[target].cache = message.minified; - Meta.js.target[target].map = message.sourceMap; - winston.verbose('[meta/js] ' + target + ' minification complete'); - minifier.kill(); - - Meta.js.commitToFile(target, callback); - break; - case 'error': - winston.error('[meta/js] Could not compile ' + target + ': ' + message.message); - minifier.kill(); - - callback(new Error(message.message)); - break; - } - }); - }; - - Meta.js.prepare = function (target, callback) { - var pluginsScripts = []; - + function getBundleScriptList(target, callback) { var pluginDirectories = []; - pluginsScripts = plugins[target === 'nodebb.min.js' ? 'clientScripts' : 'acpScripts'].filter(function (path) { + if (target === 'admin') { + target = 'acp'; + } + var pluginScripts = plugins[target + 'Scripts'].filter(function (path) { if (path.endsWith('.js')) { return true; } @@ -325,8 +278,12 @@ module.exports = function (Meta) { async.each(pluginDirectories, function (directory, next) { file.walk(directory, function (err, scripts) { - pluginsScripts = pluginsScripts.concat(scripts); - next(err); + if (err) { + return next(err); + } + + pluginScripts = pluginScripts.concat(scripts); + next(); }); }, function (err) { if (err) { @@ -335,52 +292,43 @@ module.exports = function (Meta) { var basePath = path.resolve(__dirname, '../..'); - Meta.js.target[target].scripts = Meta.js.scripts.base.concat(pluginsScripts); + var scripts = Meta.js.scripts.base.concat(pluginScripts); - if (target === 'nodebb.min.js') { - Meta.js.target[target].scripts = Meta.js.target[target].scripts.concat(Meta.js.scripts.rjs); + if (target === 'client' && global.env !== 'development') { + scripts = scripts.concat(Meta.js.scripts.rjs); } - Meta.js.target[target].scripts = Meta.js.target[target].scripts.map(function (script) { + scripts = scripts.map(function (script) { return path.resolve(basePath, script).replace(/\\/g, '/'); }); - callback(); + callback(null, scripts); }); - }; + } - Meta.js.killMinifier = function () { - if (Meta.js.minifierProc) { - Meta.js.minifierProc.kill('SIGTERM'); - } - }; + Meta.js.buildBundle = function (target, fork, callback) { + var fileNames = { + client: 'nodebb.min.js', + admin: 'acp.min.js', + }; - Meta.js.commitToFile = function (target, callback) { - fs.writeFile(path.join(__dirname, '../../build/public', target), Meta.js.target[target].cache, function (err) { - callback(err); - }); - }; + async.waterfall([ + function (next) { + getBundleScriptList(target, next); + }, + function (files, next) { + var minify = global.env !== 'development'; - function setupDebugging() { - /** - * Check if the parent process is running with the debug option --debug (or --debug-brk) - */ - var forkProcessParams = {}; - if (global.v8debug || parseInt(process.execArgv.indexOf('--debug'), 10) !== -1) { - /** - * use the line below if you want to debug minifier.js script too (or even --debug-brk option, but - * you'll have to setup your debugger and connect to the forked process) - */ - // forkProcessParams = {execArgv: ['--debug=' + (global.process.debugPort + 1), '--nolazy']}; - - /** - * otherwise, just clean up --debug/--debug-brk options which are set up by default from the parent one - */ - forkProcessParams = { - execArgv: [], - }; - } + minifier.js.bundle(files, minify, fork, next); + }, + function (bundle, next) { + var filePath = path.join(__dirname, '../../build/public', fileNames[target]); + fs.writeFile(filePath, bundle.code, next); + }, + ], callback); + }; - return forkProcessParams; - } + Meta.js.killMinifier = function () { + minifier.killAll(); + }; }; diff --git a/src/meta/minifier.js b/src/meta/minifier.js index 43761c9d33..b1f2888b16 100644 --- a/src/meta/minifier.js +++ b/src/meta/minifier.js @@ -3,86 +3,255 @@ var uglifyjs = require('uglify-js'); var async = require('async'); var fs = require('fs'); +var childProcess = require('child_process'); +var os = require('os'); +var less = require('less'); +var postcss = require('postcss'); +var autoprefixer = require('autoprefixer'); +var clean = require('postcss-clean'); + var file = require('../file'); -var Minifier = { - js: {}, +var Minifier = module.exports; + +function setupDebugging() { + /** + * Check if the parent process is running with the debug option --debug (or --debug-brk) + */ + var forkProcessParams = {}; + if (global.v8debug || parseInt(process.execArgv.indexOf('--debug'), 10) !== -1) { + /** + * use the line below if you want to debug minifier.js script too (or even --debug-brk option, but + * you'll have to setup your debugger and connect to the forked process) + */ + // forkProcessParams = { execArgv: ['--debug=' + (global.process.debugPort + 1), '--nolazy'] }; + + /** + * otherwise, just clean up --debug/--debug-brk options which are set up by default from the parent one + */ + forkProcessParams = { + execArgv: [], + }; + } + + return forkProcessParams; +} + +var children = []; + +Minifier.killAll = function () { + children.forEach(function (child) { + child.kill('SIGTERM'); + }); + + children = []; }; -/* Javascript */ -Minifier.js.minify = function (scripts, minify, callback) { - scripts = scripts.filter(function (file) { - return file && file.endsWith('.js'); +function removeChild(proc) { + children = children.filter(function (child) { + return child !== proc; }); +} - async.filter(scripts, function (script, next) { - file.exists(script, function (err, exists) { - if (err) { - return next(err); - } +function forkAction(action, callback) { + var forkProcessParams = setupDebugging(); + var proc = childProcess.fork(__filename, [], Object.assign({}, forkProcessParams, { + cwd: __dirname, + env: { + minifier_child: true, + }, + })); - if (!exists) { - console.warn('[minifier] file not found, ' + script); - } - next(null, exists); - }); - }, function (err, scripts) { - if (err) { - return callback(err); + children.push(proc); + + proc.on('message', function (message) { + if (message.type === 'error') { + proc.kill(); + return callback(new Error(message.message)); } - if (minify) { - minifyScripts(scripts, callback); - } else { - concatenateScripts(scripts, callback); + if (message.type === 'end') { + proc.kill(); + callback(null, message.result); } }); -}; + proc.on('error', function (err) { + proc.kill(); + removeChild(proc); + callback(err); + }); -process.on('message', function (payload) { - switch (payload.action) { - case 'js': - Minifier.js.minify(payload.scripts, payload.minify, function (minified/* , sourceMap*/) { - process.send({ - type: 'end', - // sourceMap: sourceMap, - minified: minified, + proc.send({ + type: 'action', + action: action, + }); + + proc.on('close', function () { + removeChild(proc); + }); +} + +var actions = {}; + +if (process.env.minifier_child) { + process.on('message', function (message) { + if (message.type === 'action') { + var action = message.action; + if (typeof actions[action.act] !== 'function') { + process.send({ + type: 'error', + message: 'Unknown action', + }); + return; + } + + actions[action.act](action, function (err, result) { + if (err) { + process.send({ + type: 'error', + message: err.message, + }); + return; + } + + process.send({ + type: 'end', + result: result, + }); }); - }); - break; + } + }); +} + +function executeAction(action, fork, callback) { + if (fork) { + forkAction(action, callback); + } else { + if (typeof actions[action.act] !== 'function') { + return callback(Error('Unknown action')); + } + actions[action.act](action, callback); } -}); - -function minifyScripts(scripts, callback) { - // The portions of code involving the source map are commented out as they're broken in UglifyJS2 - // Follow along here: https://github.com/mishoo/UglifyJS2/issues/700 - try { - var minified = uglifyjs.minify(scripts, { - // outSourceMap: "nodebb.min.js.map", - compress: false, - }); +} + +function concat(data, callback) { + if (data.files && data.files.length) { + async.mapLimit(data.files, 1000, fs.readFile, function (err, files) { + if (err) { + return callback(err); + } - callback(minified.code/* , minified.map*/); - } catch (err) { - process.send({ - type: 'error', - message: err.message, + var output = files.join(os.EOL + ';'); + callback(null, { code: output }); }); + + return; } + + callback(); } +actions.concat = concat; -function concatenateScripts(scripts, callback) { - async.map(scripts, fs.readFile, function (err, scripts) { - if (err) { - process.send({ - type: 'error', - message: err.message, +function minifyJS(data, callback) { + var minified; + + if (data.fromSource) { + var sources = data.source; + var multiple = Array.isArray(sources); + if (!multiple) { + sources = [sources]; + } + + try { + minified = sources.map(function (source) { + return uglifyjs.minify(source, { + // outSourceMap: data.filename + '.map', + compress: data.compress, + fromString: true, + output: { + // suppress uglify line length warnings + max_line_len: 400000, + }, + }); }); - return; + } catch (e) { + return callback(e); } - scripts = scripts.join(require('os').EOL + ';'); + return callback(null, multiple ? minified : minified[0]); + } + + if (data.files && data.files.length) { + async.filter(data.files, file.exists, function (err, scripts) { + if (err) { + return callback(err); + } + + try { + minified = uglifyjs.minify(scripts, { + // outSourceMap: data.filename + '.map', + compress: data.compress, + fromString: false, + }); + } catch (e) { + return callback(e); + } + + callback(null, minified); + }); - callback(scripts); + return; + } + + callback(); +} +actions.minifyJS = minifyJS; + +Minifier.js = {}; +Minifier.js.bundle = function (scripts, minify, fork, callback) { + executeAction({ + act: minify ? 'minifyJS' : 'concat', + files: scripts, + compress: false, + }, fork, callback); +}; + +Minifier.js.minify = function (source, fork, callback) { + executeAction({ + act: 'minifyJS', + fromSource: true, + source: source, + }, fork, callback); +}; + +function buildCSS(data, callback) { + less.render(data.source, { + paths: data.paths, + }, function (err, lessOutput) { + if (err) { + return callback(err); + } + + postcss(data.minify ? [ + autoprefixer, + clean({ + processImportFrom: ['local'], + }), + ] : [autoprefixer]).process(lessOutput.css).then(function (result) { + callback(null, { code: result.css }); + }, function (err) { + callback(err); + }); }); } +actions.buildCSS = buildCSS; + +Minifier.css = {}; +Minifier.css.bundle = function (source, paths, minify, fork, callback) { + executeAction({ + act: 'buildCSS', + source: source, + paths: paths, + minify: minify, + }, fork, callback); +}; From b0645cc67db6dd50f2a27548613e195eb502374e Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Wed, 17 May 2017 17:25:41 -0600 Subject: [PATCH 091/263] Plugin load refactor --- src/meta/languages.js | 19 +-- src/plugins.js | 39 +----- src/plugins/data.js | 300 +++++++++++++++++++++++++++++++++++++++ src/plugins/load.js | 317 +++++++++--------------------------------- 4 files changed, 374 insertions(+), 301 deletions(-) create mode 100644 src/plugins/data.js diff --git a/src/meta/languages.js b/src/meta/languages.js index 90c3603677..28fea3c794 100644 --- a/src/meta/languages.js +++ b/src/meta/languages.js @@ -9,29 +9,14 @@ var rimraf = require('rimraf'); var file = require('../file'); var Plugins = require('../plugins'); -var db = require('../database'); var buildLanguagesPath = path.join(__dirname, '../../build/public/language'); -var coreLanguagesPath = path.join(__dirname, '../../public/language'); +var coreLanguagesPath = path.join(__dirname, '../../public/language'); function getTranslationTree(callback) { async.waterfall([ // get plugin data - function (next) { - db.getSortedSetRange('plugins:active', 0, -1, next); - }, - function (plugins, next) { - var pluginBasePath = path.join(__dirname, '../../node_modules'); - var paths = plugins.map(function (plugin) { - return path.join(pluginBasePath, plugin); - }); - - // Filter out plugins with invalid paths - async.filter(paths, file.exists, next); - }, - function (paths, next) { - async.map(paths, Plugins.loadPluginInfo, next); - }, + Plugins.data.getActive, // generate list of languages and namespaces function (plugins, next) { diff --git a/src/plugins.js b/src/plugins.js index 6c69553a05..d32a133648 100644 --- a/src/plugins.js +++ b/src/plugins.js @@ -8,10 +8,8 @@ var semver = require('semver'); var express = require('express'); var nconf = require('nconf'); -var db = require('./database'); var hotswap = require('./hotswap'); var file = require('./file'); -var languages = require('./languages'); var app; var middleware; @@ -20,6 +18,10 @@ var middleware; require('./plugins/install')(Plugins); require('./plugins/load')(Plugins); require('./plugins/hooks')(Plugins); + Plugins.data = require('./plugins/data'); + + Plugins.getPluginPaths = Plugins.data.getPluginPaths; + Plugins.loadPluginInfo = Plugins.data.loadPluginInfo; Plugins.libraries = {}; Plugins.loadedHooks = {}; @@ -30,7 +32,6 @@ var middleware; Plugins.acpScripts = []; Plugins.libraryPaths = []; Plugins.versionWarning = []; - Plugins.languageCodes = []; Plugins.soundpacks = []; Plugins.initialized = false; @@ -84,21 +85,7 @@ var middleware; Plugins.libraryPaths.length = 0; async.waterfall([ - function (next) { - // Build language code list - languages.list(function (err, languages) { - if (err) { - return next(err); - } - - Plugins.languageCodes = languages.map(function (data) { - return data.code; - }); - - next(); - }); - }, - async.apply(Plugins.getPluginPaths), + Plugins.getPluginPaths, function (paths, next) { async.eachSeries(paths, Plugins.loadPlugin, next); }, @@ -150,21 +137,7 @@ var middleware; var templates = {}; var tplName; - async.waterfall([ - async.apply(db.getSortedSetRange, 'plugins:active', 0, -1), - function (plugins, next) { - var pluginBasePath = path.join(__dirname, '../node_modules'); - var paths = plugins.map(function (plugin) { - return path.join(pluginBasePath, plugin); - }); - - // Filter out plugins with invalid paths - async.filter(paths, file.exists, next); - }, - function (paths, next) { - async.map(paths, Plugins.loadPluginInfo, next); - }, - ], function (err, plugins) { + Plugins.data.getActive(function (err, plugins) { if (err) { return callback(err); } diff --git a/src/plugins/data.js b/src/plugins/data.js new file mode 100644 index 0000000000..d2f1278c9d --- /dev/null +++ b/src/plugins/data.js @@ -0,0 +1,300 @@ +'use strict'; + +var fs = require('fs'); +var path = require('path'); +var async = require('async'); +var winston = require('winston'); + +var db = require('../database'); +var file = require('../file'); + +var Data = module.exports; + +var basePath = path.join(__dirname, '../../'); + +function getPluginPaths(callback) { + async.waterfall([ + function (next) { + db.getSortedSetRange('plugins:active', 0, -1, next); + }, + function (plugins, next) { + if (!Array.isArray(plugins)) { + return next(); + } + + plugins = plugins.filter(function (plugin) { + return plugin && typeof plugin === 'string'; + }).map(function (plugin) { + return path.join(__dirname, '../../node_modules/', plugin); + }); + + async.filter(plugins, file.exists, next); + }, + ], callback); +} +Data.getPluginPaths = getPluginPaths; + +function loadPluginInfo(pluginPath, callback) { + async.parallel({ + package: function (next) { + fs.readFile(path.join(pluginPath, 'package.json'), next); + }, + plugin: function (next) { + fs.readFile(path.join(pluginPath, 'plugin.json'), next); + }, + }, function (err, results) { + if (err) { + return callback(err); + } + var pluginData; + var packageData; + try { + pluginData = JSON.parse(results.plugin); + packageData = JSON.parse(results.package); + + pluginData.id = packageData.name; + pluginData.name = packageData.name; + pluginData.description = packageData.description; + pluginData.version = packageData.version; + pluginData.repository = packageData.repository; + pluginData.nbbpm = packageData.nbbpm; + pluginData.path = pluginPath; + } catch (err) { + var pluginDir = path.basename(pluginPath); + + winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json! ' + err.message); + return callback(new Error('[[error:parse-error]]')); + } + + callback(null, pluginData); + }); +} +Data.loadPluginInfo = loadPluginInfo; + +function getAllPluginData(callback) { + async.waterfall([ + function (next) { + getPluginPaths(next); + }, + function (pluginPaths, next) { + async.map(pluginPaths, loadPluginInfo, next); + }, + ], callback); +} +Data.getActive = getAllPluginData; + +function getStaticDirectories(pluginData, callback) { + var validMappedPath = /^[\w\-_]+$/; + + if (!pluginData.staticDirs) { + return callback(); + } + + var dirs = Object.keys(pluginData.staticDirs); + if (!dirs.length) { + return callback(); + } + + var staticDirs = {}; + + async.each(dirs, function (route, next) { + if (!validMappedPath.test(route)) { + winston.warn('[plugins/' + pluginData.id + '] Invalid mapped path specified: ' + + route + '. Path must adhere to: ' + validMappedPath.toString()); + return next(); + } + + var dirPath = path.join(pluginData.path, pluginData.staticDirs[route]); + fs.stat(dirPath, function (err, stats) { + if (err && err.code === 'ENOENT') { + winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + + route + ' => ' + dirPath + '\' not found.'); + return next(); + } + if (err) { + return next(err); + } + + if (!stats.isDirectory()) { + winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + + route + ' => ' + dirPath + '\' is not a directory.'); + return next(); + } + + staticDirs[pluginData.id + '/' + route] = dirPath; + next(); + }); + }, function (err) { + callback(err, staticDirs); + }); +} +Data.getStaticDirectories = getStaticDirectories; + +function getFiles(pluginData, type, callback) { + if (!Array.isArray(pluginData[type]) || !pluginData[type].length) { + return callback(); + } + + if (global.env === 'development') { + winston.verbose('[plugins] Found ' + pluginData[type].length + ' ' + type + ' file(s) for plugin ' + pluginData.id); + } + + var files = pluginData[type].map(function (file) { + return path.join(pluginData.id, file); + }); + + callback(null, files); +} +Data.getFiles = getFiles; + +/** + * With npm@3, dependencies can become flattened, and appear at the root level. + * This method resolves these differences if it can. + */ +function resolveModulePath(basePath, modulePath, callback) { + var isNodeModule = /node_modules/; + + var currentPath = path.join(basePath, modulePath); + file.exists(currentPath, function (err, exists) { + if (err) { + return callback(err); + } + if (exists) { + return callback(null, currentPath); + } + if (!isNodeModule.test(modulePath)) { + winston.warn('[plugins] File not found: ' + currentPath + ' (Ignoring)'); + return callback(); + } + + var dirPath = path.dirname(basePath); + if (dirPath === basePath) { + winston.warn('[plugins] File not found: ' + currentPath + ' (Ignoring)'); + return callback(); + } + + resolveModulePath(dirPath, modulePath, callback); + }); +} + +function getScripts(pluginData, target, callback) { + target = (target === 'client') ? 'scripts' : 'acpScripts'; + + var input = pluginData[target]; + if (!Array.isArray(input) || !input.length) { + return callback(); + } + + var scripts = []; + async.each(input, function (filePath, next) { + resolveModulePath(pluginData.path, filePath, function (err, modulePath) { + if (err) { + return next(err); + } + + if (modulePath) { + scripts.push(modulePath); + } + next(); + }); + }, function (err) { + if (err) { + return callback(err); + } + + if (scripts.length && global.env === 'development') { + winston.verbose('[plugins] Found ' + scripts.length + ' js file(s) for plugin ' + pluginData.id); + } + callback(err, scripts); + }); +} +Data.getScripts = getScripts; + +function getModules(pluginData, callback) { + if (!pluginData.modules || !pluginData.hasOwnProperty('modules')) { + return callback(); + } + + var pluginModules = pluginData.modules; + + if (Array.isArray(pluginModules)) { + var strip = parseInt(pluginData.modulesStrip, 10) || 0; + + pluginModules = pluginModules.reduce(function (prev, modulePath) { + var key; + if (strip) { + key = modulePath.replace(new RegExp('.?(/[^/]+){' + strip + '}/'), ''); + } else { + key = path.basename(modulePath); + } + + prev[key] = modulePath; + return prev; + }, {}); + } + + var modules = {}; + async.each(Object.keys(pluginModules), function (key, next) { + resolveModulePath(pluginData.path, pluginModules[key], function (err, modulePath) { + if (err) { + return next(err); + } + + if (modulePath) { + modules[key] = path.relative(basePath, modulePath); + } + next(); + }); + }, function (err) { + if (err) { + return callback(err); + } + + if (global.env === 'development') { + var len = Object.keys(modules).length; + winston.verbose('[plugins] Found ' + len + ' AMD-style module(s) for plugin ' + pluginData.id); + } + callback(null, modules); + }); +} +Data.getModules = getModules; + +function getSoundpack(pluginData, callback) { + var spack = pluginData.soundpack; + if (!spack || !spack.dir || !spack.sounds) { + return callback(); + } + + var soundpack = {}; + soundpack.name = spack.name || pluginData.name; + soundpack.id = pluginData.id; + soundpack.dir = path.join(pluginData.path, spack.dir); + soundpack.sounds = {}; + + async.each(Object.keys(spack.sounds), function (name, next) { + var soundFile = spack.sounds[name]; + file.exists(path.join(soundpack.dir, soundFile), function (err, exists) { + if (err) { + return next(err); + } + if (!exists) { + winston.warn('[plugins] Sound file not found: ' + soundFile); + return next(); + } + + soundpack.sounds[name] = soundFile; + next(); + }); + }, function (err) { + if (err) { + return callback(err); + } + + if (global.env === 'development') { + var len = Object.keys(soundpack).length; + winston.verbose('[plugins] Found ' + len + ' sound file(s) for plugin ' + pluginData.id); + } + callback(null, soundpack); + }); +} +Data.getSoundpack = getSoundpack; diff --git a/src/plugins/load.js b/src/plugins/load.js index 04b65e9163..b939769588 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -1,38 +1,71 @@ 'use strict'; -var db = require('../database'); -var fs = require('fs'); var path = require('path'); var semver = require('semver'); var async = require('async'); var winston = require('winston'); var nconf = require('nconf'); -var _ = require('underscore'); -var file = require('../file'); -var meta = require('../meta'); +var meta = require('../meta'); module.exports = function (Plugins) { - Plugins.getPluginPaths = function (callback) { - async.waterfall([ - function (next) { - db.getSortedSetRange('plugins:active', 0, -1, next); + function registerPluginAssets(pluginData, fields, callback) { + function add(dest, arr) { + dest.push.apply(dest, arr || []); + } + + var handlers = { + staticDirs: function (next) { + Plugins.data.getStaticDirectories(pluginData, next); }, - function (plugins, next) { - if (!Array.isArray(plugins)) { - return next(); - } + cssFiles: function (next) { + Plugins.data.getFiles(pluginData, 'css', next); + }, + lessFiles: function (next) { + Plugins.data.getFiles(pluginData, 'less', next); + }, + clientScripts: function (next) { + Plugins.data.getScripts(pluginData, 'client', next); + }, + acpScripts: function (next) { + Plugins.data.getScripts(pluginData, 'acp', next); + }, + modules: function (next) { + Plugins.data.getModules(pluginData, next); + }, + soundpack: function (next) { + Plugins.data.getSoundpack(pluginData, next); + }, + }; + + var methods; + if (Array.isArray(fields)) { + methods = fields.reduce(function (prev, field) { + prev[field] = handlers[field]; + return prev; + }, {}); + } else { + methods = handlers; + } - plugins = plugins.filter(function (plugin) { - return plugin && typeof plugin === 'string'; - }).map(function (plugin) { - return path.join(__dirname, '../../node_modules/', plugin); - }); + async.parallel(methods, function (err, results) { + if (err) { + return callback(err); + } - async.filter(plugins, file.exists, next); - }, - ], callback); - }; + Object.assign(Plugins.staticDirs, results.staticDirs || {}); + add(Plugins.cssFiles, results.cssFiles); + add(Plugins.lessFiles, results.lessFiles); + add(Plugins.clientScripts, results.clientScripts); + add(Plugins.acpScripts, results.acpScripts); + Object.assign(meta.js.scripts.modules, results.modules || {}); + if (results.soundpack) { + Plugins.soundpacks.push(results.soundpack); + } + + callback(); + }); + } Plugins.prepareForBuild = function (callback) { Plugins.cssFiles.length = 0; @@ -42,29 +75,18 @@ module.exports = function (Plugins) { Plugins.soundpacks.length = 0; async.waterfall([ - async.apply(Plugins.getPluginPaths), - function (paths, next) { - async.map(paths, function (path, next) { - Plugins.loadPluginInfo(path, next); - }, next); - }, + Plugins.data.getActive, function (plugins, next) { async.each(plugins, function (pluginData, next) { - async.parallel([ - async.apply(mapFiles, pluginData, 'css', 'cssFiles'), - async.apply(mapFiles, pluginData, 'less', 'lessFiles'), - async.apply(mapClientSideScripts, pluginData), - async.apply(mapClientModules, pluginData), - async.apply(mapStaticDirectories, pluginData, pluginData.path), - async.apply(mapSoundpack, pluginData), - ], next); + // TODO: only load the data that's needed for the build + registerPluginAssets(pluginData, true, next); }, next); }, ], callback); }; Plugins.loadPlugin = function (pluginPath, callback) { - Plugins.loadPluginInfo(pluginPath, function (err, pluginData) { + Plugins.data.loadPluginInfo(pluginPath, function (err, pluginData) { if (err) { if (err.message === '[[error:parse-error]]') { return callback(); @@ -76,25 +98,13 @@ module.exports = function (Plugins) { async.parallel([ function (next) { - registerHooks(pluginData, pluginPath, next); - }, - function (next) { - mapStaticDirectories(pluginData, pluginPath, next); - }, - function (next) { - mapFiles(pluginData, 'css', 'cssFiles', next); - }, - function (next) { - mapFiles(pluginData, 'less', 'lessFiles', next); - }, - function (next) { - mapClientSideScripts(pluginData, next); + registerHooks(pluginData, next); }, function (next) { - mapClientModules(pluginData, next); - }, - function (next) { - mapSoundpack(pluginData, next); + // TODO: change this from `true` to `['soundpack']` + // this will skip several build-only plugin loading methods + // and only load soundpacks, which will speed up startup + registerPluginAssets(pluginData, true, next); }, ], function (err) { if (err) { @@ -124,12 +134,12 @@ module.exports = function (Plugins) { } } - function registerHooks(pluginData, pluginPath, callback) { + function registerHooks(pluginData, callback) { if (!pluginData.library) { return callback(); } - var libraryPath = path.join(pluginPath, pluginData.library); + var libraryPath = path.join(pluginData.path, pluginData.library); try { if (!Plugins.libraries[pluginData.id]) { @@ -149,199 +159,4 @@ module.exports = function (Plugins) { callback(); } } - - function mapStaticDirectories(pluginData, pluginPath, callback) { - var validMappedPath = /^[\w\-_]+$/; - - function mapStaticDirs(mappedPath, callback) { - if (Plugins.staticDirs[mappedPath]) { - winston.warn('[plugins/' + pluginData.id + '] Mapped path (' + mappedPath + ') already specified!'); - callback(); - } else if (!validMappedPath.test(mappedPath)) { - winston.warn('[plugins/' + pluginData.id + '] Invalid mapped path specified: ' + mappedPath + '. Path must adhere to: ' + validMappedPath.toString()); - callback(); - } else { - var realPath = pluginData.staticDirs[mappedPath]; - var staticDir = path.join(pluginPath, realPath); - - file.exists(staticDir, function (err, exists) { - if (exists) { - Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir; - } else { - winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.'); - } - callback(err); - }); - } - } - - pluginData.staticDirs = pluginData.staticDirs || {}; - - var dirs = Object.keys(pluginData.staticDirs); - async.each(dirs, mapStaticDirs, callback); - } - - function mapFiles(pluginData, type, globalArray, callback) { - if (Array.isArray(pluginData[type])) { - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + pluginData[type].length + ' ' + type + ' file(s) for plugin ' + pluginData.id); - } - - Plugins[globalArray] = Plugins[globalArray].concat(pluginData[type].map(function (file) { - return path.join(pluginData.id, file); - })); - } - callback(); - } - - function mapClientSideScripts(pluginData, callback) { - function mapScripts(scripts, param) { - if (Array.isArray(scripts) && scripts.length) { - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + scripts.length + ' js file(s) for plugin ' + pluginData.id); - } - - Plugins[param] = Plugins[param].concat(scripts.map(function (file) { - return resolveModulePath(path.join(__dirname, '../../node_modules/', pluginData.id, file), file); - })).filter(Boolean); - } - } - mapScripts(pluginData.scripts, 'clientScripts'); - mapScripts(pluginData.acpScripts, 'acpScripts'); - - callback(); - } - - function mapClientModules(pluginData, callback) { - if (!pluginData.hasOwnProperty('modules')) { - return callback(); - } - - var modules = {}; - - if (Array.isArray(pluginData.modules)) { - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + pluginData.modules.length + ' AMD-style module(s) for plugin ' + pluginData.id); - } - - var strip = pluginData.hasOwnProperty('modulesStrip') ? parseInt(pluginData.modulesStrip, 10) : 0; - - pluginData.modules.forEach(function (file) { - if (strip) { - modules[file.replace(new RegExp('.?(/[^/]+){' + strip + '}/'), '')] = path.join('./node_modules/', pluginData.id, file); - } else { - modules[path.basename(file)] = path.join('./node_modules/', pluginData.id, file); - } - }); - - meta.js.scripts.modules = _.extend(meta.js.scripts.modules, modules); - } else { - var keys = Object.keys(pluginData.modules); - - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + keys.length + ' AMD-style module(s) for plugin ' + pluginData.id); - } - - for (var name in pluginData.modules) { - if (pluginData.modules.hasOwnProperty(name)) { - modules[name] = path.join('./node_modules/', pluginData.id, pluginData.modules[name]); - } - } - - meta.js.scripts.modules = _.extend(meta.js.scripts.modules, modules); - } - - callback(); - } - - function mapSoundpack(pluginData, callback) { - var soundpack = pluginData.soundpack; - if (!soundpack || !soundpack.dir || !soundpack.sounds) { - return callback(); - } - soundpack.name = soundpack.name || pluginData.name; - soundpack.id = pluginData.id; - soundpack.dir = path.join(pluginData.path, soundpack.dir); - async.each(Object.keys(soundpack.sounds), function (key, next) { - file.exists(path.join(soundpack.dir, soundpack.sounds[key]), function (err, exists) { - if (!exists) { - delete soundpack.sounds[key]; - } - - next(err); - }); - }, function (err) { - if (err) { - return callback(err); - } - - if (Object.keys(soundpack.sounds).length) { - Plugins.soundpacks.push(soundpack); - } - - callback(); - }); - } - - function resolveModulePath(fullPath, relPath) { - /** - * With npm@3, dependencies can become flattened, and appear at the root level. - * This method resolves these differences if it can. - */ - var matches = fullPath.match(/node_modules/g); - var atRootLevel = !matches || matches.length === 1; - - try { - fs.statSync(fullPath); - winston.verbose('[plugins/load] File found: ' + fullPath); - return fullPath; - } catch (e) { - // File not visible to the calling process, ascend to root level if possible and try again - if (!atRootLevel && relPath) { - winston.verbose('[plugins/load] File not found: ' + fullPath + ' (Ascending)'); - return resolveModulePath(path.join(__dirname, '../..', relPath)); - } - // Already at root level, file was simply not found - winston.warn('[plugins/load] File not found: ' + fullPath + ' (Ignoring)'); - return null; - } - } - - Plugins.loadPluginInfo = function (pluginPath, callback) { - async.parallel({ - package: function (next) { - fs.readFile(path.join(pluginPath, 'package.json'), next); - }, - plugin: function (next) { - fs.readFile(path.join(pluginPath, 'plugin.json'), next); - }, - }, function (err, results) { - if (err) { - return callback(err); - } - var pluginData; - var packageData; - try { - pluginData = JSON.parse(results.plugin); - packageData = JSON.parse(results.package); - - pluginData.id = packageData.name; - pluginData.name = packageData.name; - pluginData.description = packageData.description; - pluginData.version = packageData.version; - pluginData.repository = packageData.repository; - pluginData.nbbpm = packageData.nbbpm; - pluginData.path = pluginPath; - } catch (err) { - var pluginDir = pluginPath.split(path.sep); - pluginDir = pluginDir[pluginDir.length - 1]; - - winston.error('[plugins/' + pluginDir + '] Error in plugin.json or package.json! ' + err.message); - - return callback(new Error('[[error:parse-error]]')); - } - - callback(null, pluginData); - }); - }; }; From 90da6d889d92c9e2e1400a24d5d09f5af0b55d61 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 18 May 2017 01:32:20 -0600 Subject: [PATCH 092/263] Only load necessary plugin data Fix tests to work in this case Add more verbose messages to plugins/data --- src/meta/build.js | 12 ++++++-- src/plugins/data.js | 27 ++++++++-------- src/plugins/load.js | 31 ++++++++++++++----- test/build.js | 8 +++++ test/mocks/databasemock.js | 63 ++++++++++++++++++++++---------------- test/plugins.js | 1 - 6 files changed, 92 insertions(+), 50 deletions(-) diff --git a/src/meta/build.js b/src/meta/build.js index e88bbb17e9..dfeb4d7825 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -93,7 +93,7 @@ aliases = Object.keys(aliases).reduce(function (prev, key) { return prev; }, {}); -function beforeBuild(callback) { +function beforeBuild(targets, callback) { var db = require('../database'); var plugins = require('../plugins'); meta = require('../meta'); @@ -101,7 +101,9 @@ function beforeBuild(callback) { async.series([ db.init, meta.themes.setupPaths, - plugins.prepareForBuild, + function (next) { + plugins.prepareForBuild(targets, next); + }, ], function (err) { if (err) { winston.error('[build] Encountered error preparing for build: ' + err.message); @@ -160,6 +162,8 @@ function build(targets, callback) { return arr.indexOf(target) === i; }); + winston.verbose('[build] building the following targets: ' + targets.join(', ')); + if (typeof callback !== 'function') { callback = function (err) { if (err) { @@ -179,7 +183,9 @@ function build(targets, callback) { var startTime; var totalTime; async.series([ - beforeBuild, + function (next) { + beforeBuild(targets, next); + }, function (next) { var parallel = os.cpus().length > 1 && !nconf.get('series'); if (parallel) { diff --git a/src/plugins/data.js b/src/plugins/data.js index d2f1278c9d..965ad07bfc 100644 --- a/src/plugins/data.js +++ b/src/plugins/data.js @@ -125,7 +125,12 @@ function getStaticDirectories(pluginData, callback) { next(); }); }, function (err) { - callback(err, staticDirs); + if (err) { + return callback(err); + } + winston.verbose('[plugins] found ' + Object.keys(staticDirs).length + + ' static directories for ' + pluginData.id); + callback(null, staticDirs); }); } Data.getStaticDirectories = getStaticDirectories; @@ -135,9 +140,7 @@ function getFiles(pluginData, type, callback) { return callback(); } - if (global.env === 'development') { - winston.verbose('[plugins] Found ' + pluginData[type].length + ' ' + type + ' file(s) for plugin ' + pluginData.id); - } + winston.verbose('[plugins] Found ' + pluginData[type].length + ' ' + type + ' file(s) for plugin ' + pluginData.id); var files = pluginData[type].map(function (file) { return path.join(pluginData.id, file); @@ -202,7 +205,7 @@ function getScripts(pluginData, target, callback) { return callback(err); } - if (scripts.length && global.env === 'development') { + if (scripts.length) { winston.verbose('[plugins] Found ' + scripts.length + ' js file(s) for plugin ' + pluginData.id); } callback(err, scripts); @@ -250,10 +253,9 @@ function getModules(pluginData, callback) { return callback(err); } - if (global.env === 'development') { - var len = Object.keys(modules).length; - winston.verbose('[plugins] Found ' + len + ' AMD-style module(s) for plugin ' + pluginData.id); - } + var len = Object.keys(modules).length; + winston.verbose('[plugins] Found ' + len + ' AMD-style module(s) for plugin ' + pluginData.id); + callback(null, modules); }); } @@ -290,10 +292,9 @@ function getSoundpack(pluginData, callback) { return callback(err); } - if (global.env === 'development') { - var len = Object.keys(soundpack).length; - winston.verbose('[plugins] Found ' + len + ' sound file(s) for plugin ' + pluginData.id); - } + var len = Object.keys(soundpack.sounds).length; + winston.verbose('[plugins] Found ' + len + ' sound file(s) for plugin ' + pluginData.id); + callback(null, soundpack); }); } diff --git a/src/plugins/load.js b/src/plugins/load.js index b939769588..b0575670cf 100644 --- a/src/plugins/load.js +++ b/src/plugins/load.js @@ -67,19 +67,39 @@ module.exports = function (Plugins) { }); } - Plugins.prepareForBuild = function (callback) { + Plugins.prepareForBuild = function (targets, callback) { Plugins.cssFiles.length = 0; Plugins.lessFiles.length = 0; Plugins.clientScripts.length = 0; Plugins.acpScripts.length = 0; Plugins.soundpacks.length = 0; + var map = { + 'plugin static dirs': ['staticDirs'], + 'requirejs modules': ['modules'], + 'client js bundle': ['clientScripts'], + 'admin js bundle': ['acpScripts'], + 'client side styles': ['cssFiles', 'lessFiles'], + 'admin control panel styles': ['cssFiles', 'lessFiles'], + sounds: ['soundpack'], + }; + + var fields = targets.reduce(function (prev, target) { + if (!map[target]) { + return prev; + } + return prev.concat(map[target]); + }, []).filter(function (field, i, arr) { + return arr.indexOf(field) === i; + }); + + winston.verbose('[plugins] loading the following fields from plugin data: ' + fields.join(', ')); + async.waterfall([ Plugins.data.getActive, function (plugins, next) { async.each(plugins, function (pluginData, next) { - // TODO: only load the data that's needed for the build - registerPluginAssets(pluginData, true, next); + registerPluginAssets(pluginData, fields, next); }, next); }, ], callback); @@ -101,10 +121,7 @@ module.exports = function (Plugins) { registerHooks(pluginData, next); }, function (next) { - // TODO: change this from `true` to `['soundpack']` - // this will skip several build-only plugin loading methods - // and only load soundpacks, which will speed up startup - registerPluginAssets(pluginData, true, next); + registerPluginAssets(pluginData, ['soundpack'], next); }, ], function (err) { if (err) { diff --git a/test/build.js b/test/build.js index 3b6890ebe6..1f73f5746b 100644 --- a/test/build.js +++ b/test/build.js @@ -5,6 +5,10 @@ var assert = require('assert'); var db = require('./mocks/databasemock'); describe('Build', function () { + before(function (done) { + db.setupMockDefaults(done); + }); + it('should build all assets', function (done) { this.timeout(50000); var build = require('../src/meta/build'); @@ -13,4 +17,8 @@ describe('Build', function () { done(); }); }); + + after(function (done) { + db.emptydb(done); + }); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 4432f944f1..ca280dda9e 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -83,42 +83,19 @@ before(function (done) { this.timeout(30000); - var meta; - async.waterfall([ + async.series([ function (next) { db.init(next); }, function (next) { - db.emptydb(next); - }, - function (next) { - winston.info('test_database flushed'); - meta = require('../../src/meta'); - setupDefaultConfigs(meta, next); - }, - function (next) { - meta.configs.init(next); + setupMockDefaults(next); }, function (next) { db.initSessionStore(next); }, function (next) { - meta.dependencies.check(next); - }, - function (next) { - meta.config.postDelay = 0; - meta.config.initialPostDelay = 0; - meta.config.newbiePostDelay = 0; + var meta = require('../../src/meta'); - enableDefaultPlugins(next); - }, - function (next) { - meta.themes.set({ - type: 'local', - id: 'nodebb-theme-persona', - }, next); - }, - function (next) { // nconf defaults, if not set in config if (!nconf.get('sessionKey')) { nconf.set('sessionKey', 'express.sid'); @@ -154,6 +131,40 @@ ], done); }); + function setupMockDefaults(callback) { + var meta = require('../../src/meta'); + + async.series([ + function (next) { + db.emptydb(next); + }, + function (next) { + winston.info('test_database flushed'); + setupDefaultConfigs(meta, next); + }, + function (next) { + meta.configs.init(next); + }, + function (next) { + meta.dependencies.check(next); + }, + function (next) { + meta.config.postDelay = 0; + meta.config.initialPostDelay = 0; + meta.config.newbiePostDelay = 0; + + enableDefaultPlugins(next); + }, + function (next) { + meta.themes.set({ + type: 'local', + id: 'nodebb-theme-persona', + }, next); + }, + ], callback); + } + db.setupMockDefaults = setupMockDefaults; + function setupDefaultConfigs(meta, next) { winston.info('Populating database with default configs, if not already set...\n'); diff --git a/test/plugins.js b/test/plugins.js index 97a9c1908b..caccbe2851 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -16,7 +16,6 @@ describe('Plugins', function () { assert.ifError(err); assert(plugins.libraries[pluginId]); assert(plugins.loadedHooks['static:app.load']); - assert(plugins.staticDirs['nodebb-plugin-markdown/js']); done(); }); From 2afce9f7c3d25abf6230e6a325d0d5a055661b69 Mon Sep 17 00:00:00 2001 From: Ole R Date: Thu, 18 May 2017 11:27:41 +0200 Subject: [PATCH 093/263] Update dependency nodebb-plugin-emoji-one@1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dff73ec5d5..e915c853f9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "nodebb-plugin-composer-default": "4.4.8", "nodebb-plugin-dbsearch": "2.0.2", "nodebb-plugin-emoji-extended": "1.1.1", - "nodebb-plugin-emoji-one": "1.1.5", + "nodebb-plugin-emoji-one": "1.2.0", "nodebb-plugin-markdown": "7.1.1", "nodebb-plugin-mentions": "2.0.3", "nodebb-plugin-soundpack-default": "1.0.0", From 91e5266776d0de75b960dd762611662b15fc383a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 18 May 2017 13:33:04 -0400 Subject: [PATCH 094/263] up emoji one --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dff73ec5d5..e915c853f9 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "nodebb-plugin-composer-default": "4.4.8", "nodebb-plugin-dbsearch": "2.0.2", "nodebb-plugin-emoji-extended": "1.1.1", - "nodebb-plugin-emoji-one": "1.1.5", + "nodebb-plugin-emoji-one": "1.2.0", "nodebb-plugin-markdown": "7.1.1", "nodebb-plugin-mentions": "2.0.3", "nodebb-plugin-soundpack-default": "1.0.0", From 4ab514a42267d283e49aeaa55cdd77bb05f2dea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Thu, 18 May 2017 16:59:32 -0400 Subject: [PATCH 095/263] test for .delete with zset --- test/database/keys.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/database/keys.js b/test/database/keys.js index 157cc2ca97..afbf3c947c 100644 --- a/test/database/keys.js +++ b/test/database/keys.js @@ -102,6 +102,37 @@ describe('Key methods', function () { }); }); + it('should delete all sorted set elements', function (done) { + async.parallel([ + function (next) { + db.sortedSetAdd('deletezset', 1, 'value1', next); + }, + function (next) { + db.sortedSetAdd('deletezset', 2, 'value2', next); + }, + ], function (err) { + if (err) { + return done(err); + } + db.delete('deletezset', function (err) { + assert.ifError(err); + async.parallel({ + key1exists: function (next) { + db.isSortedSetMember('deletezset', 'value1', next); + }, + key2exists: function (next) { + db.isSortedSetMember('deletezset', 'value2', next); + }, + }, function (err, results) { + assert.equal(err, null); + assert.equal(results.key1exists, false); + assert.equal(results.key2exists, false); + done(); + }); + }); + }); + }); + describe('increment', function () { it('should initialize key to 1', function (done) { db.increment('keyToIncrement', function (err, value) { From e8caee3c4c46125b628563181145c9c1d25c07cb Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 18 May 2017 17:50:49 -0600 Subject: [PATCH 096/263] Restrict total threads So machines with a small amount of cores build faster --- src/meta/build.js | 3 +-- src/meta/minifier.js | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/meta/build.js b/src/meta/build.js index e88bbb17e9..cc3321923f 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -2,7 +2,6 @@ var async = require('async'); var winston = require('winston'); -var os = require('os'); var nconf = require('nconf'); var padstart = require('lodash.padstart'); @@ -181,7 +180,7 @@ function build(targets, callback) { async.series([ beforeBuild, function (next) { - var parallel = os.cpus().length > 1 && !nconf.get('series'); + var parallel = !nconf.get('series'); if (parallel) { winston.info('[build] Building in parallel mode'); } else { diff --git a/src/meta/minifier.js b/src/meta/minifier.js index b1f2888b16..53ca9f5f28 100644 --- a/src/meta/minifier.js +++ b/src/meta/minifier.js @@ -5,6 +5,7 @@ var async = require('async'); var fs = require('fs'); var childProcess = require('child_process'); var os = require('os'); +var winston = require('winston'); var less = require('less'); var postcss = require('postcss'); var autoprefixer = require('autoprefixer'); @@ -39,6 +40,10 @@ function setupDebugging() { var children = []; +Minifier.maxThreads = os.cpus().length - 1; + +winston.verbose('[minifier] utilizing a maximum of ' + Minifier.maxThreads + ' additional threads'); + Minifier.killAll = function () { children.forEach(function (child) { child.kill('SIGTERM'); @@ -65,13 +70,14 @@ function forkAction(action, callback) { children.push(proc); proc.on('message', function (message) { + proc.kill(); + removeChild(proc); + if (message.type === 'error') { - proc.kill(); - return callback(new Error(message.message)); + return callback(message.err); } if (message.type === 'end') { - proc.kill(); callback(null, message.result); } }); @@ -85,10 +91,6 @@ function forkAction(action, callback) { type: 'action', action: action, }); - - proc.on('close', function () { - removeChild(proc); - }); } var actions = {}; @@ -109,7 +111,7 @@ if (process.env.minifier_child) { if (err) { process.send({ type: 'error', - message: err.message, + err: err, }); return; } @@ -124,7 +126,7 @@ if (process.env.minifier_child) { } function executeAction(action, fork, callback) { - if (fork) { + if (fork && children.length < Minifier.maxThreads) { forkAction(action, callback); } else { if (typeof actions[action.act] !== 'function') { From 9f5ce24993ff6690191c1a2c015d5c0e024498a7 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Thu, 18 May 2017 21:20:04 -0600 Subject: [PATCH 097/263] Minify modules in a batch --- src/meta/js.js | 76 ++++++++++++++++++------------- src/meta/minifier.js | 105 +++++++++++++++++++++++++------------------ 2 files changed, 106 insertions(+), 75 deletions(-) diff --git a/src/meta/js.js b/src/meta/js.js index e7b22939fc..bfae9260c2 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -88,44 +88,56 @@ module.exports = function (Meta) { }, }; - function minifyModules(modules, fork, callback) { - // for it to never fork - // otherwise it spawns way too many processes - // maybe eventually we can pool modules - // and pass the pools to the minifer - // to reduce the total number of threads - fork = false; + function copyFile(source, target, cb) { + var called = false; - async.eachLimit(modules, 500, function (mod, next) { - var srcPath = mod.srcPath; - var destPath = mod.destPath; + var rd = fs.createReadStream(source); + rd.on('error', done); - async.parallel({ - dirped: function (cb) { - mkdirp(path.dirname(destPath), cb); - }, - minified: function (cb) { - fs.readFile(srcPath, function (err, buffer) { - if (err) { - return cb(err); - } + var wr = fs.createWriteStream(target); + wr.on('error', done); + wr.on('close', function () { + done(); + }); + rd.pipe(wr); - if (srcPath.endsWith('.min.js') || path.dirname(srcPath).endsWith('min')) { - return cb(null, { code: buffer.toString() }); - } + function done(err) { + if (!called) { + cb(err); + called = true; + } + } + } - minifier.js.minify(buffer.toString(), fork, cb); - }); - }, - }, function (err, results) { - if (err) { - return next(err); + function minifyModules(modules, fork, callback) { + async.eachLimit(modules, 1000, function (mod, next) { + mkdirp(path.dirname(mod.destPath), next); + }, function (err) { + if (err) { + return callback(err); + } + + var filtered = modules.reduce(function (prev, mod) { + if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) { + prev.skip.push(mod); + } else { + prev.minify.push(mod); } - var minified = results.minified; - fs.writeFile(destPath, minified.code, next); - }); - }, callback); + return prev; + }, { minify: [], skip: [] }); + + async.parallel([ + function (cb) { + minifier.js.minifyBatch(filtered.minify, fork, cb); + }, + function (cb) { + async.eachLimit(filtered.skip, 500, function (mod, next) { + copyFile(mod.srcPath, mod.destPath, next); + }, cb); + }, + ], callback); + }); } function linkModules(callback) { diff --git a/src/meta/minifier.js b/src/meta/minifier.js index 53ca9f5f28..da7c570ce7 100644 --- a/src/meta/minifier.js +++ b/src/meta/minifier.js @@ -38,27 +38,26 @@ function setupDebugging() { return forkProcessParams; } -var children = []; +var pool = []; +var free = []; Minifier.maxThreads = os.cpus().length - 1; winston.verbose('[minifier] utilizing a maximum of ' + Minifier.maxThreads + ' additional threads'); Minifier.killAll = function () { - children.forEach(function (child) { + pool.forEach(function (child) { child.kill('SIGTERM'); }); - children = []; + pool.length = 0; }; -function removeChild(proc) { - children = children.filter(function (child) { - return child !== proc; - }); -} +function getChild() { + if (free.length) { + return free.shift(); + } -function forkAction(action, callback) { var forkProcessParams = setupDebugging(); var proc = childProcess.fork(__filename, [], Object.assign({}, forkProcessParams, { cwd: __dirname, @@ -66,12 +65,26 @@ function forkAction(action, callback) { minifier_child: true, }, })); + pool.push(proc); + + return proc; +} - children.push(proc); +function freeChild(proc) { + proc.removeAllListeners(); + free.push(proc); +} + +function removeChild(proc) { + var i = pool.indexOf(proc); + pool.splice(i, 1); +} + +function forkAction(action, callback) { + var proc = getChild(); proc.on('message', function (message) { - proc.kill(); - removeChild(proc); + freeChild(proc); if (message.type === 'error') { return callback(message.err); @@ -102,7 +115,7 @@ if (process.env.minifier_child) { if (typeof actions[action.act] !== 'function') { process.send({ type: 'error', - message: 'Unknown action', + err: Error('Unknown action'), }); return; } @@ -126,7 +139,7 @@ if (process.env.minifier_child) { } function executeAction(action, fork, callback) { - if (fork && children.length < Minifier.maxThreads) { + if (fork && (pool.length - free.length) < Minifier.maxThreads) { forkAction(action, callback); } else { if (typeof actions[action.act] !== 'function') { @@ -155,32 +168,38 @@ function concat(data, callback) { actions.concat = concat; function minifyJS(data, callback) { - var minified; + if (data.batch) { + async.eachLimit(data.files, 1000, function (ref, next) { + var srcPath = ref.srcPath; + var destPath = ref.destPath; + + fs.readFile(srcPath, function (err, buffer) { + if (err && err.code === 'ENOENT') { + return next(null, null); + } + if (err) { + return next(err); + } - if (data.fromSource) { - var sources = data.source; - var multiple = Array.isArray(sources); - if (!multiple) { - sources = [sources]; - } + try { + var minified = uglifyjs.minify(buffer.toString(), { + // outSourceMap: data.filename + '.map', + compress: data.compress, + fromString: true, + output: { + // suppress uglify line length warnings + max_line_len: 400000, + }, + }); - try { - minified = sources.map(function (source) { - return uglifyjs.minify(source, { - // outSourceMap: data.filename + '.map', - compress: data.compress, - fromString: true, - output: { - // suppress uglify line length warnings - max_line_len: 400000, - }, - }); + fs.writeFile(destPath, minified.code, next); + } catch (e) { + next(e); + } }); - } catch (e) { - return callback(e); - } + }, callback); - return callback(null, multiple ? minified : minified[0]); + return; } if (data.files && data.files.length) { @@ -190,16 +209,16 @@ function minifyJS(data, callback) { } try { - minified = uglifyjs.minify(scripts, { + var minified = uglifyjs.minify(scripts, { // outSourceMap: data.filename + '.map', compress: data.compress, fromString: false, }); + + callback(null, minified); } catch (e) { - return callback(e); + callback(e); } - - callback(null, minified); }); return; @@ -218,11 +237,11 @@ Minifier.js.bundle = function (scripts, minify, fork, callback) { }, fork, callback); }; -Minifier.js.minify = function (source, fork, callback) { +Minifier.js.minifyBatch = function (scripts, fork, callback) { executeAction({ act: 'minifyJS', - fromSource: true, - source: source, + files: scripts, + batch: true, }, fork, callback); }; From b888ed8e1aeb29fc20d62dba378388f277b5bd10 Mon Sep 17 00:00:00 2001 From: Ole R Date: Fri, 19 May 2017 11:07:37 +0200 Subject: [PATCH 098/263] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e915c853f9..ec678b10a5 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "nodebb-plugin-composer-default": "4.4.8", "nodebb-plugin-dbsearch": "2.0.2", "nodebb-plugin-emoji-extended": "1.1.1", - "nodebb-plugin-emoji-one": "1.2.0", + "nodebb-plugin-emoji-one": "1.2.1", "nodebb-plugin-markdown": "7.1.1", "nodebb-plugin-mentions": "2.0.3", "nodebb-plugin-soundpack-default": "1.0.0", From 4fadbcbbc88ad6bc53a97ccd8a710be70bf04572 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Fri, 19 May 2017 09:23:36 +0000 Subject: [PATCH 099/263] Latest translations and fallbacks --- .../language/tr/admin/advanced/database.json | 8 +++---- public/language/tr/admin/advanced/events.json | 2 +- .../language/tr/admin/general/dashboard.json | 22 +++++++++---------- .../language/tr/admin/general/homepage.json | 6 ++--- .../language/tr/admin/settings/uploads.json | 12 +++++----- public/language/tr/error.json | 4 ++-- 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/public/language/tr/admin/advanced/database.json b/public/language/tr/admin/advanced/database.json index 25989418d7..39fccc540c 100644 --- a/public/language/tr/admin/advanced/database.json +++ b/public/language/tr/admin/advanced/database.json @@ -8,13 +8,13 @@ "mongo": "Mongo", "mongo.version": "MongoDB Sürümü", "mongo.storage-engine": "Storage Engine", - "mongo.collections": "Collections", - "mongo.objects": "Objects", + "mongo.collections": "Koleksiyonlar", + "mongo.objects": "Objeler", "mongo.avg-object-size": "Avg. Object Size", "mongo.data-size": "Veri Boyutu", "mongo.storage-size": "Storage Size", - "mongo.index-size": "Index Size", - "mongo.file-size": "File Size", + "mongo.index-size": "İndex Boyutu", + "mongo.file-size": "Dosya Boyutu", "mongo.resident-memory": "Resident Memory", "mongo.virtual-memory": "Virtual Memory", "mongo.mapped-memory": "Mapped Memory", diff --git a/public/language/tr/admin/advanced/events.json b/public/language/tr/admin/advanced/events.json index 5bac444857..bca47b6a68 100644 --- a/public/language/tr/admin/advanced/events.json +++ b/public/language/tr/admin/advanced/events.json @@ -1,6 +1,6 @@ { "events": "Olaylar", "no-events": "Olay yok", - "control-panel": "Events Control Panel", + "control-panel": "Etkinlik Kontrol Paneli", "delete-events": "Olayları Sil" } \ No newline at end of file diff --git a/public/language/tr/admin/general/dashboard.json b/public/language/tr/admin/general/dashboard.json index 81f4fd76c6..e1145a6e63 100644 --- a/public/language/tr/admin/general/dashboard.json +++ b/public/language/tr/admin/general/dashboard.json @@ -5,14 +5,14 @@ "users": "Kullanıcılar", "posts": "İletiler", "topics": "Başlıklar", - "page-views-seven": "Last 7 Days", - "page-views-thirty": "Last 30 Days", - "page-views-last-day": "Last 24 hours", - "page-views-custom": "Custom Date Range", - "page-views-custom-start": "Range Start", - "page-views-custom-end": "Range End", + "page-views-seven": "Son 7 Gün", + "page-views-thirty": "Son 30 Gün", + "page-views-last-day": "Son 24 saat", + "page-views-custom": "Özel Tarih Aralığı", + "page-views-custom-start": "Başlangıç", + "page-views-custom-end": "Son", "page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is YYYY-MM-DD", - "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + "page-views-custom-error": "Lütfen tarih aralığını geçerli formatta girin YYYY-MM-DD", "stats.day": "Gün", "stats.week": "Hafta", @@ -36,9 +36,9 @@ "search-plugin-tooltip": "Install a search plugin from the plugin page in order to activate search functionality", "control-panel": "Sistem Kontrol", - "reload": "Tekrar Yükle", - "restart": "Yeniden Başlat", - "restart-warning": "Reloading or Restarting your NodeBB will drop all existing connections for a few seconds.", + "reload": "Reload", + "restart": "Restart", + "restart-warning": "NodeBB yeniden yüklemek veya yeniden başlatmak için mevcut tüm bağlantıları birkaç saniye düşürür.", "maintenance-mode": "Bakım Modu", "maintenance-mode-title": "NodeBB için bakım modunu ayarlamak için buraya tıklayın", "realtime-chart-updates": "Gerçek Zamanlı Grafik Güncellemeleri", @@ -60,7 +60,7 @@ "recent": "Yeni", "unread": "Okunmamış", - "high-presence-topics": "High Presence Topics", + "high-presence-topics": "Öne Çıkan Başlıklar", "graphs.page-views": "Sayfa Gösterimi", "graphs.unique-visitors": "Benzersiz Ziyaretçiler", diff --git a/public/language/tr/admin/general/homepage.json b/public/language/tr/admin/general/homepage.json index e38827a2b3..abca35b9e1 100644 --- a/public/language/tr/admin/general/homepage.json +++ b/public/language/tr/admin/general/homepage.json @@ -1,7 +1,7 @@ { "home-page": "Ana Sayfa", "description": "Choose what page is shown when users navigate to the root URL of your forum.", - "home-page-route": "Home Page Route", - "custom-route": "Custom Route", - "allow-user-home-pages": "Allow User Home Pages" + "home-page-route": "Anasayfa Yolu", + "custom-route": "Özel Yol", + "allow-user-home-pages": "Kullanıcılara anasayfayı özelleştirmeye izin ver" } \ No newline at end of file diff --git a/public/language/tr/admin/settings/uploads.json b/public/language/tr/admin/settings/uploads.json index 4daa55400b..b260563c77 100644 --- a/public/language/tr/admin/settings/uploads.json +++ b/public/language/tr/admin/settings/uploads.json @@ -8,19 +8,19 @@ "max-file-size-help": "(in kilobytes, default: 2048 KiB)", "allow-topic-thumbnails": "Allow users to upload topic thumbnails", "topic-thumb-size": "Topic Thumb Size", - "allowed-file-extensions": "Allowed File Extensions", + "allowed-file-extensions": "İzin Verilen Dosya Uzantıları", "allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. pdf,xls,doc). An empty list means all extensions are allowed.", - "profile-avatars": "Profile Avatars", + "profile-avatars": "Profil Avatarları", "allow-profile-image-uploads": "Allow users to upload profile images", "convert-profile-image-png": "Convert profile image uploads to PNG", "default-avatar": "Custom Default Avatar", "upload": "Yükle", - "profile-image-dimension": "Profile Image Dimension", - "profile-image-dimension-help": "(in pixels, default: 128 pixels)", + "profile-image-dimension": "Profil Resmi Boyutu", + "profile-image-dimension-help": "(Piksel cinsinden, varsayılan: 128 piksel)", "max-profile-image-size": "Maximum Profile Image File Size", - "max-profile-image-size-help": "(in kilobytes, default: 256 KiB)", + "max-profile-image-size-help": "(Kilobayt, varsayılan: 256 KiB)", "max-cover-image-size": "Maksimum Kapak Görseli Dosya Boyutu", - "max-cover-image-size-help": "(in kilobytes, default: 2,048 KiB)", + "max-cover-image-size-help": "(Kilobayt, varsayılan: 2,048 KiB)", "keep-all-user-images": "Keep old versions of avatars and profile covers on the server", "profile-covers": "Profil Kapakları", "default-covers": "Varsayılan Kapak Görseli", diff --git a/public/language/tr/error.json b/public/language/tr/error.json index 16c3a33815..f52f4aa8ea 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -30,7 +30,7 @@ "password-too-long": "Parola çok uzun", "user-banned": "Kullanıcı Yasaklı", "user-banned-reason": "Maalesef, bu hesap yasaklandı (Sebep:% 1)", - "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", + "user-banned-reason-until": "Maalesef, bu hesap %1 kadar yasaklandı (Sebep: %2)", "user-too-new": "Özür dileriz, ilk iletinizi yapmadan önce %1 saniye beklemeniz gerekiyor", "blacklisted-ip": "Üzgünüz, IP adresiniz, bu toplulukta yasaklandı. Bunun bir hata olduğunu düşünüyorsanız, bir yönetici ile irtibata geçiniz.", "ban-expiry-missing": "Bu yasak için bir bitiş tarihi girin", @@ -105,7 +105,7 @@ "chat-disabled": "Sohbet özelliği kapalı", "too-many-messages": "Ardı ardına çok fazla mesaj yolladınız, lütfen biraz bekleyiniz.", "invalid-chat-message": "Geçersiz sohbet mesajı", - "chat-message-too-long": "Chat messages can not be longer than %1 characters.", + "chat-message-too-long": "Sohbet mesajı %1 karakterden daha uzun olamaz.", "cant-edit-chat-message": "Bu mesajı düzenlemek için izin verilmez", "cant-remove-last-user": "Son kullanıcıyı silemezsiniz", "cant-delete-chat-message": "Bu mesajı silmek için izin verilmez", From 95196ceedcc1c05de17a0d41590b3c7beda0f7af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 14:52:36 -0400 Subject: [PATCH 100/263] closes #5692 --- src/database/redis/sorted.js | 8 +++++++- src/user/bans.js | 2 +- test/database/hash.js | 9 +++++++++ test/database/sorted.js | 2 ++ 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/database/redis/sorted.js b/src/database/redis/sorted.js index 9bb004550a..282928da97 100644 --- a/src/database/redis/sorted.js +++ b/src/database/redis/sorted.js @@ -129,7 +129,13 @@ module.exports = function (redisClient, module) { } redisClient.zscore(key, value, function (err, score) { - callback(err, !err ? parseFloat(score) : null); + if (err) { + return callback(err); + } + if (score === null) { + return callback(null, score); + } + callback(null, parseFloat(score)); }); }; diff --git a/src/user/bans.js b/src/user/bans.js index ad51c7c07c..e2cf2193b3 100644 --- a/src/user/bans.js +++ b/src/user/bans.js @@ -61,7 +61,7 @@ module.exports = function (User) { async.waterfall([ async.apply(User.getUserFields, uid, ['banned', 'banned:expire']), function (userData, next) { - var banned = parseInt(userData.banned, 10) === 1; + var banned = userData && parseInt(userData.banned, 10) === 1; if (!banned) { return next(null, banned); } diff --git a/test/database/hash.js b/test/database/hash.js index 7f9dc305d0..cffa761229 100644 --- a/test/database/hash.js +++ b/test/database/hash.js @@ -190,6 +190,15 @@ describe('Hash methods', function () { done(); }); }); + + it('should return undefined for all fields if object does not exist', function (done) { + db.getObjectsFields(['doesnotexist1', 'doesnotexist2'], ['name', 'age'], function (err, data) { + assert.ifError(err); + assert.equal(data.name, null); + assert.equal(data.age, null); + done(); + }); + }); }); describe('getObjectKeys()', function () { diff --git a/test/database/sorted.js b/test/database/sorted.js index b90e9cf71f..9e0178d4ed 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -303,6 +303,7 @@ describe('Sorted Set methods', function () { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!score, false); + assert.strictEqual(score, null); done(); }); }); @@ -312,6 +313,7 @@ describe('Sorted Set methods', function () { assert.equal(err, null); assert.equal(arguments.length, 2); assert.equal(!!score, false); + assert.strictEqual(score, null); done(); }); }); From fd47e230573dee60ecc6bce43acc4fbb4a32ce3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 16:18:18 -0400 Subject: [PATCH 101/263] test login for non-existant user --- test/authentication.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/authentication.js b/test/authentication.js index 7b49f69efa..6691a96c66 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -147,6 +147,33 @@ describe('authentication', function () { }); }); + it('should fail to login if user does not exist', function (done) { + var jar = request.jar(); + request({ + url: nconf.get('url') + '/api/config', + json: true, + jar: jar, + }, function (err, response, body) { + assert.ifError(err); + + request.post(nconf.get('url') + '/login', { + form: { + username: 'doesnotexist', + password: 'nopassword', + }, + json: true, + jar: jar, + headers: { + 'x-csrf-token': body.csrf_token, + }, + }, function (err, response, body) { + assert.equal(response.statusCode, 403); + assert.equal(body, '[[error:invalid-login-credentials]]'); + done(); + }); + }); + }); + after(function (done) { db.emptydb(done); From 4c1e25c8ce752bc3130b0ac83c849e568d070cd4 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Fri, 19 May 2017 14:27:52 -0600 Subject: [PATCH 102/263] Link instead of copying files Only mkdirp the necessary directories --- src/meta/js.js | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/src/meta/js.js b/src/meta/js.js index bfae9260c2..d0399c70c1 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -88,31 +88,16 @@ module.exports = function (Meta) { }, }; - function copyFile(source, target, cb) { - var called = false; - - var rd = fs.createReadStream(source); - rd.on('error', done); - - var wr = fs.createWriteStream(target); - wr.on('error', done); - wr.on('close', function () { - done(); - }); - rd.pipe(wr); - - function done(err) { - if (!called) { - cb(err); - called = true; + function minifyModules(modules, fork, callback) { + var moduleDirs = modules.reduce(function (prev, mod) { + var dir = path.resolve(path.dirname(mod.destPath)); + if (prev.indexOf(dir) === -1) { + prev.push(dir); } - } - } + return prev; + }, []); - function minifyModules(modules, fork, callback) { - async.eachLimit(modules, 1000, function (mod, next) { - mkdirp(path.dirname(mod.destPath), next); - }, function (err) { + async.eachLimit(moduleDirs, 1000, mkdirp, function (err) { if (err) { return callback(err); } @@ -133,7 +118,7 @@ module.exports = function (Meta) { }, function (cb) { async.eachLimit(filtered.skip, 500, function (mod, next) { - copyFile(mod.srcPath, mod.destPath, next); + file.link(mod.srcPath, mod.destPath, next); }, cb); }, ], callback); From 8f8e7202e5fef92d0f982f7aa78306c1c564e62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 16:33:40 -0400 Subject: [PATCH 103/263] fix lint --- test/authentication.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/authentication.js b/test/authentication.js index 6691a96c66..9d49a8e0ff 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -167,6 +167,7 @@ describe('authentication', function () { 'x-csrf-token': body.csrf_token, }, }, function (err, response, body) { + assert.ifError(err); assert.equal(response.statusCode, 403); assert.equal(body, '[[error:invalid-login-credentials]]'); done(); From 29ca1dbf0883fc6158132075696498d91deb5ffe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 17:14:46 -0400 Subject: [PATCH 104/263] replies/reply fix --- public/language/en-GB/topic.json | 1 + public/src/client/topic/replies.js | 15 +++++- src/topics/posts.js | 76 +++++++++++++++++------------- 3 files changed, 56 insertions(+), 36 deletions(-) diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index b960fb0fba..727417ca5f 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -17,6 +17,7 @@ "quote": "Quote", "reply": "Reply", "replies_to_this_post": "%1 Replies", + "one_reply_to_this_post": "1 Reply", "last_reply_time": "Last reply", "reply-as-topic": "Reply as topic", "guest-login-reply": "Log in to reply", diff --git a/public/src/client/topic/replies.js b/public/src/client/topic/replies.js index 7d9b9474bc..37ddd1912b 100644 --- a/public/src/client/topic/replies.js +++ b/public/src/client/topic/replies.js @@ -89,8 +89,19 @@ define('forum/topic/replies', ['navigator', 'components', 'forum/topic/posts'], var timestamp = replyCount.find('.timeago').attr('title', post.timestampISO); countEl.attr('data-replies', count); - replyCount.toggleClass('hidden', !count); - countEl.translateText('[[topic:replies_to_this_post, ' + count + ']]'); + replyCount.toggleClass('hidden', count <= 0); + if (count > 1) { + countEl.translateText('[[topic:replies_to_this_post, ' + count + ']]'); + } else { + countEl.translateText('[[topic:one_reply_to_this_post]]'); + } + + if (!avatars.find('[data-uid="' + post.uid + '"]').length && count < 7) { + app.parseAndTranslate('topic', 'posts', { posts: [{ replies: { users: [post.user] } }] }, function (html) { + avatars.prepend(html.find('[component="post/reply-count/avatars"] [component="user/picture"]')); + }); + } + avatars.addClass('hasMore'); timestamp.data('timeago', null).timeago(); diff --git a/src/topics/posts.js b/src/topics/posts.js index ce2b6dd457..3ed8deafc7 100644 --- a/src/topics/posts.js +++ b/src/topics/posts.js @@ -392,30 +392,36 @@ module.exports = function (Topics) { function getPostReplies(pids, callerUid, callback) { async.map(pids, function (pid, next) { - db.getSortedSetRange('pid:' + pid + ':replies', 0, -1, function (err, replyPids) { - if (err) { - return next(err); - } - - var uids = []; - var count = 0; - - async.until(function () { - return count === replyPids.length || uids.length === 6; - }, function (next) { - posts.getPostField(replyPids[count], 'uid', function (err, uid) { - uid = parseInt(uid, 10); - if (uids.indexOf(uid) === -1) { - uids.push(uid); - } - count += 1; - next(err); - }); - }, function (err) { - if (err) { - return next(err); - } - + var replyPids; + var uids = []; + async.waterfall([ + function (next) { + db.getSortedSetRange('pid:' + pid + ':replies', 0, -1, next); + }, + function (_replyPids, next) { + replyPids = _replyPids; + + var count = 0; + + async.until(function () { + return count === replyPids.length || uids.length === 6; + }, function (next) { + async.waterfall([ + function (next) { + posts.getPostField(replyPids[count], 'uid', next); + }, + function (uid, next) { + uid = parseInt(uid, 10); + if (uids.indexOf(uid) === -1) { + uids.push(uid); + } + count += 1; + next(); + }, + ], next); + }, next); + }, + function (next) { async.parallel({ users: function (next) { user.getUsersWithFields(uids, ['uid', 'username', 'userslug', 'picture'], callerUid, next); @@ -425,17 +431,19 @@ module.exports = function (Topics) { next(err, utils.toISOString(timestamp)); }); }, - }, function (err, replies) { - if (replies.users.length > 5) { - replies.users.shift(); - replies.hasMore = true; - } + }, next); + }, + function (replies, next) { + if (replies.users.length > 5) { + replies.users.shift(); + replies.hasMore = true; + } - replies.count = replyPids.length; - next(err, replies); - }); - }); - }); + replies.count = replyPids.length; + replies.text = replies.count > 1 ? '[[topic:replies_to_this_post, ' + replies.count + ']]' : '[[topic:one_reply_to_this_post]]'; + next(null, replies); + }, + ], next); }, callback); } }; From b1fe0c8f5f26c7f123abdfb01272438f2a99ac3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 18:49:34 -0400 Subject: [PATCH 105/263] up persona --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4be80a1ce1..7d7bacd4d9 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "nodebb-plugin-spam-be-gone": "0.5.0", "nodebb-rewards-essentials": "0.0.9", "nodebb-theme-lavender": "4.0.0", - "nodebb-theme-persona": "5.0.0", + "nodebb-theme-persona": "5.0.1", "nodebb-theme-vanilla": "6.0.2", "nodebb-widget-essentials": "3.0.0", "nodemailer": "2.6.4", From b3a265f981ecf720255294596b3a8ba84caacfc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 18:50:19 -0400 Subject: [PATCH 106/263] closes #5694 --- src/topics/unread.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/topics/unread.js b/src/topics/unread.js index ac26a277f9..f76206628f 100644 --- a/src/topics/unread.js +++ b/src/topics/unread.js @@ -267,6 +267,7 @@ module.exports = function (Topics) { categories.markAsRead(cids, uid, next); }, function (next) { + plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids }); next(null, true); }, ], callback); From 77894915eb01871844980c31f4aacd12ff322fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 19:35:10 -0400 Subject: [PATCH 107/263] closes #5684 --- src/controllers/topics.js | 2 +- src/socket.io/topics/infinitescroll.js | 2 +- test/topics.js | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/controllers/topics.js b/src/controllers/topics.js index ddd6fd889a..5c592f5bb3 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -49,7 +49,7 @@ topicsController.get = function (req, res, callback) { userPrivileges = results.privileges; - if (!userPrivileges.read || !userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { + if (!userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) { return helpers.notAllowed(req, res); } diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js index 1d732de2a4..17d44712ea 100644 --- a/src/socket.io/topics/infinitescroll.js +++ b/src/socket.io/topics/infinitescroll.js @@ -27,7 +27,7 @@ module.exports = function (SocketTopics) { }, next); }, function (results, next) { - if (!results.privileges.read || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { + if (!results.privileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { return callback(new Error('[[error:no-privileges]]')); } diff --git a/test/topics.js b/test/topics.js index c362ca0824..4551595fa6 100644 --- a/test/topics.js +++ b/test/topics.js @@ -760,13 +760,13 @@ describe('Topic\'s', function () { it('should 401 if not allowed to read as guest', function (done) { var privileges = require('../src/privileges'); - privileges.categories.rescind(['read'], topicData.cid, 'guests', function (err) { + privileges.categories.rescind(['topics:read'], topicData.cid, 'guests', function (err) { assert.ifError(err); request(nconf.get('url') + '/api/topic/' + topicData.slug, function (err, response, body) { assert.ifError(err); assert.equal(response.statusCode, 401); assert(body); - privileges.categories.give(['read'], topicData.cid, 'guests', done); + privileges.categories.give(['topics:read'], topicData.cid, 'guests', done); }); }); }); @@ -1551,7 +1551,7 @@ describe('Topic\'s', function () { it('should return empty array if first param is empty', function (done) { - topics.getTeasers([], function (err, teasers) { + topics.getTeasers([], 1, function (err, teasers) { assert.ifError(err); assert.equal(0, teasers.length); done(); @@ -1559,7 +1559,7 @@ describe('Topic\'s', function () { }); it('should get teasers with 2 params', function (done) { - topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) { + topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) { assert.ifError(err); assert.deepEqual([undefined, undefined], teasers); done(); @@ -1568,7 +1568,7 @@ describe('Topic\'s', function () { it('should get teasers with first posts', function (done) { meta.config.teaserPost = 'first'; - topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) { + topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) { assert.ifError(err); assert.equal(2, teasers.length); assert(teasers[0]); @@ -1581,7 +1581,7 @@ describe('Topic\'s', function () { }); it('should get teasers even if one topic is falsy', function (done) { - topics.getTeasers([null, topic2.topicData], function (err, teasers) { + topics.getTeasers([null, topic2.topicData], 1, function (err, teasers) { assert.ifError(err); assert.equal(2, teasers.length); assert.equal(undefined, teasers[0]); @@ -1598,7 +1598,7 @@ describe('Topic\'s', function () { topics.reply({ uid: adminUid, content: 'reply 1 content', tid: topic1.topicData.tid }, function (err, result) { assert.ifError(err); topic1.topicData.teaserPid = result.pid; - topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) { + topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) { assert.ifError(err); assert(teasers[0]); assert(teasers[1]); @@ -1610,7 +1610,7 @@ describe('Topic\'s', function () { }); it('should get teasers by tids', function (done) { - topics.getTeasersByTids([topic2.topicData.tid, topic1.topicData.tid], function (err, teasers) { + topics.getTeasersByTids([topic2.topicData.tid, topic1.topicData.tid], 1, function (err, teasers) { assert.ifError(err); assert(2, teasers.length); assert.equal(teasers[1].content, 'reply 1 content'); @@ -1619,7 +1619,7 @@ describe('Topic\'s', function () { }); it('should return empty array ', function (done) { - topics.getTeasersByTids([], function (err, teasers) { + topics.getTeasersByTids([], 1, function (err, teasers) { assert.ifError(err); assert.equal(0, teasers.length); done(); @@ -1627,7 +1627,7 @@ describe('Topic\'s', function () { }); it('should get teaser by tid', function (done) { - topics.getTeaser(topic2.topicData.tid, function (err, teaser) { + topics.getTeaser(topic2.topicData.tid, 1, function (err, teaser) { assert.ifError(err); assert(teaser); assert.equal(teaser.content, 'content 2'); From fd0043f36a8916c59ea6b2d8f9424bea71f70e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 20:24:54 -0400 Subject: [PATCH 108/263] more tests --- src/database/mongo.js | 128 ++++++++++++++++++++----------------- src/database/redis.js | 81 ++++++++++++----------- test/database.js | 43 +++++++++++-- test/database/list.js | 45 ++++++++++++- test/mocks/databasemock.js | 7 ++ 5 files changed, 201 insertions(+), 103 deletions(-) diff --git a/src/database/mongo.js b/src/database/mongo.js index fc37c7ae74..507a9c9a86 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -161,8 +161,11 @@ mongoModule.createIndices = function (callback) { mongoModule.checkCompatibility = function (callback) { var mongoPkg = require('mongodb/package.json'); + mongoModule.checkCompatibilityVersion(mongoPkg.version, callback); +}; - if (semver.lt(mongoPkg.version, '2.0.0')) { +mongoModule.checkCompatibilityVersion = function (version, callback) { + if (semver.lt(version, '2.0.0')) { return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.')); } @@ -173,66 +176,75 @@ mongoModule.info = function (db, callback) { if (!db) { return callback(); } - async.parallel({ - serverStatus: function (next) { - db.command({ serverStatus: 1 }, next); - }, - stats: function (next) { - db.command({ dbStats: 1 }, next); + async.waterfall([ + function (next) { + async.parallel({ + serverStatus: function (next) { + db.command({ serverStatus: 1 }, next); + }, + stats: function (next) { + db.command({ dbStats: 1 }, next); + }, + listCollections: function (next) { + getCollectionStats(db, next); + }, + }, next); }, - listCollections: function (next) { - db.listCollections().toArray(function (err, items) { - if (err) { - return next(err); - } - async.map(items, function (collection, next) { - db.collection(collection.name).stats(next); - }, next); + function (results, next) { + var stats = results.stats; + var scale = 1024 * 1024 * 1024; + + results.listCollections = results.listCollections.map(function (collectionInfo) { + return { + name: collectionInfo.ns, + count: collectionInfo.count, + size: collectionInfo.size, + avgObjSize: collectionInfo.avgObjSize, + storageSize: collectionInfo.storageSize, + totalIndexSize: collectionInfo.totalIndexSize, + indexSizes: collectionInfo.indexSizes, + }; }); - }, - }, function (err, results) { - if (err) { - return callback(err); - } - var stats = results.stats; - var scale = 1024 * 1024 * 1024; - - results.listCollections = results.listCollections.map(function (collectionInfo) { - return { - name: collectionInfo.ns, - count: collectionInfo.count, - size: collectionInfo.size, - avgObjSize: collectionInfo.avgObjSize, - storageSize: collectionInfo.storageSize, - totalIndexSize: collectionInfo.totalIndexSize, - indexSizes: collectionInfo.indexSizes, - }; - }); - stats.mem = results.serverStatus.mem; - stats.mem = results.serverStatus.mem; - stats.mem.resident = (stats.mem.resident / 1024).toFixed(2); - stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2); - stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2); - stats.collectionData = results.listCollections; - stats.network = results.serverStatus.network; - stats.raw = JSON.stringify(stats, null, 4); - - stats.avgObjSize = stats.avgObjSize.toFixed(2); - stats.dataSize = (stats.dataSize / scale).toFixed(2); - stats.storageSize = (stats.storageSize / scale).toFixed(2); - stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0; - stats.indexSize = (stats.indexSize / scale).toFixed(2); - stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1'; - stats.host = results.serverStatus.host; - stats.version = results.serverStatus.version; - stats.uptime = results.serverStatus.uptime; - stats.mongo = true; - - callback(null, stats); - }); + stats.mem = results.serverStatus.mem; + stats.mem = results.serverStatus.mem; + stats.mem.resident = (stats.mem.resident / 1024).toFixed(2); + stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2); + stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2); + stats.collectionData = results.listCollections; + stats.network = results.serverStatus.network; + stats.raw = JSON.stringify(stats, null, 4); + + stats.avgObjSize = stats.avgObjSize.toFixed(2); + stats.dataSize = (stats.dataSize / scale).toFixed(2); + stats.storageSize = (stats.storageSize / scale).toFixed(2); + stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0; + stats.indexSize = (stats.indexSize / scale).toFixed(2); + stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1'; + stats.host = results.serverStatus.host; + stats.version = results.serverStatus.version; + stats.uptime = results.serverStatus.uptime; + stats.mongo = true; + + next(null, stats); + }, + ], callback); }; -mongoModule.close = function () { - db.close(); +function getCollectionStats(db, callback) { + async.waterfall([ + function (next) { + db.listCollections().toArray(next); + }, + function (items, next) { + async.map(items, function (collection, next) { + db.collection(collection.name).stats(next); + }, next); + }, + ], callback); +} + +mongoModule.close = function (callback) { + callback = callback || function () {}; + db.close(callback); }; diff --git a/src/database/redis.js b/src/database/redis.js index 9ebc154705..d0d80d0038 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -1,6 +1,7 @@ 'use strict'; var _ = require('underscore'); +var async = require('async'); var winston = require('winston'); var nconf = require('nconf'); var semver = require('semver'); @@ -71,10 +72,6 @@ redisModule.connect = function (options) { var redis_socket_or_host = nconf.get('redis:host'); var cxn; - if (!redis) { - redis = require('redis'); - } - options = options || {}; if (nconf.get('redis:password')) { @@ -101,10 +98,10 @@ redisModule.connect = function (options) { } var dbIdx = parseInt(nconf.get('redis:database'), 10); - if (dbIdx) { - cxn.select(dbIdx, function (error) { - if (error) { - winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + error.message); + if (dbIdx >= 0) { + cxn.select(dbIdx, function (err) { + if (err) { + winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + err.message); process.exit(); } }); @@ -118,46 +115,52 @@ redisModule.createIndices = function (callback) { }; redisModule.checkCompatibility = function (callback) { - redisModule.info(redisModule.client, function (err, info) { - if (err) { - return callback(err); - } - - if (semver.lt(info.redis_version, '2.8.9')) { - return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.')); - } + async.waterfall([ + function (next) { + redisModule.info(redisModule.client, next); + }, + function (info, next) { + redisModule.checkCompatibilityVersion(info.redis_version, next); + }, + ], callback); +}; - callback(); - }); +redisModule.checkCompatibilityVersion = function (version, callback) { + if (semver.lt(version, '2.8.9')) { + return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.')); + } + callback(); }; -redisModule.close = function () { - redisClient.quit(); +redisModule.close = function (callback) { + callback = callback || function () {}; + redisClient.quit(callback); }; redisModule.info = function (cxn, callback) { if (!cxn) { return callback(); } - cxn.info(function (err, data) { - if (err) { - return callback(err); - } - - var lines = data.toString().split('\r\n').sort(); - var redisData = {}; - lines.forEach(function (line) { - var parts = line.split(':'); - if (parts[1]) { - redisData[parts[0]] = parts[1]; - } - }); - redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2); - redisData.raw = JSON.stringify(redisData, null, 4); - redisData.redis = true; - - callback(null, redisData); - }); + async.waterfall([ + function (next) { + cxn.info(next); + }, + function (data, next) { + var lines = data.toString().split('\r\n').sort(); + var redisData = {}; + lines.forEach(function (line) { + var parts = line.split(':'); + if (parts[1]) { + redisData[parts[0]] = parts[1]; + } + }); + redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2); + redisData.raw = JSON.stringify(redisData, null, 4); + redisData.redis = true; + + next(null, redisData); + }, + ], callback); }; redisModule.helpers = redisModule.helpers || {}; diff --git a/test/database.js b/test/database.js index f55bf78edb..327d8095cf 100644 --- a/test/database.js +++ b/test/database.js @@ -2,6 +2,7 @@ var assert = require('assert'); +var nconf = require('nconf'); var db = require('./mocks/databasemock'); @@ -12,14 +13,46 @@ describe('Test database', function () { }); }); - it('should return info about database', function (done) { - db.info(db.client, function (err, info) { - assert.ifError(err); - assert(info); - done(); + describe('info', function () { + it('should return info about database', function (done) { + db.info(db.client, function (err, info) { + assert.ifError(err); + assert(info); + done(); + }); + }); + + it('should not error and return null if client is falsy', function (done) { + db.info(null, function (err, info) { + assert.ifError(err); + assert.equal(info, null); + done(); + }); + }); + }); + + describe('checkCompatibility', function () { + it('should not throw', function (done) { + db.checkCompatibility(done); + }); + + it('should return error with a too low version', function (done) { + var dbName = nconf.get('database'); + if (dbName === 'redis') { + db.checkCompatibilityVersion('2.4.0', function (err) { + assert.equal(err.message, 'Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'); + done(); + }); + } else if (dbName === 'mongo') { + db.checkCompatibilityVersion('1.8.0', function (err) { + assert.equal(err.message, 'The `mongodb` package is out-of-date, please run `./nodebb setup` again.'); + done(); + }); + } }); }); + require('./database/keys'); require('./database/list'); require('./database/sets'); diff --git a/test/database/list.js b/test/database/list.js index 8475ad2f52..1f5d72c6c3 100644 --- a/test/database/list.js +++ b/test/database/list.js @@ -9,11 +9,18 @@ describe('List methods', function () { describe('listAppend()', function () { it('should append to a list', function (done) { db.listAppend('testList1', 5, function (err) { - assert.equal(err, null); + assert.ifError(err); assert.equal(arguments.length, 1); done(); }); }); + + it('should not add anyhing if key is falsy', function (done) { + db.listAppend(null, 3, function (err) { + assert.ifError(err); + done(); + }); + }); }); describe('listPrepend()', function () { @@ -38,6 +45,13 @@ describe('List methods', function () { done(); }); }); + + it('should not add anyhing if key is falsy', function (done) { + db.listPrepend(null, 3, function (err) { + assert.ifError(err); + done(); + }); + }); }); describe('getListRange()', function () { @@ -83,6 +97,14 @@ describe('List methods', function () { done(); }); }); + + it('should not get anyhing if key is falsy', function (done) { + db.getListRange(null, 0, -1, function (err, data) { + assert.ifError(err); + assert.equal(data, undefined); + done(); + }); + }); }); describe('listRemoveLast()', function () { @@ -105,6 +127,13 @@ describe('List methods', function () { done(); }); }); + + it('should not remove anyhing if key is falsy', function (done) { + db.listRemoveLast(null, function (err) { + assert.ifError(err); + done(); + }); + }); }); describe('listRemoveAll()', function () { @@ -132,6 +161,13 @@ describe('List methods', function () { }); }); }); + + it('should not remove anyhing if key is falsy', function (done) { + db.listRemoveAll(null, 3, function (err) { + assert.ifError(err); + done(); + }); + }); }); describe('listTrim()', function () { @@ -156,6 +192,13 @@ describe('List methods', function () { }); }); }); + + it('should not add anyhing if key is falsy', function (done) { + db.listTrim(null, 0, 3, function (err) { + assert.ifError(err); + done(); + }); + }); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 4432f944f1..5b186e7f35 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -81,6 +81,10 @@ var db = require('../../src/database'); + after(function (done) { + db.close(done); + }); + before(function (done) { this.timeout(30000); var meta; @@ -91,6 +95,9 @@ function (next) { db.emptydb(next); }, + function (next) { + db.createIndices(next); + }, function (next) { winston.info('test_database flushed'); meta = require('../../src/meta'); From fe0a5aaf28a281ce8b8f6376d7667524ab5f1d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Fri, 19 May 2017 20:32:31 -0400 Subject: [PATCH 109/263] fix redis tests --- src/database/redis/list.js | 18 ++++++++++++++++++ test/database/list.js | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/database/redis/list.js b/src/database/redis/list.js index fb445573ff..f8108a194d 100644 --- a/src/database/redis/list.js +++ b/src/database/redis/list.js @@ -3,6 +3,9 @@ module.exports = function (redisClient, module) { module.listPrepend = function (key, value, callback) { callback = callback || function () {}; + if (!key) { + return callback(); + } redisClient.lpush(key, value, function (err) { callback(err); }); @@ -10,6 +13,9 @@ module.exports = function (redisClient, module) { module.listAppend = function (key, value, callback) { callback = callback || function () {}; + if (!key) { + return callback(); + } redisClient.rpush(key, value, function (err) { callback(err); }); @@ -17,11 +23,17 @@ module.exports = function (redisClient, module) { module.listRemoveLast = function (key, callback) { callback = callback || function () {}; + if (!key) { + return callback(); + } redisClient.rpop(key, callback); }; module.listRemoveAll = function (key, value, callback) { callback = callback || function () {}; + if (!key) { + return callback(); + } redisClient.lrem(key, 0, value, function (err) { callback(err); }); @@ -29,6 +41,9 @@ module.exports = function (redisClient, module) { module.listTrim = function (key, start, stop, callback) { callback = callback || function () {}; + if (!key) { + return callback(); + } redisClient.ltrim(key, start, stop, function (err) { callback(err); }); @@ -36,6 +51,9 @@ module.exports = function (redisClient, module) { module.getListRange = function (key, start, stop, callback) { callback = callback || function () {}; + if (!key) { + return callback(); + } redisClient.lrange(key, start, stop, callback); }; }; diff --git a/test/database/list.js b/test/database/list.js index 1f5d72c6c3..7d00df3e8d 100644 --- a/test/database/list.js +++ b/test/database/list.js @@ -98,7 +98,7 @@ describe('List methods', function () { }); }); - it('should not get anyhing if key is falsy', function (done) { + it('should not get anything if key is falsy', function (done) { db.getListRange(null, 0, -1, function (err, data) { assert.ifError(err); assert.equal(data, undefined); From 4b94446cd9ed98b09ff52f009dd814c802fbdbc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 16:26:56 -0400 Subject: [PATCH 110/263] more user search tests --- src/user/search.js | 74 ++++++++++++++++++++++++---------------------- test/user.js | 64 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 36 deletions(-) diff --git a/src/user/search.js b/src/user/search.js index 4583e0e28f..967c32c32b 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -68,16 +68,17 @@ module.exports = function (User) { var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20; var hardCap = resultsPerPage * 10; - db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function (err, data) { - if (err) { - return callback(err); - } - - var uids = data.map(function (data) { - return data.split(':')[1]; - }); - callback(null, uids); - }); + async.waterfall([ + function (next) { + db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, next); + }, + function (data, next) { + var uids = data.map(function (data) { + return data.split(':')[1]; + }); + next(null, uids); + }, + ], callback); } function filterAndSortUids(uids, data, callback) { @@ -94,37 +95,38 @@ module.exports = function (User) { fields.push('flags'); } - User.getUsersFields(uids, fields, function (err, userData) { - if (err) { - return callback(err); - } - - if (data.onlineOnly) { - userData = userData.filter(function (user) { - return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000); - }); - } + async.waterfall([ + function (next) { + User.getUsersFields(uids, fields, next); + }, + function (userData, next) { + if (data.onlineOnly) { + userData = userData.filter(function (user) { + return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000); + }); + } - if (data.bannedOnly) { - userData = userData.filter(function (user) { - return user && user.banned; - }); - } + if (data.bannedOnly) { + userData = userData.filter(function (user) { + return user && parseInt(user.banned, 10) === 1; + }); + } - if (data.flaggedOnly) { - userData = userData.filter(function (user) { - return user && parseInt(user.flags, 10) > 0; - }); - } + if (data.flaggedOnly) { + userData = userData.filter(function (user) { + return user && parseInt(user.flags, 10) > 0; + }); + } - sortUsers(userData, sortBy); + sortUsers(userData, sortBy); - uids = userData.map(function (user) { - return user && user.uid; - }); + uids = userData.map(function (user) { + return user && user.uid; + }); - callback(null, uids); - }); + next(null, uids); + }, + ], callback); } function sortUsers(userData, sortBy) { diff --git a/test/user.js b/test/user.js index f7d9986336..fd40b990dd 100644 --- a/test/user.js +++ b/test/user.js @@ -237,6 +237,70 @@ describe('User', function () { done(); }); }); + + it('should search users by ip', function (done) { + User.create({ username: 'ipsearch' }, function (err, uid) { + assert.ifError(err); + db.sortedSetAdd('ip:1.1.1.1:uid', [1, 1], [testUid, uid], function (err) { + assert.ifError(err); + socketUser.search({ uid: testUid }, { query: '1.1.1.1', searchBy: 'ip' }, function (err, data) { + assert.ifError(err); + assert(Array.isArray(data.users)); + assert.equal(data.users.length, 2); + done(); + }); + }); + }); + }); + + it('should return empty array if query is empty', function (done) { + socketUser.search({ uid: testUid }, { query: '' }, function (err, data) { + assert.ifError(err); + assert.equal(data.users.length, 0); + done(); + }); + }); + + it('should filter users', function (done) { + User.create({ username: 'ipsearch_filter' }, function (err, uid) { + assert.ifError(err); + User.setUserFields(uid, { banned: 1, flags: 10 }, function (err) { + assert.ifError(err); + socketUser.search({ uid: testUid }, { + query: 'ipsearch', + onlineOnly: true, + bannedOnly: true, + flaggedOnly: true, + }, function (err, data) { + assert.ifError(err); + assert.equal(data.users[0].username, 'ipsearch_filter'); + done(); + }); + }); + }); + }); + + it('should sort results by username', function (done) { + async.waterfall([ + function (next) { + User.create({ username: 'brian' }, next); + }, + function (uid, next) { + User.create({ username: 'baris' }, next); + }, + function (uid, next) { + socketUser.search({ uid: testUid }, { + query: 'b', + sortBy: 'username', + }, next); + }, + ], function (err, data) { + assert.ifError(err); + assert.equal(data.users[0].username, 'baris'); + assert.equal(data.users[1].username, 'brian'); + done(); + }); + }); }); describe('.delete()', function () { From 9fdd313e308539c5845be3c742390efbb0309f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 16:46:07 -0400 Subject: [PATCH 111/263] tweak test --- test/user.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/user.js b/test/user.js index fd40b990dd..6f5b2e3150 100644 --- a/test/user.js +++ b/test/user.js @@ -288,16 +288,21 @@ describe('User', function () { function (uid, next) { User.create({ username: 'baris' }, next); }, + function (uid, next) { + User.create({ username: 'bzari' }, next); + }, function (uid, next) { socketUser.search({ uid: testUid }, { query: 'b', sortBy: 'username', + paginate: false, }, next); }, ], function (err, data) { assert.ifError(err); assert.equal(data.users[0].username, 'baris'); assert.equal(data.users[1].username, 'brian'); + assert.equal(data.users[2].username, 'bzari'); done(); }); }); From 769d2b00b1fce4e2ac14429624014b2c942a34a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 17:08:20 -0400 Subject: [PATCH 112/263] change test method call --- test/user.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/user.js b/test/user.js index 6f5b2e3150..fd1d720ab5 100644 --- a/test/user.js +++ b/test/user.js @@ -292,7 +292,8 @@ describe('User', function () { User.create({ username: 'bzari' }, next); }, function (uid, next) { - socketUser.search({ uid: testUid }, { + User.search({ + uid: testUid, query: 'b', sortBy: 'username', paginate: false, From edeb22fcdfb872ed6dae334a258e84db58cb90c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 20:02:53 -0400 Subject: [PATCH 113/263] change style of meta.js --- src/meta.js | 118 ++++++++++++++++++++++++++-------------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/src/meta.js b/src/meta.js index 1fb0170297..59c3447cf4 100644 --- a/src/meta.js +++ b/src/meta.js @@ -8,71 +8,71 @@ var nconf = require('nconf'); var pubsub = require('./pubsub'); var utils = require('./utils'); -(function (Meta) { - Meta.reloadRequired = false; +var Meta = module.exports; - require('./meta/configs')(Meta); - require('./meta/themes')(Meta); - require('./meta/js')(Meta); - require('./meta/css')(Meta); - require('./meta/sounds')(Meta); - require('./meta/settings')(Meta); - require('./meta/logs')(Meta); - require('./meta/errors')(Meta); - require('./meta/tags')(Meta); - require('./meta/dependencies')(Meta); - Meta.templates = require('./meta/templates'); - Meta.blacklist = require('./meta/blacklist'); - Meta.languages = require('./meta/languages'); +Meta.reloadRequired = false; - /* Assorted */ - Meta.userOrGroupExists = function (slug, callback) { - var user = require('./user'); - var groups = require('./groups'); - slug = utils.slugify(slug); - async.parallel([ - async.apply(user.existsBySlug, slug), - async.apply(groups.existsBySlug, slug), - ], function (err, results) { - callback(err, results ? results.some(function (result) { return result; }) : false); - }); - }; +require('./meta/configs')(Meta); +require('./meta/themes')(Meta); +require('./meta/js')(Meta); +require('./meta/css')(Meta); +require('./meta/sounds')(Meta); +require('./meta/settings')(Meta); +require('./meta/logs')(Meta); +require('./meta/errors')(Meta); +require('./meta/tags')(Meta); +require('./meta/dependencies')(Meta); +Meta.templates = require('./meta/templates'); +Meta.blacklist = require('./meta/blacklist'); +Meta.languages = require('./meta/languages'); - /** - * Reload deprecated as of v1.1.2+, remove in v2.x - */ - Meta.reload = function (callback) { - restart(); - callback(); - }; +/* Assorted */ +Meta.userOrGroupExists = function (slug, callback) { + var user = require('./user'); + var groups = require('./groups'); + slug = utils.slugify(slug); + async.parallel([ + async.apply(user.existsBySlug, slug), + async.apply(groups.existsBySlug, slug), + ], function (err, results) { + callback(err, results ? results.some(function (result) { return result; }) : false); + }); +}; - Meta.restart = function () { - pubsub.publish('meta:restart', { hostname: os.hostname() }); - restart(); - }; +/** + * Reload deprecated as of v1.1.2+, remove in v2.x + */ +Meta.reload = function (callback) { + restart(); + callback(); +}; - Meta.getSessionTTLSeconds = function () { - var ttlDays = 60 * 60 * 24 * (parseInt(Meta.config.loginDays, 10) || 0); - var ttlSeconds = (parseInt(Meta.config.loginSeconds, 10) || 0); - var ttl = ttlSeconds || ttlDays || 1209600; // Default to 14 days - return ttl; - }; +Meta.restart = function () { + pubsub.publish('meta:restart', { hostname: os.hostname() }); + restart(); +}; - if (nconf.get('isPrimary') === 'true') { - pubsub.on('meta:restart', function (data) { - if (data.hostname !== os.hostname()) { - restart(); - } - }); - } +Meta.getSessionTTLSeconds = function () { + var ttlDays = 60 * 60 * 24 * (parseInt(Meta.config.loginDays, 10) || 0); + var ttlSeconds = (parseInt(Meta.config.loginSeconds, 10) || 0); + var ttl = ttlSeconds || ttlDays || 1209600; // Default to 14 days + return ttl; +}; - function restart() { - if (process.send) { - process.send({ - action: 'restart', - }); - } else { - winston.error('[meta.restart] Could not restart, are you sure NodeBB was started with `./nodebb start`?'); +if (nconf.get('isPrimary') === 'true') { + pubsub.on('meta:restart', function (data) { + if (data.hostname !== os.hostname()) { + restart(); } + }); +} + +function restart() { + if (process.send) { + process.send({ + action: 'restart', + }); + } else { + winston.error('[meta.restart] Could not restart, are you sure NodeBB was started with `./nodebb start`?'); } -}(exports)); +} From 7fb45cc14822a77be643fe106bf316c26313f862 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 20:03:41 -0400 Subject: [PATCH 114/263] more tests --- src/socket.io/modules.js | 133 +++++++------- test/messaging.js | 298 ++++++++++++++++++++++++++++++- test/meta.js | 15 ++ test/mocks/databasemock.js | 353 ++++++++++++++++++------------------- 4 files changed, 555 insertions(+), 244 deletions(-) diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index a3a9ff4fe3..172d91e60f 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -12,16 +12,16 @@ var utils = require('../utils'); var server = require('./'); var user = require('../user'); -var SocketModules = { - chats: {}, - sounds: {}, - settings: {}, -}; +var SocketModules = module.exports; + +SocketModules.chats = {}; +SocketModules.sounds = {}; +SocketModules.settings = {}; /* Chat */ SocketModules.chats.getRaw = function (socket, data, callback) { - if (!data || !data.hasOwnProperty('mid')) { + if (!data || !data.hasOwnProperty('mid') || !data.hasOwnProperty('roomId')) { return callback(new Error('[[error:invalid-data]]')); } async.waterfall([ @@ -57,13 +57,14 @@ SocketModules.chats.newRoom = function (socket, data, callback) { return callback(new Error('[[error:too-many-messages]]')); } - Messaging.canMessageUser(socket.uid, data.touid, function (err) { - if (err) { - return callback(err); - } - - Messaging.newRoom(socket.uid, [data.touid], callback); - }); + async.waterfall([ + function (next) { + Messaging.canMessageUser(socket.uid, data.touid, next); + }, + function (next) { + Messaging.newRoom(socket.uid, [data.touid], next); + }, + ], callback); }; SocketModules.chats.send = function (socket, data, callback) { @@ -223,17 +224,21 @@ SocketModules.chats.leave = function (socket, roomid, callback) { SocketModules.chats.edit = function (socket, data, callback) { - if (!data || !data.roomId) { + if (!data || !data.roomId || !data.message) { return callback(new Error('[[error:invalid-data]]')); } - Messaging.canEdit(data.mid, socket.uid, function (err, allowed) { - if (err || !allowed) { - return callback(err || new Error('[[error:cant-edit-chat-message]]')); - } - - Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, callback); - }); + async.waterfall([ + function (next) { + Messaging.canEdit(data.mid, socket.uid, next); + }, + function (allowed, next) { + if (!allowed) { + return next(new Error('[[error:cant-edit-chat-message]]')); + } + Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, next); + }, + ], callback); }; SocketModules.chats.delete = function (socket, data, callback) { @@ -241,13 +246,18 @@ SocketModules.chats.delete = function (socket, data, callback) { return callback(new Error('[[error:invalid-data]]')); } - Messaging.canEdit(data.messageId, socket.uid, function (err, allowed) { - if (err || !allowed) { - return callback(err || new Error('[[error:cant-delete-chat-message]]')); - } + async.waterfall([ + function (next) { + Messaging.canEdit(data.messageId, socket.uid, next); + }, + function (allowed, next) { + if (!allowed) { + return next(new Error('[[error:cant-delete-chat-message]]')); + } - Messaging.deleteMessage(data.messageId, data.roomId, callback); - }); + Messaging.deleteMessage(data.messageId, data.roomId, next); + }, + ], callback); }; SocketModules.chats.canMessage = function (socket, roomId, callback) { @@ -255,37 +265,38 @@ SocketModules.chats.canMessage = function (socket, roomId, callback) { }; SocketModules.chats.markRead = function (socket, roomId, callback) { - if (!socket.uid) { + if (!socket.uid || !roomId) { return callback(new Error('[[error:invalid-data]]')); } - async.parallel({ - uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1), - markRead: async.apply(Messaging.markRead, socket.uid, roomId), - }, function (err, results) { - if (err) { - return callback(err); - } - - Messaging.pushUnreadCount(socket.uid); - server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId }); - - if (results.uidsInRoom.indexOf(socket.uid.toString()) === -1) { - return callback(); - } - - // Mark notification read - var nids = results.uidsInRoom.filter(function (uid) { - return parseInt(uid, 10) !== socket.uid; - }).map(function (uid) { - return 'chat_' + uid + '_' + roomId; - }); - - notifications.markReadMultiple(nids, socket.uid, function () { - user.notifications.pushCount(socket.uid); - }); - - callback(); - }); + async.waterfall([ + function (next) { + async.parallel({ + uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1), + markRead: async.apply(Messaging.markRead, socket.uid, roomId), + }, next); + }, + function (results, next) { + Messaging.pushUnreadCount(socket.uid); + server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId }); + + if (results.uidsInRoom.indexOf(socket.uid.toString()) === -1) { + return callback(); + } + + // Mark notification read + var nids = results.uidsInRoom.filter(function (uid) { + return parseInt(uid, 10) !== socket.uid; + }).map(function (uid) { + return 'chat_' + uid + '_' + roomId; + }); + + notifications.markReadMultiple(nids, socket.uid, function () { + user.notifications.pushCount(socket.uid); + }); + + next(); + }, + ], callback); }; SocketModules.chats.markAllRead = function (socket, data, callback) { @@ -301,8 +312,8 @@ SocketModules.chats.markAllRead = function (socket, data, callback) { }; SocketModules.chats.renameRoom = function (socket, data, callback) { - if (!data) { - return callback(new Error('[[error:invalid-name]]')); + if (!data || !data.roomId || !data.newName) { + return callback(new Error('[[error:invalid-data]]')); } async.waterfall([ @@ -333,13 +344,13 @@ SocketModules.chats.getRecentChats = function (socket, data, callback) { SocketModules.chats.hasPrivateChat = function (socket, uid, callback) { if (!socket.uid || !uid) { - return callback(null, new Error('[[error:invalid-data]]')); + return callback(new Error('[[error:invalid-data]]')); } Messaging.hasPrivateChat(socket.uid, uid, callback); }; SocketModules.chats.getMessages = function (socket, data, callback) { - if (!socket.uid || !data.uid || !data.roomId) { + if (!socket.uid || !data || !data.uid || !data.roomId) { return callback(new Error('[[error:invalid-data]]')); } @@ -358,5 +369,3 @@ SocketModules.chats.getMessages = function (socket, data, callback) { SocketModules.sounds.getUserSoundMap = function getUserSoundMap(socket, data, callback) { meta.sounds.getUserSoundMap(socket.uid, callback); }; - -module.exports = SocketModules; diff --git a/test/messaging.js b/test/messaging.js index 436fd78a87..a009d5ad21 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -11,7 +11,7 @@ var User = require('../src/user'); var Groups = require('../src/groups'); var Messaging = require('../src/messaging'); var helpers = require('./helpers'); - +var socketModules = require('../src/socket.io/modules'); describe('Messaging Library', function () { var fooUid; @@ -55,7 +55,10 @@ describe('Messaging Library', function () { assert.ifError(err); Messaging.canMessageUser(herpUid, bazUid, function (err) { assert.strictEqual(err.message, '[[error:chat-restricted]]'); - done(); + socketModules.chats.addUserToRoom({ uid: herpUid }, { roomId: 1, username: 'baz' }, function (err) { + assert.equal(err.message, '[[error:chat-restricted]]'); + done(); + }); }); }); }); @@ -78,23 +81,93 @@ describe('Messaging Library', function () { }); describe('rooms', function () { - var socketModules = require('../src/socket.io/modules'); + it('should fail to create a new chat room with invalid data', function (done) { + socketModules.chats.newRoom({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + + it('should return rate limit error on second try', function (done) { + var socketMock = { uid: fooUid }; + socketModules.chats.newRoom(socketMock, { touid: bazUid }, function (err) { + assert.ifError(err); + socketModules.chats.newRoom(socketMock, { touid: bazUid }, function (err) { + assert.equal(err.message, '[[error:too-many-messages]]'); + done(); + }); + }); + }); + it('should create a new chat room', function (done) { socketModules.chats.newRoom({ uid: fooUid }, { touid: bazUid }, function (err, _roomId) { roomId = _roomId; assert.ifError(err); assert(roomId); - done(); + socketModules.chats.canMessage({ uid: fooUid }, _roomId, function (err) { + assert.ifError(err); + done(); + }); + }); + }); + + it('should fail to add user to room with invalid data', function (done) { + socketModules.chats.addUserToRoom({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); }); }); it('should add a user to room', function (done) { socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) { assert.ifError(err); + Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) { + assert.ifError(err); + assert(isInRoom); + done(); + }); + }); + }); + + it('should fail to add users to room if max is reached', function (done) { + meta.config.maximumUsersInChatRoom = 2; + socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'test' }, function (err) { + assert.equal(err.message, '[[error:cant-add-more-users-to-chat-room]]'); + meta.config.maximumUsersInChatRoom = 0; + done(); + }); + }); + + it('should fail to add users to room if user does not exist', function (done) { + socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'doesnotexist' }, function (err) { + assert.equal(err.message, '[[error:no-user]]'); + done(); + }); + }); + + it('should fail to add self to room', function (done) { + socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'foo' }, function (err) { + assert.equal(err.message, '[[error:cant-add-self-to-chat-room]]'); done(); }); }); + it('should fail to leave room with invalid data', function (done) { + socketModules.chats.leave({ uid: null }, roomId, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.leave({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + it('should leave the chat room', function (done) { socketModules.chats.leave({ uid: bazUid }, roomId, function (err) { assert.ifError(err); @@ -106,6 +179,60 @@ describe('Messaging Library', function () { }); }); + it('should fail to remove user from room', function (done) { + socketModules.chats.removeUserFromRoom({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.removeUserFromRoom({ uid: fooUid }, {}, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + + it('should fail to remove user from room if user does not exist', function (done) { + socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'doesnotexist' }, function (err) { + assert.equal(err.message, '[[error:no-user]]'); + done(); + }); + }); + + it('should remove user from room', function (done) { + socketModules.chats.newRoom({ uid: fooUid }, { touid: herpUid }, function (err, roomId) { + assert.ifError(err); + Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) { + assert.ifError(err); + assert(isInRoom); + socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) { + assert.equal(err.message, '[[error:cant-remove-last-user]]'); + socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'baz' }, function (err) { + assert.ifError(err); + socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) { + assert.ifError(err); + Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) { + assert.ifError(err); + assert(!isInRoom); + done(); + }); + }); + }); + }); + }); + }); + }); + + it('should fail to send a message to room with invalid data', function (done) { + socketModules.chats.send({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.send({ uid: fooUid }, { roomId: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.send({ uid: null }, { roomId: 1 }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + }); + it('should send a message to a room', function (done) { socketModules.chats.send({ uid: fooUid }, { roomId: roomId, message: 'first chat message' }, function (err, messageData) { assert.ifError(err); @@ -116,11 +243,39 @@ describe('Messaging Library', function () { socketModules.chats.getRaw({ uid: fooUid }, { roomId: roomId, mid: messageData.mid }, function (err, raw) { assert.ifError(err); assert.equal(raw, 'first chat message'); + setTimeout(done, 300); + }); + }); + }); + + it('should fail to send second message due to rate limit', function (done) { + var socketMock = { uid: fooUid }; + socketModules.chats.send(socketMock, { roomId: roomId, message: 'first chat message' }, function (err) { + assert.ifError(err); + socketModules.chats.send(socketMock, { roomId: roomId, message: 'first chat message' }, function (err) { + assert.equal(err.message, '[[error:too-many-messages]]'); + done(); + }); + }); + }); + + it('should return invalid-data error', function (done) { + socketModules.chats.getRaw({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.getRaw({ uid: fooUid }, { }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); done(); }); }); }); + it('should return not in room error', function (done) { + socketModules.chats.getRaw({ uid: 0 }, { roomId: roomId, mid: 1 }, function (err) { + assert.equal(err.message, '[[error:not-allowed]]'); + done(); + }); + }); + it('should notify offline users of message', function (done) { Messaging.notificationSendDelay = 100; @@ -143,6 +298,22 @@ describe('Messaging Library', function () { }); }); + it('should fail to get messages from room with invalid data', function (done) { + socketModules.chats.getMessages({ uid: null }, null, function (err, messages) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.getMessages({ uid: fooUid }, null, function (err, messages) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.getMessages({ uid: fooUid }, { uid: null }, function (err, messages) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.getMessages({ uid: fooUid }, { uid: 1, roomId: null }, function (err, messages) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + }); + }); + it('should get messages from room', function (done) { socketModules.chats.getMessages({ uid: fooUid }, { uid: fooUid, @@ -157,6 +328,23 @@ describe('Messaging Library', function () { }); }); + it('should fail to mark read with invalid data', function (done) { + socketModules.chats.markRead({ uid: null }, roomId, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.markRead({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + + it('should not error if user is not in room', function (done) { + socketModules.chats.markRead({ uid: herpUid }, roomId, function (err) { + assert.ifError(err); + done(); + }); + }); + it('should mark room read', function (done) { socketModules.chats.markRead({ uid: fooUid }, roomId, function (err) { assert.ifError(err); @@ -171,10 +359,39 @@ describe('Messaging Library', function () { }); }); + it('should fail to rename room with invalid data', function (done) { + socketModules.chats.renameRoom({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.renameRoom({ uid: fooUid }, { roomId: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.renameRoom({ uid: fooUid }, { roomId: roomId, newName: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + }); + it('should rename room', function (done) { socketModules.chats.renameRoom({ uid: fooUid }, { roomId: roomId, newName: 'new room name' }, function (err) { assert.ifError(err); + done(); + }); + }); + + it('should fail to load room with invalid-data', function (done) { + socketModules.chats.loadRoom({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.loadRoom({ uid: fooUid }, { roomId: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + it('should fail to load room if user is not in', function (done) { + socketModules.chats.loadRoom({ uid: 0 }, { roomId: roomId }, function (err) { + assert.equal(err.message, '[[error:not-allowed]]'); done(); }); }); @@ -198,6 +415,45 @@ describe('Messaging Library', function () { }); }); }); + + it('should fail to load recent chats with invalid data', function (done) { + socketModules.chats.getRecentChats({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.getRecentChats({ uid: fooUid }, { after: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.getRecentChats({ uid: fooUid }, { after: 0, uid: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + }); + + it('should load recent chats of user', function (done) { + socketModules.chats.getRecentChats({ uid: fooUid }, { after: 0, uid: fooUid }, function (err, data) { + assert.ifError(err); + assert(Array.isArray(data.rooms)); + done(); + }); + }); + + it('should fail to check if user has private chat with invalid data', function (done) { + socketModules.chats.hasPrivateChat({ uid: null }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.hasPrivateChat({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + + it('should check if user has private chat with another uid', function (done) { + socketModules.chats.hasPrivateChat({ uid: fooUid }, herpUid, function (err, roomId) { + assert.ifError(err); + assert(roomId); + done(); + }); + }); }); describe('edit/delete', function () { @@ -211,6 +467,26 @@ describe('Messaging Library', function () { }); }); + it('should fail to edit message with invalid data', function (done) { + socketModules.chats.edit({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.edit({ uid: fooUid }, { roomId: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.edit({ uid: fooUid }, { roomId: 1, message: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + }); + + it('should fail to edit message if not own message', function (done) { + socketModules.chats.edit({ uid: herpUid }, { mid: 5, roomId: roomId, message: 'message edited' }, function (err) { + assert.equal(err.message, '[[error:cant-edit-chat-message]]'); + done(); + }); + }); + it('should edit message', function (done) { socketModules.chats.edit({ uid: fooUid }, { mid: mid, roomId: roomId, message: 'message edited' }, function (err) { assert.ifError(err); @@ -222,6 +498,20 @@ describe('Messaging Library', function () { }); }); + it('should fail to delete message with invalid data', function (done) { + socketModules.chats.delete({ uid: fooUid }, null, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.delete({ uid: fooUid }, { roomId: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + socketModules.chats.delete({ uid: fooUid }, { roomId: 1, messageId: null }, function (err) { + assert.equal(err.message, '[[error:invalid-data]]'); + done(); + }); + }); + }); + }); + + it('should delete message', function (done) { socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) { assert.ifError(err); diff --git a/test/meta.js b/test/meta.js index 8b32e90070..a92f6c74f6 100644 --- a/test/meta.js +++ b/test/meta.js @@ -262,6 +262,21 @@ describe('meta', function () { }); + describe('sounds', function () { + var socketModules = require('../src/socket.io/modules'); + + it('should getUserMap', function (done) { + socketModules.sounds.getUserSoundMap({ uid: 1 }, null, function (err, data) { + assert.ifError(err); + assert(data.hasOwnProperty('chat-incoming')); + assert(data.hasOwnProperty('chat-outgoing')); + assert(data.hasOwnProperty('notification')); + done(); + }); + }); + }); + + after(function (done) { db.emptydb(done); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 5b186e7f35..bd7831ac82 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -5,181 +5,178 @@ * ATTENTION: testing db is flushed before every use! */ -(function (module) { - var async = require('async'); - var winston = require('winston'); - var path = require('path'); - var nconf = require('nconf'); - var url = require('url'); - var errorText; - - - nconf.file({ file: path.join(__dirname, '../../config.json') }); - nconf.defaults({ - base_dir: path.join(__dirname, '../..'), - themes_path: path.join(__dirname, '../../node_modules'), - upload_path: 'public/uploads', - views_dir: path.join(__dirname, '../../build/public/templates'), - relative_path: '', - }); - - if (!nconf.get('isCluster')) { - nconf.set('isPrimary', 'true'); - nconf.set('isCluster', 'false'); - } - - var dbType = nconf.get('database'); - var testDbConfig = nconf.get('test_database'); - var productionDbConfig = nconf.get(dbType); - - if (!testDbConfig) { - errorText = 'test_database is not defined'; - winston.info( - '\n===========================================================\n' + - 'Please, add parameters for test database in config.json\n' + - 'For example (redis):\n' + - '"test_database": {\n' + - ' "host": "127.0.0.1",\n' + - ' "port": "6379",\n' + - ' "password": "",\n' + - ' "database": "1"\n' + - '}\n' + - ' or (mongo):\n' + - '"test_database": {\n' + - ' "host": "127.0.0.1",\n' + - ' "port": "27017",\n' + - ' "password": "",\n' + - ' "database": "1\n' + - '}\n' + - ' or (mongo) in a replicaset\n' + - '"test_database": {\n' + - ' "host": "127.0.0.1,127.0.0.1,127.0.0.1",\n' + - ' "port": "27017,27018,27019",\n' + - ' "username": "",\n' + - ' "password": "",\n' + - ' "database": "nodebb_test"\n' + - '}\n' + - '===========================================================' - ); - winston.error(errorText); - throw new Error(errorText); - } - - if (testDbConfig.database === productionDbConfig.database && - testDbConfig.host === productionDbConfig.host && - testDbConfig.port === productionDbConfig.port) { - errorText = 'test_database has the same config as production db'; - winston.error(errorText); - throw new Error(errorText); - } - - nconf.set(dbType, testDbConfig); - - winston.info('database config'); - winston.info(dbType); - winston.info(testDbConfig); - - var db = require('../../src/database'); - - after(function (done) { - db.close(done); - }); - - before(function (done) { - this.timeout(30000); - var meta; - async.waterfall([ - function (next) { - db.init(next); - }, - function (next) { - db.emptydb(next); - }, - function (next) { - db.createIndices(next); - }, - function (next) { - winston.info('test_database flushed'); - meta = require('../../src/meta'); - setupDefaultConfigs(meta, next); - }, - function (next) { - meta.configs.init(next); - }, - function (next) { - db.initSessionStore(next); - }, - function (next) { - meta.dependencies.check(next); - }, - function (next) { - meta.config.postDelay = 0; - meta.config.initialPostDelay = 0; - meta.config.newbiePostDelay = 0; - - enableDefaultPlugins(next); - }, - function (next) { - meta.themes.set({ - type: 'local', - id: 'nodebb-theme-persona', - }, next); - }, - function (next) { - // nconf defaults, if not set in config - if (!nconf.get('sessionKey')) { - nconf.set('sessionKey', 'express.sid'); - } - // Parse out the relative_url and other goodies from the configured URL - var urlObject = url.parse(nconf.get('url')); - var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : ''; - nconf.set('base_url', urlObject.protocol + '//' + urlObject.host); - nconf.set('secure', urlObject.protocol === 'https:'); - nconf.set('use_port', !!urlObject.port); - nconf.set('relative_path', relativePath); - nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); - nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path'))); - - nconf.set('core_templates_path', path.join(__dirname, '../../src/views')); - nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); - nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path')); - nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json')); - nconf.set('bcrypt_rounds', 1); - - next(); - }, - function (next) { - var webserver = require('../../src/webserver'); - var sockets = require('../../src/socket.io'); - sockets.init(webserver.server); - - require('../../src/notifications').startJobs(); - require('../../src/user').startJobs(); - - webserver.listen(next); - }, - ], done); - }); - - function setupDefaultConfigs(meta, next) { - winston.info('Populating database with default configs, if not already set...\n'); - - var defaults = require(path.join(nconf.get('base_dir'), 'install/data/defaults.json')); - - meta.configs.setOnEmpty(defaults, next); - } - - function enableDefaultPlugins(callback) { - winston.info('Enabling default plugins\n'); - - var defaultEnabled = [ - 'nodebb-plugin-dbsearch', - ]; - - winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled); - - db.sortedSetAdd('plugins:active', [0], defaultEnabled, callback); - } - - module.exports = db; -}(module)); +var async = require('async'); +var winston = require('winston'); +var path = require('path'); +var nconf = require('nconf'); +var url = require('url'); +var errorText; + + +nconf.file({ file: path.join(__dirname, '../../config.json') }); +nconf.defaults({ + base_dir: path.join(__dirname, '../..'), + themes_path: path.join(__dirname, '../../node_modules'), + upload_path: 'public/uploads', + views_dir: path.join(__dirname, '../../build/public/templates'), + relative_path: '', +}); + +if (!nconf.get('isCluster')) { + nconf.set('isPrimary', 'true'); + nconf.set('isCluster', 'false'); +} + +var dbType = nconf.get('database'); +var testDbConfig = nconf.get('test_database'); +var productionDbConfig = nconf.get(dbType); + +if (!testDbConfig) { + errorText = 'test_database is not defined'; + winston.info( + '\n===========================================================\n' + + 'Please, add parameters for test database in config.json\n' + + 'For example (redis):\n' + + '"test_database": {\n' + + ' "host": "127.0.0.1",\n' + + ' "port": "6379",\n' + + ' "password": "",\n' + + ' "database": "1"\n' + + '}\n' + + ' or (mongo):\n' + + '"test_database": {\n' + + ' "host": "127.0.0.1",\n' + + ' "port": "27017",\n' + + ' "password": "",\n' + + ' "database": "1\n' + + '}\n' + + ' or (mongo) in a replicaset\n' + + '"test_database": {\n' + + ' "host": "127.0.0.1,127.0.0.1,127.0.0.1",\n' + + ' "port": "27017,27018,27019",\n' + + ' "username": "",\n' + + ' "password": "",\n' + + ' "database": "nodebb_test"\n' + + '}\n' + + '===========================================================' + ); + winston.error(errorText); + throw new Error(errorText); +} + +if (testDbConfig.database === productionDbConfig.database && + testDbConfig.host === productionDbConfig.host && + testDbConfig.port === productionDbConfig.port) { + errorText = 'test_database has the same config as production db'; + winston.error(errorText); + throw new Error(errorText); +} + +nconf.set(dbType, testDbConfig); + +winston.info('database config'); +winston.info(dbType); +winston.info(testDbConfig); + +var db = require('../../src/database'); +module.exports = db; + +after(function (done) { + db.close(done); +}); + +before(function (done) { + this.timeout(30000); + var meta; + async.waterfall([ + function (next) { + db.init(next); + }, + function (next) { + db.emptydb(next); + }, + function (next) { + db.createIndices(next); + }, + function (next) { + winston.info('test_database flushed'); + meta = require('../../src/meta'); + setupDefaultConfigs(meta, next); + }, + function (next) { + meta.configs.init(next); + }, + function (next) { + db.initSessionStore(next); + }, + function (next) { + meta.dependencies.check(next); + }, + function (next) { + meta.config.postDelay = 0; + meta.config.initialPostDelay = 0; + meta.config.newbiePostDelay = 0; + + enableDefaultPlugins(next); + }, + function (next) { + meta.themes.set({ + type: 'local', + id: 'nodebb-theme-persona', + }, next); + }, + function (next) { + // nconf defaults, if not set in config + if (!nconf.get('sessionKey')) { + nconf.set('sessionKey', 'express.sid'); + } + // Parse out the relative_url and other goodies from the configured URL + var urlObject = url.parse(nconf.get('url')); + var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : ''; + nconf.set('base_url', urlObject.protocol + '//' + urlObject.host); + nconf.set('secure', urlObject.protocol === 'https:'); + nconf.set('use_port', !!urlObject.port); + nconf.set('relative_path', relativePath); + nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567); + nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path'))); + + nconf.set('core_templates_path', path.join(__dirname, '../../src/views')); + nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates')); + nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path')); + nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json')); + nconf.set('bcrypt_rounds', 1); + + next(); + }, + function (next) { + var webserver = require('../../src/webserver'); + var sockets = require('../../src/socket.io'); + sockets.init(webserver.server); + + require('../../src/notifications').startJobs(); + require('../../src/user').startJobs(); + + webserver.listen(next); + }, + ], done); +}); + +function setupDefaultConfigs(meta, next) { + winston.info('Populating database with default configs, if not already set...\n'); + + var defaults = require(path.join(nconf.get('base_dir'), 'install/data/defaults.json')); + + meta.configs.setOnEmpty(defaults, next); +} + +function enableDefaultPlugins(callback) { + winston.info('Enabling default plugins\n'); + + var defaultEnabled = [ + 'nodebb-plugin-dbsearch', + ]; + + winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled); + + db.sortedSetAdd('plugins:active', [0], defaultEnabled, callback); +} From c364cebbe490489637432906d9989bb2d06349ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 20:20:53 -0400 Subject: [PATCH 115/263] more tests --- test/messaging.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/messaging.js b/test/messaging.js index a009d5ad21..5a9b0ac4a3 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -339,7 +339,7 @@ describe('Messaging Library', function () { }); it('should not error if user is not in room', function (done) { - socketModules.chats.markRead({ uid: herpUid }, roomId, function (err) { + socketModules.chats.markRead({ uid: herpUid }, 10, function (err) { assert.ifError(err); done(); }); @@ -511,6 +511,13 @@ describe('Messaging Library', function () { }); }); + it('should fail to delete message if not owner', function (done) { + socketModules.chats.delete({ uid: herpUid }, { messageId: mid, roomId: roomId }, function (err) { + assert.equal(err.message, '[[error:cant-delete-chat-message]]'); + done(); + }); + }); + it('should delete message', function (done) { socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) { From 59387540c757658fa6509a4767a8bd683897a92e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 20:26:54 -0400 Subject: [PATCH 116/263] style --- src/messaging/rooms.js | 48 ++++++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index ead62fee4d..0d7e1d2b1f 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -9,26 +9,33 @@ var plugins = require('../plugins'); module.exports = function (Messaging) { Messaging.getRoomData = function (roomId, callback) { - db.getObject('chat:room:' + roomId, function (err, data) { - if (err || !data) { - return callback(err || new Error('[[error:no-chat-room]]')); - } - modifyRoomData([data]); - callback(null, data); - }); + async.waterfall([ + function (next) { + db.getObject('chat:room:' + roomId, next); + }, + function (data, next) { + if (!data) { + return callback(new Error('[[error:no-chat-room]]')); + } + modifyRoomData([data]); + next(null, data); + }, + ], callback); }; Messaging.getRoomsData = function (roomIds, callback) { var keys = roomIds.map(function (roomId) { return 'chat:room:' + roomId; }); - db.getObjects(keys, function (err, roomData) { - if (err) { - return callback(err); + async.waterfall([ + function (next) { + db.getObjects(keys, next); + }, + function (roomData, next) { + modifyRoomData(roomData); + next(null, roomData); } - modifyRoomData(roomData); - callback(null, roomData); - }); + ], callback); }; function modifyRoomData(rooms) { @@ -96,13 +103,14 @@ module.exports = function (Messaging) { }; Messaging.isRoomOwner = function (uid, roomId, callback) { - db.getObjectField('chat:room:' + roomId, 'owner', function (err, owner) { - if (err) { - return callback(err); - } - - callback(null, parseInt(uid, 10) === parseInt(owner, 10)); - }); + async.waterfall([ + function (next) { + db.getObjectField('chat:room:' + roomId, 'owner', next); + }, + function (owner, next) { + next(null, parseInt(uid, 10) === parseInt(owner, 10)); + }, + ], callback); }; Messaging.addUsersToRoom = function (uid, uids, roomId, callback) { From 9af252b8998932fdced017f380df1b7f1dcea7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 21:01:55 -0400 Subject: [PATCH 117/263] notifications tests --- src/messaging/rooms.js | 2 +- src/notifications.js | 145 ++++++++++++++++++++------------------- src/socket.io/helpers.js | 31 +++++---- test/notifications.js | 96 +++++++++++++++++++++++++- 4 files changed, 186 insertions(+), 88 deletions(-) diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index 0d7e1d2b1f..41f09c5125 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -34,7 +34,7 @@ module.exports = function (Messaging) { function (roomData, next) { modifyRoomData(roomData); next(null, roomData); - } + }, ], callback); }; diff --git a/src/notifications.js b/src/notifications.js index 931e0ad293..d5505f88dd 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -29,7 +29,7 @@ Notifications.get = function (nid, callback) { }; Notifications.getMultiple = function (nids, callback) { - if (!nids.length) { + if (!Array.isArray(nids) || !nids.length) { return setImmediate(callback, null, []); } var keys = nids.map(function (nid) { @@ -106,50 +106,47 @@ Notifications.findRelated = function (mergeIds, set, callback) { db.getObjectsFields(keys, ['mergeId'], next); }, - ], function (err, sets) { - if (err) { - return callback(err); - } - - sets = sets.map(function (set) { - return set.mergeId; - }); + function (sets, next) { + sets = sets.map(function (set) { + return set.mergeId; + }); - callback(null, _nids.filter(function (nid, idx) { - return mergeIds.indexOf(sets[idx]) !== -1; - })); - }); + next(null, _nids.filter(function (nid, idx) { + return mergeIds.indexOf(sets[idx]) !== -1; + })); + }, + ], callback); }; Notifications.create = function (data, callback) { if (!data.nid) { - return callback(new Error('no-notification-id')); + return callback(new Error('[[error:no-notification-id]]')); } data.importance = data.importance || 5; - db.getObject('notifications:' + data.nid, function (err, oldNotification) { - if (err) { - return callback(err); - } - - if (oldNotification) { - if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) { - return callback(null, null); + async.waterfall([ + function (next) { + db.getObject('notifications:' + data.nid, next); + }, + function (oldNotification, next) { + if (oldNotification) { + if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) { + return callback(null, null); + } } - } - - var now = Date.now(); - data.datetime = now; - async.parallel([ - function (next) { - db.sortedSetAdd('notifications', now, data.nid, next); - }, - function (next) { - db.setObject('notifications:' + data.nid, data, next); - }, - ], function (err) { - callback(err, data); - }); - }); + var now = Date.now(); + data.datetime = now; + async.parallel([ + function (next) { + db.sortedSetAdd('notifications', now, data.nid, next); + }, + function (next) { + db.setObject('notifications:' + data.nid, data, next); + }, + ], function (err) { + next(err, data); + }); + }, + ], callback); }; Notifications.push = function (notification, uids, callback) { @@ -233,25 +230,31 @@ function pushToUids(uids, notification, callback) { Notifications.pushGroup = function (notification, groupName, callback) { callback = callback || function () {}; - groups.getMembers(groupName, 0, -1, function (err, members) { - if (err || !Array.isArray(members) || !members.length) { - return callback(err); - } + async.waterfall([ + function (next) { + groups.getMembers(groupName, 0, -1, next); + }, + function (members, next) { + if (!Array.isArray(members) || !members.length) { + return callback(); + } - Notifications.push(notification, members, callback); - }); + Notifications.push(notification, members, next); + }, + ], callback); }; Notifications.pushGroups = function (notification, groupNames, callback) { callback = callback || function () {}; - groups.getMembersOfGroups(groupNames, function (err, groupMembers) { - if (err) { - return callback(err); - } - - var members = _.unique(_.flatten(groupMembers)); - Notifications.push(notification, members, callback); - }); + async.waterfall([ + function (next) { + groups.getMembersOfGroups(groupNames, next); + }, + function (groupMembers, next) { + var members = _.unique(_.flatten(groupMembers)); + Notifications.push(notification, members, next); + }, + ], callback); }; Notifications.rescind = function (nid, callback) { @@ -261,13 +264,7 @@ Notifications.rescind = function (nid, callback) { async.apply(db.sortedSetRemove, 'notifications', nid), async.apply(db.delete, 'notifications:' + nid), ], function (err) { - if (err) { - winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message); - } else { - winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"'); - } - - callback(err, nid); + callback(err); }); }; @@ -284,18 +281,22 @@ Notifications.markUnread = function (nid, uid, callback) { if (!parseInt(uid, 10) || !nid) { return callback(); } + async.waterfall([ + function (next) { + db.getObject('notifications:' + nid, next); + }, + function (notification, next) { + if (!notification) { + return callback(new Error('[[error:no-notification]]')); + } + notification.datetime = notification.datetime || Date.now(); - db.getObject('notifications:' + nid, function (err, notification) { - if (err || !notification) { - return callback(err || new Error('[[error:no-notification]]')); - } - notification.datetime = notification.datetime || Date.now(); - - async.parallel([ - async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid), - async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid), - ], callback); - }); + async.parallel([ + async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid), + async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid), + ], next); + }, + ], callback); }; Notifications.markReadMultiple = function (nids, uid, callback) { @@ -377,9 +378,9 @@ Notifications.markAllRead = function (uid, callback) { Notifications.prune = function (callback) { callback = callback || function () {}; - var week = 604800000; + var week = 604800000; - var cutoffTime = Date.now() - week; + var cutoffTime = Date.now() - week; async.waterfall([ function (next) { @@ -390,7 +391,7 @@ Notifications.prune = function (callback) { return callback(); } - var keys = nids.map(function (nid) { + var keys = nids.map(function (nid) { return 'notifications:' + nid; }); diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 241937d5fb..9eedab2fb0 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -169,21 +169,26 @@ SocketHelpers.sendNotificationToTopicOwner = function (tid, fromuid, command, no }; SocketHelpers.rescindUpvoteNotification = function (pid, fromuid) { - var nid = 'upvote:post:' + pid + ':uid:' + fromuid; - notifications.rescind(nid); - - posts.getPostField(pid, 'uid', function (err, uid) { + var uid; + async.waterfall([ + function (next) { + notifications.rescind('upvote:post:' + pid + ':uid:' + fromuid, next); + }, + function (next) { + posts.getPostField(pid, 'uid', next); + }, + function (_uid, next) { + uid = _uid; + user.notifications.getUnreadCount(uid, next); + }, + function (count, next) { + websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); + next(); + }, + ], function (err) { if (err) { - return winston.error(err); + winston.error(err); } - - user.notifications.getUnreadCount(uid, function (err, count) { - if (err) { - return winston.error(err); - } - - websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); - }); }); }; diff --git a/test/notifications.js b/test/notifications.js index c5a22a6d2c..fbbf4fd49b 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -24,11 +24,19 @@ describe('Notifications', function () { }); }); + it('should fail to create notification without a nid', function (done) { + notifications.create({}, function (err) { + assert.equal(err.message, '[[error:no-notification-id]]'); + done(); + }); + }); + it('should create a notification', function (done) { notifications.create({ bodyShort: 'bodyShort', nid: 'notification_id', path: '/notification/path', + pid: 1, }, function (err, _notification) { notification = _notification; assert.ifError(err); @@ -45,6 +53,29 @@ describe('Notifications', function () { }); }); + it('should return null if pid is same and importance is lower', function (done) { + notifications.create({ + bodyShort: 'bodyShort', + nid: 'notification_id', + path: '/notification/path', + pid: 1, + importance: 1, + }, function (err, notification) { + assert.ifError(err); + assert.strictEqual(notification, null); + done(); + }); + }); + + it('should get empty array', function (done) { + notifications.getMultiple(null, function (err, data) { + assert.ifError(err); + assert(Array.isArray(data)); + assert.equal(data.length, 0); + done(); + }); + }); + it('should get notifications', function (done) { notifications.getMultiple([notification.nid], function (err, notificationsData) { assert.ifError(err); @@ -55,6 +86,19 @@ describe('Notifications', function () { }); }); + it('should do nothing', function (done) { + notifications.push(null, [], function (err) { + assert.ifError(err); + notifications.push({ nid: null }, [], function (err) { + assert.ifError(err); + notifications.push(notification, [], function (err) { + assert.ifError(err); + done(); + }); + }); + }); + }); + it('should push a notification to uid', function (done) { notifications.push(notification, [uid], function (err) { assert.ifError(err); @@ -94,6 +138,16 @@ describe('Notifications', function () { }); }); + it('should not mark anything with invalid uid or nid', function (done) { + socketNotifications.markRead({ uid: null }, null, function (err) { + assert.ifError(err); + socketNotifications.markRead({ uid: uid }, null, function (err) { + assert.ifError(err); + done(); + }); + }); + }); + it('should mark a notification read', function (done) { socketNotifications.markRead({ uid: uid }, notification.nid, function (err) { assert.ifError(err); @@ -109,6 +163,23 @@ describe('Notifications', function () { }); }); + it('should not mark anything with invalid uid or nid', function (done) { + socketNotifications.markUnread({ uid: null }, null, function (err) { + assert.ifError(err); + socketNotifications.markUnread({ uid: uid }, null, function (err) { + assert.ifError(err); + done(); + }); + }); + }); + + it('should error if notification does not exist', function (done) { + socketNotifications.markUnread({ uid: uid }, 123123, function (err) { + assert.equal(err.message, '[[error:no-notification]]'); + done(); + }); + }); + it('should mark a notification unread', function (done) { socketNotifications.markUnread({ uid: uid }, notification.nid, function (err) { assert.ifError(err); @@ -143,6 +214,13 @@ describe('Notifications', function () { }); }); + it('should not do anything', function (done) { + socketNotifications.markAllRead({ uid: 1000 }, null, function (err) { + assert.ifError(err); + done(); + }); + }); + it('should link to the first unread post in a watched topic', function (done) { var categories = require('../src/categories'); var topics = require('../src/topics'); @@ -256,9 +334,23 @@ describe('Notifications', function () { bodyShort: 'bodyShort', nid: 'tobedeleted', path: '/notification/path', - }, function (err) { + }, function (err, notification) { assert.ifError(err); - notifications.prune(done); + notifications.prune(function (err) { + assert.ifError(err); + var week = 604800000; + db.sortedSetAdd('notifications', Date.now() - (2 * week), notification.nid, function (err) { + assert.ifError(err); + notifications.prune(function (err) { + assert.ifError(err); + notifications.get(notification.nid, function (err, data) { + assert.ifError(err); + assert(!data); + done(); + }); + }); + }); + }); }); }); From c5c755fbb0bcdebde136147d47718e22e509101c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sat, 20 May 2017 22:30:12 -0400 Subject: [PATCH 118/263] more notification tests --- src/user/follow.js | 4 +- src/user/notifications.js | 603 +++++++++++++++++++------------------- test/notifications.js | 122 ++++++++ 3 files changed, 432 insertions(+), 297 deletions(-) diff --git a/src/user/follow.js b/src/user/follow.js index fe3dc0931d..d2056065b4 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -57,7 +57,9 @@ module.exports = function (User) { ], next); } }, - ], callback); + ], function (err) { + callback(err); + }); } User.getFollowing = function (uid, start, stop, callback) { diff --git a/src/user/notifications.js b/src/user/notifications.js index 318794fb20..953caeb2c2 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -10,16 +10,17 @@ var meta = require('../meta'); var notifications = require('../notifications'); var privileges = require('../privileges'); -(function (UserNotifications) { - UserNotifications.get = function (uid, callback) { - if (!parseInt(uid, 10)) { - return callback(null, { read: [], unread: [] }); - } - getNotifications(uid, 0, 9, function (err, notifications) { - if (err) { - return callback(err); - } +var UserNotifications = module.exports; +UserNotifications.get = function (uid, callback) { + if (!parseInt(uid, 10)) { + return callback(null, { read: [], unread: [] }); + } + async.waterfall([ + function (next) { + getNotifications(uid, 0, 9, next); + }, + function (notifications, next) { notifications.read = notifications.read.filter(Boolean); notifications.unread = notifications.unread.filter(Boolean); @@ -28,326 +29,336 @@ var privileges = require('../privileges'); notifications.read.length = maxNotifs - notifications.unread.length; } - callback(null, notifications); - }); - }; - - function filterNotifications(nids, filter, callback) { - if (!filter) { - return setImmediate(callback, null, nids); - } - async.waterfall([ - function (next) { - var keys = nids.map(function (nid) { - return 'notifications:' + nid; - }); - db.getObjectsFields(keys, ['nid', 'type'], next); - }, - function (notifications, next) { - nids = notifications.filter(function (notification) { - return notification && notification.nid && notification.type === filter; - }).map(function (notification) { - return notification.nid; - }); - next(null, nids); - }, - ], callback); - } - - UserNotifications.getAll = function (uid, filter, callback) { - var nids; - async.waterfall([ - function (next) { - async.parallel({ - unread: function (next) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, -1, next); - }, - read: function (next) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, -1, next); - }, - }, next); - }, - function (results, next) { - nids = results.unread.concat(results.read); - db.isSortedSetMembers('notifications', nids, next); - }, - function (exists, next) { - var deleteNids = []; - - nids = nids.filter(function (nid, index) { - if (!nid || !exists[index]) { - deleteNids.push(nid); - } - return nid && exists[index]; - }); - - deleteUserNids(deleteNids, uid, next); - }, - function (next) { - filterNotifications(nids, filter, next); - }, - ], callback); - }; - - function deleteUserNids(nids, uid, callback) { - callback = callback || function () {}; - if (!nids.length) { - return setImmediate(callback); - } - async.parallel([ - function (next) { - db.sortedSetRemove('uid:' + uid + ':notifications:read', nids, next); - }, - function (next) { - db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); - }, - ], function (err) { - callback(err); - }); - } + next(null, notifications); + }, + ], callback); +}; - function getNotifications(uid, start, stop, callback) { - async.parallel({ - unread: function (next) { - getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next); - }, - read: function (next) { - getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next); - }, - }, callback); +function filterNotifications(nids, filter, callback) { + if (!filter) { + return setImmediate(callback, null, nids); } - - function getNotificationsFromSet(set, read, uid, start, stop, callback) { - async.waterfall([ - function (next) { - db.getSortedSetRevRange(set, start, stop, next); - }, - function (nids, next) { - if (!Array.isArray(nids) || !nids.length) { - return callback(null, []); + async.waterfall([ + function (next) { + var keys = nids.map(function (nid) { + return 'notifications:' + nid; + }); + db.getObjectsFields(keys, ['nid', 'type'], next); + }, + function (notifications, next) { + nids = notifications.filter(function (notification) { + return notification && notification.nid && notification.type === filter; + }).map(function (notification) { + return notification.nid; + }); + next(null, nids); + }, + ], callback); +} + +UserNotifications.getAll = function (uid, filter, callback) { + var nids; + async.waterfall([ + function (next) { + async.parallel({ + unread: function (next) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, -1, next); + }, + read: function (next) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, -1, next); + }, + }, next); + }, + function (results, next) { + nids = results.unread.concat(results.read); + db.isSortedSetMembers('notifications', nids, next); + }, + function (exists, next) { + var deleteNids = []; + + nids = nids.filter(function (nid, index) { + if (!nid || !exists[index]) { + deleteNids.push(nid); } + return nid && exists[index]; + }); - UserNotifications.getNotifications(nids, uid, next); - }, - ], callback); + deleteUserNids(deleteNids, uid, next); + }, + function (next) { + filterNotifications(nids, filter, next); + }, + ], callback); +}; + +function deleteUserNids(nids, uid, callback) { + callback = callback || function () {}; + if (!nids.length) { + return setImmediate(callback); } - - UserNotifications.getNotifications = function (nids, uid, callback) { - var notificationData = []; - async.waterfall([ - function (next) { - async.parallel({ - notifications: function (next) { - notifications.getMultiple(nids, next); - }, - hasRead: function (next) { - db.isSortedSetMembers('uid:' + uid + ':notifications:read', nids, next); - }, - }, next); - }, - function (results, next) { - var deletedNids = []; - notificationData = results.notifications.filter(function (notification, index) { - if (!notification || !notification.nid) { - deletedNids.push(nids[index]); - } - if (notification) { - notification.read = results.hasRead[index]; - notification.readClass = !notification.read ? 'unread' : ''; - } - - return notification && notification.path; - }); - - deleteUserNids(deletedNids, uid, next); - }, - function (next) { - notifications.merge(notificationData, next); - }, - ], callback); - }; - - UserNotifications.getDailyUnread = function (uid, callback) { - var yesterday = Date.now() - (1000 * 60 * 60 * 24); // Approximate, can be more or less depending on time changes, makes no difference really. - - db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, function (err, nids) { - if (err) { - return callback(err); - } - + async.parallel([ + function (next) { + db.sortedSetRemove('uid:' + uid + ':notifications:read', nids, next); + }, + function (next) { + db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next); + }, + ], function (err) { + callback(err); + }); +} + +function getNotifications(uid, start, stop, callback) { + async.parallel({ + unread: function (next) { + getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next); + }, + read: function (next) { + getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next); + }, + }, callback); +} + +function getNotificationsFromSet(set, read, uid, start, stop, callback) { + async.waterfall([ + function (next) { + db.getSortedSetRevRange(set, start, stop, next); + }, + function (nids, next) { if (!Array.isArray(nids) || !nids.length) { return callback(null, []); } - UserNotifications.getNotifications(nids, uid, callback); - }); - }; - - UserNotifications.getUnreadCount = function (uid, callback) { - if (!parseInt(uid, 10)) { - return callback(null, 0); - } + UserNotifications.getNotifications(nids, uid, next); + }, + ], callback); +} + +UserNotifications.getNotifications = function (nids, uid, callback) { + var notificationData = []; + async.waterfall([ + function (next) { + async.parallel({ + notifications: function (next) { + notifications.getMultiple(nids, next); + }, + hasRead: function (next) { + db.isSortedSetMembers('uid:' + uid + ':notifications:read', nids, next); + }, + }, next); + }, + function (results, next) { + var deletedNids = []; + notificationData = results.notifications.filter(function (notification, index) { + if (!notification || !notification.nid) { + deletedNids.push(nids[index]); + } + if (notification) { + notification.read = results.hasRead[index]; + notification.readClass = !notification.read ? 'unread' : ''; + } - // Collapse any notifications with identical mergeIds - async.waterfall([ - async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':notifications:unread', 0, 99), - async.apply(notifications.filterExists), - function (nids, next) { - var keys = nids.map(function (nid) { - return 'notifications:' + nid; - }); - - db.getObjectsFields(keys, ['mergeId'], next); - }, - function (mergeIds, next) { - mergeIds = mergeIds.map(function (set) { - return set.mergeId; - }); - - next(null, mergeIds.reduce(function (count, mergeId, idx, arr) { - // A missing (null) mergeId means that notification is counted separately. - if (mergeId === null || idx === arr.indexOf(mergeId)) { - count += 1; - } - - return count; - }, 0)); - }, - ], callback); - }; - - UserNotifications.getUnreadByField = function (uid, field, values, callback) { - db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) { - if (err) { - return callback(err); - } + return notification && notification.path; + }); + deleteUserNids(deletedNids, uid, next); + }, + function (next) { + notifications.merge(notificationData, next); + }, + ], callback); +}; + +UserNotifications.getDailyUnread = function (uid, callback) { + var yesterday = Date.now() - (1000 * 60 * 60 * 24); // Approximate, can be more or less depending on time changes, makes no difference really. + + async.waterfall([ + function (next) { + db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, next); + }, + function (nids, next) { if (!Array.isArray(nids) || !nids.length) { return callback(null, []); } + UserNotifications.getNotifications(nids, uid, next); + }, + ], callback); +}; + +UserNotifications.getUnreadCount = function (uid, callback) { + if (!parseInt(uid, 10)) { + return callback(null, 0); + } + + + async.waterfall([ + function (next) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next); + }, + function (nids, next) { + notifications.filterExists(nids, next); + }, + function (nids, next) { var keys = nids.map(function (nid) { return 'notifications:' + nid; }); - db.getObjectsFields(keys, ['nid', field], function (err, notifications) { - if (err) { - return callback(err); + db.getObjectsFields(keys, ['mergeId'], next); + }, + function (mergeIds, next) { + // Collapse any notifications with identical mergeIds + mergeIds = mergeIds.map(function (set) { + return set.mergeId; + }); + + next(null, mergeIds.reduce(function (count, mergeId, idx, arr) { + // A missing (null) mergeId means that notification is counted separately. + if (mergeId === null || idx === arr.indexOf(mergeId)) { + count += 1; } - values = values.map(function () { return values.toString(); }); - nids = notifications.filter(function (notification) { - return notification && notification[field] && values.indexOf(notification[field].toString()) !== -1; - }).map(function (notification) { - return notification.nid; - }); + return count; + }, 0)); + }, + ], callback); +}; + +UserNotifications.getUnreadByField = function (uid, field, values, callback) { + var nids; + async.waterfall([ + function (next) { + db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next); + }, + function (_nids, next) { + nids = _nids; + if (!Array.isArray(nids) || !nids.length) { + return callback(null, []); + } - callback(null, nids); + var keys = nids.map(function (nid) { + return 'notifications:' + nid; }); - }); - }; - UserNotifications.deleteAll = function (uid, callback) { - if (!parseInt(uid, 10)) { - return callback(); - } - async.parallel([ - function (next) { - db.delete('uid:' + uid + ':notifications:unread', next); - }, - function (next) { - db.delete('uid:' + uid + ':notifications:read', next); - }, - ], callback); - }; - - UserNotifications.sendTopicNotificationToFollowers = function (uid, topicData, postData) { - var followers; - async.waterfall([ - function (next) { - db.getSortedSetRange('followers:' + uid, 0, -1, next); - }, - function (followers, next) { - if (!Array.isArray(followers) || !followers.length) { - return; - } - privileges.categories.filterUids('read', topicData.cid, followers, next); - }, - function (_followers, next) { - followers = _followers; - if (!followers.length) { - return; - } + db.getObjectsFields(keys, ['nid', field], next); + }, + function (notifications, next) { + values = values.map(function () { return values.toString(); }); + nids = notifications.filter(function (notification) { + return notification && notification[field] && values.indexOf(notification[field].toString()) !== -1; + }).map(function (notification) { + return notification.nid; + }); - var title = topicData.title; - if (title) { - title = S(title).decodeHTMLEntities().s; - } + next(null, nids); + }, + ], callback); +}; - notifications.create({ - type: 'new-topic', - bodyShort: '[[notifications:user_posted_topic, ' + postData.user.username + ', ' + title + ']]', - bodyLong: postData.content, - pid: postData.pid, - path: '/post/' + postData.pid, - nid: 'tid:' + postData.tid + ':uid:' + uid, - tid: postData.tid, - from: uid, - }, next); - }, - ], function (err, notification) { - if (err) { - return winston.error(err); +UserNotifications.deleteAll = function (uid, callback) { + if (!parseInt(uid, 10)) { + return callback(); + } + async.parallel([ + function (next) { + db.delete('uid:' + uid + ':notifications:unread', next); + }, + function (next) { + db.delete('uid:' + uid + ':notifications:read', next); + }, + ], callback); +}; + +UserNotifications.sendTopicNotificationToFollowers = function (uid, topicData, postData) { + var followers; + async.waterfall([ + function (next) { + db.getSortedSetRange('followers:' + uid, 0, -1, next); + }, + function (followers, next) { + if (!Array.isArray(followers) || !followers.length) { + return; + } + privileges.categories.filterUids('read', topicData.cid, followers, next); + }, + function (_followers, next) { + followers = _followers; + if (!followers.length) { + return; } - if (notification) { - notifications.push(notification, followers); + var title = topicData.title; + if (title) { + title = S(title).decodeHTMLEntities().s; } - }); - }; - UserNotifications.sendWelcomeNotification = function (uid, callback) { - callback = callback || function () {}; - if (!meta.config.welcomeNotification) { - return callback(); + notifications.create({ + type: 'new-topic', + bodyShort: '[[notifications:user_posted_topic, ' + postData.user.username + ', ' + title + ']]', + bodyLong: postData.content, + pid: postData.pid, + path: '/post/' + postData.pid, + nid: 'tid:' + postData.tid + ':uid:' + uid, + tid: postData.tid, + from: uid, + }, next); + }, + ], function (err, notification) { + if (err) { + return winston.error(err); } - var path = meta.config.welcomeLink ? meta.config.welcomeLink : '#'; + if (notification) { + notifications.push(notification, followers); + } + }); +}; - notifications.create({ - bodyShort: meta.config.welcomeNotification, - path: path, - nid: 'welcome_' + uid, - }, function (err, notification) { - if (err || !notification) { - return callback(err); - } +UserNotifications.sendWelcomeNotification = function (uid, callback) { + callback = callback || function () {}; + if (!meta.config.welcomeNotification) { + return callback(); + } - notifications.push(notification, [uid], callback); - }); - }; - - UserNotifications.sendNameChangeNotification = function (uid, username) { - notifications.create({ - bodyShort: '[[user:username_taken_workaround, ' + username + ']]', - image: 'brand:logo', - nid: 'username_taken:' + uid, - datetime: Date.now(), - }, function (err, notification) { - if (!err && notification) { - notifications.push(notification, uid); - } - }); - }; - - UserNotifications.pushCount = function (uid) { - var websockets = require('./../socket.io'); - UserNotifications.getUnreadCount(uid, function (err, count) { - if (err) { - return winston.error(err.stack); + var path = meta.config.welcomeLink ? meta.config.welcomeLink : '#'; + + async.waterfall([ + function (next) { + notifications.create({ + bodyShort: meta.config.welcomeNotification, + path: path, + nid: 'welcome_' + uid, + }, next); + }, + function (notification, next) { + if (!notification) { + return next(); } + notifications.push(notification, [uid], next); + }, + ], callback); +}; + +UserNotifications.sendNameChangeNotification = function (uid, username) { + notifications.create({ + bodyShort: '[[user:username_taken_workaround, ' + username + ']]', + image: 'brand:logo', + nid: 'username_taken:' + uid, + datetime: Date.now(), + }, function (err, notification) { + if (!err && notification) { + notifications.push(notification, uid); + } + }); +}; + +UserNotifications.pushCount = function (uid) { + var websockets = require('./../socket.io'); + UserNotifications.getUnreadCount(uid, function (err, count) { + if (err) { + return winston.error(err.stack); + } - websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); - }); - }; -}(exports)); + websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); + }); +}; diff --git a/test/notifications.js b/test/notifications.js index fbbf4fd49b..d2585e4029 100644 --- a/test/notifications.js +++ b/test/notifications.js @@ -5,7 +5,11 @@ var assert = require('assert'); var async = require('async'); var db = require('./mocks/databasemock'); +var meta = require('../src/meta'); var user = require('../src/user'); +var topics = require('../src/topics'); +var categories = require('../src/categories'); +var groups = require('../src/groups'); var notifications = require('../src/notifications'); var socketNotifications = require('../src/socket.io/notifications'); @@ -14,6 +18,7 @@ describe('Notifications', function () { var notification; before(function (done) { + groups.resetCache(); user.create({ username: 'poster' }, function (err, _uid) { if (err) { return done(err); @@ -329,6 +334,123 @@ describe('Notifications', function () { }); }); + it('should return empty with falsy uid', function (done) { + user.notifications.get(0, function (err, data) { + assert.ifError(err); + assert.equal(data.read.length, 0); + assert.equal(data.unread.length, 0); + done(); + }); + }); + + it('should get all notifications and filter', function (done) { + var nid = 'willbefiltered'; + notifications.create({ + bodyShort: 'bodyShort', + nid: nid, + path: '/notification/path', + type: 'post', + }, function (err, notification) { + assert.ifError(err); + notifications.push(notification, [uid], function (err) { + assert.ifError(err); + setTimeout(function () { + user.notifications.getAll(uid, 'post', function (err, nids) { + assert.ifError(err); + assert.notEqual(nids.indexOf(nid), -1); + done(); + }); + }, 1500); + }); + }); + }); + + it('should not get anything if notifications does not exist', function (done) { + user.notifications.getNotifications(['doesnotexistnid1', 'doesnotexistnid2'], uid, function (err, data) { + assert.ifError(err); + assert.deepEqual(data, []); + done(); + }); + }); + + it('should get daily notifications', function (done) { + user.notifications.getDailyUnread(uid, function (err, data) { + assert.ifError(err); + assert.equal(data[0].nid, 'willbefiltered'); + done(); + }); + }); + + it('should return 0 for falsy uid', function (done) { + user.notifications.getUnreadCount(0, function (err, count) { + assert.ifError(err); + assert.equal(count, 0); + done(); + }); + }); + + it('should not do anything if uid is falsy', function (done) { + user.notifications.deleteAll(0, function (err) { + assert.ifError(err); + done(); + }); + }); + + it('should send notification to followers of user when he posts', function (done) { + var followerUid; + async.waterfall([ + function (next) { + user.create({ username: 'follower' }, next); + }, + function (_followerUid, next) { + followerUid = _followerUid; + user.follow(followerUid, uid, next); + }, + function (next) { + categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + }, next); + }, + function (category, next) { + topics.post({ + uid: uid, + cid: category.cid, + title: 'Test Topic Title', + content: 'The content of test topic', + }, next); + }, + function (data, next) { + setTimeout(next, 1100); + }, + function (next) { + user.notifications.getAll(followerUid, '', next); + }, + ], function (err, data) { + assert.ifError(err); + assert(data); + done(); + }); + }); + + it('should send welcome notification', function (done) { + meta.config.welcomeNotification = 'welcome to the forums'; + user.notifications.sendWelcomeNotification(uid, function (err) { + assert.ifError(err); + user.notifications.sendWelcomeNotification(uid, function (err) { + assert.ifError(err); + setTimeout(function () { + user.notifications.getAll(uid, '', function (err, data) { + meta.config.welcomeNotification = ''; + assert.ifError(err); + assert.notEqual(data.indexOf('welcome_' + uid), -1); + done(); + }); + }, 1100); + }); + }); + }); + it('should prune notifications', function (done) { notifications.create({ bodyShort: 'bodyShort', From 57c5e0b554a02201ff7decb23fc37016e56e8b96 Mon Sep 17 00:00:00 2001 From: "Misty (Bot)" Date: Sun, 21 May 2017 09:23:08 +0000 Subject: [PATCH 119/263] Latest translations and fallbacks --- public/language/es/admin/extend/plugins.json | 12 ++--- public/language/th/category.json | 28 +++++------ public/language/th/email.json | 48 +++++++++---------- public/language/th/error.json | 32 ++++++------- public/language/th/uploads.json | 8 ++-- public/language/th/user.json | 2 +- .../tr/admin/appearance/customise.json | 8 ++-- .../language/tr/admin/appearance/skins.json | 8 ++-- .../language/tr/admin/appearance/themes.json | 12 ++--- public/language/tr/admin/general/sounds.json | 6 +-- .../language/tr/admin/manage/categories.json | 40 ++++++++-------- public/language/tr/admin/manage/users.json | 6 +-- public/language/tr/email.json | 8 ++-- public/language/tr/modules.json | 2 +- public/language/tr/notifications.json | 2 +- 15 files changed, 111 insertions(+), 111 deletions(-) diff --git a/public/language/es/admin/extend/plugins.json b/public/language/es/admin/extend/plugins.json index dba3e65d87..c2414d3a25 100644 --- a/public/language/es/admin/extend/plugins.json +++ b/public/language/es/admin/extend/plugins.json @@ -7,7 +7,7 @@ "none-active": "No hay Plug-ins activos", "find-plugins": "Buscar Plug-in", - "plugin-search": "Plug-in de Búsqueda", + "plugin-search": "Buscar", "plugin-search-placeholder": "Búscando Plug-in", "reorder-plugins": "Re-ordenar Plug-ins", "order-active": "Ordenar Plug-ins Activos", @@ -18,14 +18,14 @@ "order.explanation": "Los plug-in son cargados en el orden especificado, de arriba a abajo.", "plugin-item.themes": "Temas", - "plugin-item.deactivate": "Desactivado", - "plugin-item.activate": "Activado", + "plugin-item.deactivate": "Desactivar", + "plugin-item.activate": "Activar", "plugin-item.install": "Instalar", "plugin-item.uninstall": "Desinstalar", "plugin-item.settings": "Configuraciones", "plugin-item.installed": "Instalados", "plugin-item.latest": "Ultimos", - "plugin-item.upgrade": "Actualizado", + "plugin-item.upgrade": "Actualizar", "plugin-item.more-info": "Para mas información:", "plugin-item.unknown": "Desconocido", "plugin-item.unknown-explanation": "El estado de este plug-in no puede determinsarse, posiblemente es debido a un error de configuración.", @@ -35,9 +35,9 @@ "alert.upgraded": "Plug-in Actualizado", "alert.installed": "Plug-in Instalado", "alert.uninstalled": "Plug-in Desinstalado", - "alert.activate-success": "Por favor reinicia NodeBB para activar el plug-in por completo", + "alert.activate-success": "Por favor reiniciá NodeBB para activar el plug-in por completo", "alert.deactivate-success": "Plugin successfully deactivated", - "alert.upgrade-success": "Please reload your NodeBB to fully upgrade this plugin", + "alert.upgrade-success": "Por favor recargá NodeBB para actualizar el plug-in por completo", "alert.install-success": "Plugin successfully installed, please activate the plugin.", "alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.", "alert.suggest-error": "

NodeBB could not reach the package manager, proceed with installation of latest version?

Server returned (%1): %2
", diff --git a/public/language/th/category.json b/public/language/th/category.json index 1802bb7f1b..8daa641ba5 100644 --- a/public/language/th/category.json +++ b/public/language/th/category.json @@ -1,20 +1,20 @@ { - "category": "Category", - "subcategories": "Subcategories", - "new_topic_button": "กระทู้", - "guest-login-post": "เข้าสู่ระบบเพื่อโพส", + "category": "หมวดหมู่", + "subcategories": "หมวดหมู่ย่อย", + "new_topic_button": "ตั้งกระทู้", + "guest-login-post": "เข้าสู่ระบบเพื่อโพสต์", "no_topics": "ยังไม่มีกระทู้ในหมวดนี้
โพสต์กระทู้แรก?", "browsing": "เรียกดู", "no_replies": "ยังไม่มีใครตอบ", - "no_new_posts": "No new posts.", - "share_this_category": "แชร์ Category นี้", - "watch": "Watch", + "no_new_posts": "ไม่มีกระทู้ใหม่", + "share_this_category": "แชร์หมวดนี้", + "watch": "ตามดู", "ignore": "ไม่ต้องสนใจอีก", - "watching": "Watching", - "ignoring": "Ignoring", - "watching.description": "Show topics in unread", - "ignoring.description": "Do not show topics in unread", - "watch.message": "You are now watching updates from this category and all subcategories", - "ignore.message": "You are now ignoring updates from this category and all subcategories", - "watched-categories": "Watched categories" + "watching": "กำลังตามดู", + "ignoring": "เมินเฉย", + "watching.description": "แสดงกระทู้ที่ยังไม่ได้อ่าน", + "ignoring.description": "ไม่แสดงกระทู้ที่ยังไม่ได้อ่าน", + "watch.message": "ตอนนี้คุณกำลังตามดูอัพเดทจากกระทู้หมวดนี้และหมวดหมู่ย่อยทั้งหมดในนี้", + "ignore.message": "ตอนนี้คุณกำลังเมินเฉยต่ออัพเดทจากกระทู้หมวดนี้และหมวดหมู่ย่อยทั้งหมดในนี้", + "watched-categories": "หมวดหมู่ที่ดูแล้ว" } \ No newline at end of file diff --git a/public/language/th/email.json b/public/language/th/email.json index f6e3cd7e5b..609703602e 100644 --- a/public/language/th/email.json +++ b/public/language/th/email.json @@ -1,40 +1,40 @@ { - "password-reset-requested": "Password Reset Requested - %1!", + "password-reset-requested": "ส่งคำขอตั้งค่ารหัสผ่านใหม่แล้ว - %1!", "welcome-to": "ยินดีต้อนรับ %1", - "invite": "Invitation from %1", + "invite": "คำเชิญจาก %1", "greeting_no_name": "สวัสดี", "greeting_with_name": "สวัสดี %1", "welcome.text1": "ขอบคุณที่ลงทะเบียนกับ %1", - "welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.", - "welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.", + "welcome.text2": "เพื่อให้การบัญชีของคุณใช้งานได้อย่างเสร็จสมบูรณ์ เราจำเป็นต้องยืนยันว่าคุณเป็นเจ้าของที่แท้จริงอีเมล์ที่ใช้สมัครสมาชิก", + "welcome.text3": "ผู้ดูแลระบบได้ทำการยอมรับการสมัครสมาชิกของคุณแล้ว คุณสามารถเข้าสู่ระบบด้วย ชื่อผู้ใช้/รหัสผ่าน ได้แล้วตอนนี้", "welcome.cta": "กดตรงนี้เพื่อยืนยันอีเมลของคุณ", - "invitation.text1": "%1 has invited you to join %2", - "invitation.ctr": "Click here to create your account.", - "reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.", + "invitation.text1": "%1 ได้เชิญคุณให้เข้าร่วม %2", + "invitation.ctr": "คลิกที่นี่เพื่อสร้างบัญชีของคุณ", + "reset.text1": "เราได้รับคำร้องให้ตั้งค่ารหัสผ่านใหม่ของคุณ อาจจะเป็นเพราะว่าคุณลืมรหัสผ่านและได้ทำการส่งคำขอเข้ามา หากไม่ใช่ กรุณาเพิกเฉยต่ออีเมล์นี้และไม่ต้องดำเนินการใดๆทั้งสิ้น", "reset.text2": "เพื่อดำเนินการตั้งรหัสผ่านใหม่ต่อไป, โปรดกดที่ลิ้งค์นี้:", "reset.cta": "กดตรงนี้เพื่อตั้งรหัสผ่านใหม่", - "reset.notify.subject": "Password successfully changed", - "reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.", - "reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.", + "reset.notify.subject": "ตั้งค่ารหัสผ่านใหม่เรียบร้อยแล้ว", + "reset.notify.text1": "เรากำลังแจ้งคุณว่าตอน %1 รหัสผ่านของคุณถูกเปลี่ยนเรียบร้อยแล้ว", + "reset.notify.text2": "หากคุณไม่ได้เป็นคนอนุญาตสิ่งนี้ กรุณาแจ้งไปยังผู้ดูแลระบบโดยทันที", "digest.notifications": "คุณมีข้อความแจ้งเตือนที่ยังไม่ได้อ่านจาก %1:", "digest.latest_topics": "หัวข้อสนทนาล่าสุดจาก %1", "digest.cta": "กดตรงนี้เพื่อเข้าดู %1", - "digest.unsub.info": "This digest was sent to you due to your subscription settings.", - "digest.no_topics": "There have been no active topics in the past %1", - "digest.day": "day", - "digest.week": "week", - "digest.month": "month", - "digest.subject": "Digest for %1", - "notif.chat.subject": "New chat message received from %1", + "digest.unsub.info": "คำชี้แจงถูกส่งไปให้คุณแล้ว เนื่องมาจากการตั้งค่าสมาชิกของคุณ", + "digest.no_topics": "ไม่มีกระทู้ใดๆเลยใน %1 ที่ผ่านมา", + "digest.day": "วัน", + "digest.week": "สัปดาห์", + "digest.month": "เดือน", + "digest.subject": "คำชี้แจงสำหรับ %1", + "notif.chat.subject": "ได้รับข้อความแชทใหม่จาก %1", "notif.chat.cta": "กดตรงนี้เพื่อกลับไปยังบทสนทนา", - "notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.", - "notif.post.cta": "Click here to read the full topic", - "notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.", + "notif.chat.unsub.info": "การแจ้งเตือนแชทนี้ถูกส่งไปหาคุณเนื่องจากการตั้งค่าสมาชิกของคุณ", + "notif.post.cta": "คลิกที่นี่เพื่ออ่านกระทู้ฉบับเต็ม", + "notif.post.unsub.info": "การแจ้งเตือนกระทู้นี้ถูกส่งไปยังคุณเนื่องการตั้งค่าสมาชิกของคุณ", "test.text1": "นี่คืออีเมลทดสอบเพื่อยืนยันว่าระบบอีเมลมีการตั้งค่าที่ถูกต้องสำหรับ NodeBB ของคุณ", "unsub.cta": "กดตรงนี้เพื่อเปลี่ยนแปลงการตั้งค่า", - "banned.subject": "You have been banned from %1", - "banned.text1": "The user %1 has been banned from %2.", - "banned.text2": "This ban will last until %1.", - "banned.text3": "This is the reason why you have been banned:", + "banned.subject": "คุณถูกแบนจาก %1 แล้ว", + "banned.text1": "ผู้ใช้ %1 ได้ถูกแบนจาก %2", + "banned.text2": "การแบนนี้จะใช้เวลาจนถึง %1", + "banned.text3": "นี่คือเหตุผลที่ทำไมคุณถึงถูกแบน", "closing": "ขอบคุณ!" } \ No newline at end of file diff --git a/public/language/th/error.json b/public/language/th/error.json index 4d5cf25465..96c3592123 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -2,7 +2,7 @@ "invalid-data": "ข้อมูลไม่ถูกต้อง", "not-logged-in": "คุณยังไม่ได้ลงชื่อเข้าระบบ", "account-locked": "บัญชีของคุณถูกระงับการใช้งานชั่วคราว", - "search-requires-login": "Searching requires an account - please login or register.", + "search-requires-login": "\"ฟังก์ชั่นการค้นหา\" ต้องการบัญชีผู้ใช้ กรุณาเข้าสู่ระบบหรือสมัครสมาชิก", "invalid-cid": "Category ID ไม่ถูกต้อง", "invalid-tid": "Topic ID ไม่ถูกต้อง", "invalid-pid": "Post ID ไม่ถูกต้อง", @@ -14,35 +14,35 @@ "invalid-password": "รหัสผ่านไม่ถูกต้อง", "invalid-username-or-password": "กรุณาระบุชื่อผู้ใช้และรหัสผ่าน", "invalid-search-term": "ข้อความค้นหาไม่ถูกต้อง", - "csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again", - "invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2", + "csrf-invalid": "เราไม่สามารถนำท่านเข้าสู่ระบบได้ เหมือนกับว่าเซสชั่นหมดอายุแล้ว กรุณาลองใหม่อีกครั้ง", + "invalid-pagination-value": "หมายเลขหน้าไม่ถูกต้อง จำเป็นต้องเป็นตัวเลขอย่างน้อย %1 และอย่างมาก %2", "username-taken": "ชื่อผู้ใช้นี้มีการใช้แล้ว", "email-taken": "อีเมลนี้มีการใช้แล้ว", "email-not-confirmed": "ยังไม่มีการยืนยันอีเมลของคุณ, โปรดกดยืนยันอีเมลของคุณตรงนี้", - "email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.", - "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.", + "email-not-confirmed-chat": "คุณไม่สามารถแชทได้จนกว่าอีเมล์ของคุณจะได้รับการยืนยัน กรุณาคลิกที่นี่เพื่อยืนยันอีกมเมล์ของคุณ", + "email-not-confirmed-email-sent": "อีเมล์ของคุณยังไม่ได้รับการยืนยัน กรุณาเช็คกล่องข้อความในอีเมล์เพื่อกดยืนยัน", "no-email-to-confirm": "Forum นี้ต้องการการยืนยันอีเมล กรุณากดที่นี่เพื่อระบุอีเมล", "email-confirm-failed": "เราไม่สามารถยืนยันอีเมลของคุณ ณ ขณะนี้ กรุณาลองใหม่อีกครั้งภายหลัง", - "confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.", - "sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.", + "confirm-email-already-sent": "อีเมล์ยืนยันตัวตนถูกส่งไปยังคุณเรียบร้อยแล้ว กรุณารอ %1 นาที(s) ก่อนการตัดสินใจส่งอีกครั้ง", + "sendmail-not-found": "ไม่พบการประมวลผลสำหรับการส่งอีเมล์ กรุณาตรวจสอบให้แน่ใจว่าได้มีการติดตั้งโปรแกรมการประมวลผลแล้วโดยผู้ใช้ที่กำลังใช้ NodeBB", "username-too-short": "ชื่อบัญชีผู้ใช้ สั้นเกินไป", "username-too-long": "ชื่อบัญชีผู้ใช้ ยาวเกินไป", - "password-too-long": "Password too long", - "user-banned": "User banned", - "user-banned-reason": "Sorry, this account has been banned (Reason: %1)", - "user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)", - "user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post", - "blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.", - "ban-expiry-missing": "Please provide an end date for this ban", + "password-too-long": "รหัสผ่านยาวเกินไป", + "user-banned": "ผู้ใช้ได้รับการแบน", + "user-banned-reason": "ขออภัย บัญชีผู้ใช้นี้ได้รับการแบน (เหตุผล : %1)", + "user-banned-reason-until": "ขออภัย บัญชีผู้ใช้นี้ได้รับการแบนจนถึง %1 (เหตุผล : %2)", + "user-too-new": "ขออภัย คุณจำเป็นต้องรอ %1 วินาที(s) ก่อนการสร้างกระทู้แรกของคุณ", + "blacklisted-ip": "ขออภัย IP Address ของคุณถูกแบนจากชุมชนนี้ หากคุณคิดว่านี่เป็นเออเร่อของระบบ กรุณาติดต่อผู้ดูแลระบบ", + "ban-expiry-missing": "กรุณาระบุวันสิ้นสุดสำหรับการแบนในครั้งนี้", "no-category": "ยังไม่มี Category นี้", "no-topic": "ยังไม่มี Topic นี้", "no-post": "ยังไม่มี Post นี้", "no-group": "ยังไม่มี Group นี้", "no-user": "ยังไม่มีผู้ใช้งานนี้", - "no-teaser": "Teaser does not exist", + "no-teaser": "ยังไม่มีทีเซอร์นี้", "no-privileges": "คุณมีสิทธิ์ไม่เพียงพอที่จะทำรายการนี้", "category-disabled": "Category นี้ถูกปิดการใช้งานแล้ว", - "topic-locked": "Topic Locked", + "topic-locked": "กระทู้ถูกล็อก", "post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting", "post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting", "post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting", diff --git a/public/language/th/uploads.json b/public/language/th/uploads.json index 1622cb5693..b43c5bcae6 100644 --- a/public/language/th/uploads.json +++ b/public/language/th/uploads.json @@ -1,6 +1,6 @@ { - "uploading-file": "Uploading the file...", - "select-file-to-upload": "Select a file to upload!", - "upload-success": "File uploaded successfully!", - "maximum-file-size": "Maximum %1 kb" + "uploading-file": "กำลังอัพโหลดไฟล์ ...", + "select-file-to-upload": "กรุณาเลือกไฟล์ที่จะอัพโหลด", + "upload-success": "อัพโหลดไฟล์เรียบร้อยแล้ว", + "maximum-file-size": "มากที่สุดได้ %1 kb" } \ No newline at end of file diff --git a/public/language/th/user.json b/public/language/th/user.json index 538c59c7f7..a426a45a2e 100644 --- a/public/language/th/user.json +++ b/public/language/th/user.json @@ -104,7 +104,7 @@ "open_links_in_new_tab": "เปิดลิงค์ในแท็บใหม่", "enable_topic_searching": "เปิดใช้การค้นหาแบบ In-Topic", "topic_search_help": "หากเปิดใช้งาน, \"การค้นหาภายในกระทู้\" จะแทนที่ระบบ \"การค้นหาจากค่าเริ่มต้นของเบราเซอร์\" และจะทำให้คุณค้นหาข้อมูลต่างๆภายในกระทู้ได้ แทนที่จะเป็นการหาแค่สิ่งที่แสดงบนหน้าจอเท่านั้น", - "delay_image_loading": "Delay Image Loading", + "delay_image_loading": "การโหลดรูปภาพช้าลง", "image_load_delay_help": "หากเปิดใช้งาน, รูปภาพในกระทู้จะไม่โหลดจนกว่าจะมีการเลื่อนไปดู", "scroll_to_my_post": "After posting a reply, show the new post", "follow_topics_you_reply_to": "ดูกระทู้ที่คุณตอบ", diff --git a/public/language/tr/admin/appearance/customise.json b/public/language/tr/admin/appearance/customise.json index 83e506e854..afcc788881 100644 --- a/public/language/tr/admin/appearance/customise.json +++ b/public/language/tr/admin/appearance/customise.json @@ -1,9 +1,9 @@ { "custom-css": "Özel CSS", - "custom-css.description": "Enter your own CSS declarations here, which will be applied after all other styles.", - "custom-css.enable": "Özel CSS Aktif", + "custom-css.description": "Özel CSS kodlarınızı bu alana girin.", + "custom-css.enable": "Özel CSS Etkinleştir", "custom-header": "Özel Header", - "custom-header.description": "Enter custom HTML here (ex. JavaScript, Meta Tags, etc.), which will be appended to the <head> section of your forum's markup.", - "custom-header.enable": "Enable Custom Header" + "custom-header.description": "Forumunuzun biçimlendirmesini sağlayacak <head> bölümüne eklenecek özel HTML'yi (ör. JavaScript, Meta Etiketler vb.) Girin.", + "custom-header.enable": "Özel Header'ı Etkinleştir" } \ No newline at end of file diff --git a/public/language/tr/admin/appearance/skins.json b/public/language/tr/admin/appearance/skins.json index 57638271e7..335dd89547 100644 --- a/public/language/tr/admin/appearance/skins.json +++ b/public/language/tr/admin/appearance/skins.json @@ -2,8 +2,8 @@ "loading": "Deriler yükleniyor...", "homepage": "Anasayfa", "select-skin": "Deri Seç", - "current-skin": "Current Skin", - "skin-updated": "Skin Updated", - "applied-success": "%1 skin was succesfully applied", - "revert-success": "Skin reverted to base colours" + "current-skin": "Mevcut Deri", + "skin-updated": "Deri Güncellendi", + "applied-success": "%1 deri başarıyla uygulandı", + "revert-success": "Deri taban renkleri geri döndürüldü" } \ No newline at end of file diff --git a/public/language/tr/admin/appearance/themes.json b/public/language/tr/admin/appearance/themes.json index 9cdc64769e..e8df4d06b8 100644 --- a/public/language/tr/admin/appearance/themes.json +++ b/public/language/tr/admin/appearance/themes.json @@ -2,10 +2,10 @@ "checking-for-installed": "Yüklü temalar kontrol ediliyor...", "homepage": "Anasayfa", "select-theme": "Tema Seç", - "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" + "current-theme": "Geçerli Tema", + "no-themes": "Yüklü tema bulunamadı", + "revert-confirm": "Varsayılan NodeBB temasını geri yüklemek istediğinizden emin misiniz?", + "theme-changed": "Tema Değiştirildi", + "revert-success": "NodeBB'nin varsayılan temasına başarıyla geri dönüş yaptınız.", + "restart-to-activate": "Temayı tamamen aktif hale getirebilmek için NodeBB'yi yeniden başlat" } \ No newline at end of file diff --git a/public/language/tr/admin/general/sounds.json b/public/language/tr/admin/general/sounds.json index b2c52aa9f1..be203d3582 100644 --- a/public/language/tr/admin/general/sounds.json +++ b/public/language/tr/admin/general/sounds.json @@ -2,8 +2,8 @@ "notifications": "Bildiriler", "chat-messages": "Sohbet Mesajları", "play-sound": "Oynat", - "incoming-message": "Incoming Message", - "outgoing-message": "Outgoing Message", + "incoming-message": "Gelen İleti", + "outgoing-message": "Giden İleti", "upload-new-sound": "Yeni Ses Yükle", - "saved": "Settings Saved" + "saved": "Ayarlar Kaydedildi" } \ No newline at end of file diff --git a/public/language/tr/admin/manage/categories.json b/public/language/tr/admin/manage/categories.json index d5cb9e7729..930d9590bf 100644 --- a/public/language/tr/admin/manage/categories.json +++ b/public/language/tr/admin/manage/categories.json @@ -1,30 +1,30 @@ { "settings": "Kategori Ayarları", - "privileges": "Privileges", + "privileges": "İzinler", "name": "Kategori Adı", - "description": "Category Description", - "bg-color": "Background Colour", + "description": "Kategori Açıklama", + "bg-color": "Arkaplan Rengi", "text-color": "Yazı Rengi", "bg-image-size": "Arkaplan Görseli Boyutu", "custom-class": "Özel Sınıf", "num-recent-replies": "# of Recent Replies", - "ext-link": "External Link", + "ext-link": "Harici Bağlantı", "upload-image": "Görsel Yükle", - "delete-image": "Remove", - "category-image": "Category Image", + "delete-image": "Sil", + "category-image": "Kategori Görseli", "parent-category": "Parent Category", "optional-parent-category": "(Optional) Parent Category", - "parent-category-none": "(None)", + "parent-category-none": "(Hiçbiri)", "copy-settings": "Copy Settings From", "optional-clone-settings": "(Optional) Clone Settings From Category", "purge": "Purge Category", - "enable": "Enable", - "disable": "Disable", - "edit": "Edit", + "enable": "Etkinleştir", + "disable": "Devredışı", + "edit": "Düzenle", - "select-category": "Select Category", + "select-category": "Kategori Seç", "set-parent-category": "Set Parent Category", "privileges.description": "You can configure the access control privileges for this category in this section. Privileges can be granted on a per-user or a per-group basis. You can add a new user to this table by searching for them in the form below.", @@ -32,27 +32,27 @@ "privileges.section-viewing": "Viewing Privileges", "privileges.section-posting": "Posting Privileges", "privileges.section-moderation": "Moderation Privileges", - "privileges.section-user": "User", - "privileges.search-user": "Add User", + "privileges.section-user": "Kullanıcı", + "privileges.search-user": "Kullanıcı Ekle", "privileges.no-users": "No user-specific privileges in this category.", - "privileges.section-group": "Group", - "privileges.group-private": "This group is private", - "privileges.search-group": "Add Group", + "privileges.section-group": "Grup", + "privileges.group-private": "Bu grup gizlidir", + "privileges.search-group": "Grup Ekle", "privileges.copy-to-children": "Copy to Children", "privileges.copy-from-category": "Copy from Category", "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", - "analytics.back": "Back to Categories List", + "analytics.back": "Kategori listesine geri dön", "analytics.title": "Analytics for \"%1\" category", "analytics.pageviews-hourly": "Figure 1 – Hourly page views for this category", "analytics.pageviews-daily": "Figure 2 – Daily page views for this category", "analytics.topics-daily": "Figure 3 – Daily topics created in this category", "analytics.posts-daily": "Figure 4 – Daily posts made in this category", - "alert.created": "Created", + "alert.created": "Yaratıldı", "alert.create-success": "Category successfully created!", "alert.none-active": "You have no active categories.", - "alert.create": "Create a Category", + "alert.create": "Bir Kategori Yarat", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", "alert.confirm-purge": "

Do you really want to purge this category \"%1\"?

Warning! All topics and posts in this category will be purged!

Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category temporarily, you'll want to \"disable\" the category instead.

", "alert.purge-success": "Category purged!", @@ -61,7 +61,7 @@ "alert.updated": "Updated Categories", "alert.updated-success": "Category IDs %1 successfully updated.", "alert.upload-image": "Kategori görseli yükle", - "alert.find-user": "Find a User", + "alert.find-user": "Bir Kullanıcı Ara", "alert.user-search": "Search for a user here...", "alert.find-group": "Find a Group", "alert.group-search": "Search for a group here..." diff --git a/public/language/tr/admin/manage/users.json b/public/language/tr/admin/manage/users.json index 0f1a1c7b8d..6cacd0e9b9 100644 --- a/public/language/tr/admin/manage/users.json +++ b/public/language/tr/admin/manage/users.json @@ -2,7 +2,7 @@ "users": "Kullanıcılar", "edit": "Düzenle", "make-admin": "Yönetici Yap", - "remove-admin": "Remove Admin", + "remove-admin": "Yöneticiliği Sil", "validate-email": "Validate Email", "send-validation-email": "Send Validation Email", "password-reset-email": "Send Password Reset Email", @@ -10,7 +10,7 @@ "temp-ban": "Ban User(s) Temporarily", "unban": "Unban User(s)", "reset-lockout": "Reset Lockout", - "reset-flags": "Reset Flags", + "reset-flags": "Bayrakları Sıfırla", "delete": "Delete User(s)", "purge": "Delete User(s) and Content", "download-csv": "Download CSV", @@ -24,7 +24,7 @@ "pills.top-rep": "Most Reputation", "pills.inactive": "Inactive", "pills.flagged": "Most Flagged", - "pills.banned": "Banned", + "pills.banned": "Yasaklandı", "pills.search": "User Search", "search.username": "By User Name", diff --git a/public/language/tr/email.json b/public/language/tr/email.json index 4cf508e292..5c1233f451 100644 --- a/public/language/tr/email.json +++ b/public/language/tr/email.json @@ -32,9 +32,9 @@ "notif.post.unsub.info": "Bu yazı bildirimi size abonelik ayarlarınız nedeni ile gönderilmiştir.", "test.text1": "Bu ileti NodeBB e-posta ayarlarınızın doğru çalışıp çalışmadığını kontrol etmek için gönderildi.", "unsub.cta": "Buraya tıklayarak ayarlarınızı değiştirebilirsiniz.", - "banned.subject": "You have been banned from %1", - "banned.text1": "The user %1 has been banned from %2.", - "banned.text2": "This ban will last until %1.", - "banned.text3": "This is the reason why you have been banned:", + "banned.subject": "%1 'den yasaklandınız", + "banned.text1": "%1 kullanıcısı %2 'den yasaklandı.", + "banned.text2": "Bu yasak %1 'e kadar sürecek.", + "banned.text3": "Yasaklanmanın nedeni:", "closing": "Teşekkürler!" } \ No newline at end of file diff --git a/public/language/tr/modules.json b/public/language/tr/modules.json index a96b07be46..0ec408ba31 100644 --- a/public/language/tr/modules.json +++ b/public/language/tr/modules.json @@ -20,7 +20,7 @@ "chat.three_months": "3 Ay", "chat.delete_message_confirm": "Bu mesajı silmek istediğinden emin misin?", "chat.add-users-to-room": "Odaya Kullanıcı Ekle", - "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.confirm-chat-with-dnd-user": "Bu kullanıcı durumunu rahatsız etmeyin olarak ayarladı. Hala onunla sohbet etmek istiyor musun?", "composer.compose": "Yaz", "composer.show_preview": "Önizleme Göster", "composer.hide_preview": "Önizleme Sakla", diff --git a/public/language/tr/notifications.json b/public/language/tr/notifications.json index b4e317f224..dce29e6f2b 100644 --- a/public/language/tr/notifications.json +++ b/public/language/tr/notifications.json @@ -40,7 +40,7 @@ "user_started_following_you_multiple": "%1 ve %2 kişi daha seni takip etmeye başladı.", "new_register": "%1 kayıt olma isteği gönderdi.", "new_register_multiple": "Beklemede %1 kayıt olma isteği bulunmaktadır.", - "flag_assigned_to_you": "Flag %1 has been assigned to you", + "flag_assigned_to_you": "Bayrak %1 size devredildi", "email-confirmed": "E-posta onaylandı", "email-confirmed-message": "E-postanızı onaylandığınız için teşekkürler. Hesabınız tamamen aktive edildi.", "email-confirm-error-message": "E-posta adresinizi onaylarken bir hata oluştu. Kodunuz geçersiz ya da eski olabilir.", From a4ba88dc60a7b809b6c83644568779d6560e0f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 21 May 2017 19:45:25 -0400 Subject: [PATCH 120/263] more feed tests --- src/controllers/404.js | 24 ++-- src/routes/feeds.js | 262 +++++++++++++++++++---------------------- test/feeds.js | 119 +++++++++++++++++++ 3 files changed, 254 insertions(+), 151 deletions(-) create mode 100644 test/feeds.js diff --git a/src/controllers/404.js b/src/controllers/404.js index bc4e2e1d00..44dcf59174 100644 --- a/src/controllers/404.js +++ b/src/controllers/404.js @@ -30,18 +30,20 @@ exports.handle404 = function (req, res) { } meta.errors.log404(req.path.replace(/^\/api/, '') || ''); - res.status(404); - - var path = String(req.path || ''); - - if (res.locals.isAPI) { - return res.json({ path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]' }); - } - var middleware = require('../middleware'); - middleware.buildHeader(req, res, function () { - res.render('404', { path: validator.escape(path), title: '[[global:404.title]]' }); - }); + exports.send404(req, res); } else { res.status(404).type('txt').send('Not found'); } }; + +exports.send404 = function (req, res) { + res.status(404); + var path = String(req.path || ''); + if (res.locals.isAPI) { + return res.json({ path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]' }); + } + var middleware = require('../middleware'); + middleware.buildHeader(req, res, function () { + res.render('404', { path: validator.escape(path), title: '[[global:404.title]]' }); + }); +}; diff --git a/src/routes/feeds.js b/src/routes/feeds.js index 5e3d75c34d..3171babbf6 100644 --- a/src/routes/feeds.js +++ b/src/routes/feeds.js @@ -12,6 +12,7 @@ var categories = require('../categories'); var meta = require('../meta'); var helpers = require('../controllers/helpers'); var privileges = require('../privileges'); +var controllers404 = require('../controllers/404.js'); module.exports = function (app, middleware) { app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic); @@ -25,10 +26,9 @@ module.exports = function (app, middleware) { app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag); }; - function generateForTopic(req, res, callback) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return callback(); + return controllers404.send404(req, res); } var tid = req.params.topic_id; @@ -45,65 +45,59 @@ function generateForTopic(req, res, callback) { }, next); }, function (results, next) { - if (!results.topic) { - return callback(); + if (!results.topic || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { + return controllers404.send404(req, res); } - if (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted) { - return callback(); - } - if (!results.privileges.read || !results.privileges['topics:read']) { + if (!results.privileges['topics:read']) { return helpers.notAllowed(req, res); } userPrivileges = results.privileges; topics.getTopicWithPosts(results.topic, 'tid:' + tid + ':posts', req.uid, 0, 25, false, next); }, - ], function (err, topicData) { - if (err) { - return callback(err); - } - - topics.modifyPostsByPrivilege(topicData, userPrivileges); - - var description = topicData.posts.length ? topicData.posts[0].content : ''; - var image_url = topicData.posts.length ? topicData.posts[0].picture : ''; - var author = topicData.posts.length ? topicData.posts[0].username : ''; - - var feed = new rss({ - title: topicData.title, - description: description, - feed_url: nconf.get('url') + '/topic/' + tid + '.rss', - site_url: nconf.get('url') + '/topic/' + topicData.slug, - image_url: image_url, - author: author, - ttl: 60, - }); - var dateStamp; - - if (topicData.posts.length > 0) { - feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString(); - } - - topicData.posts.forEach(function (postData) { - if (!postData.deleted) { - dateStamp = new Date(parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10)).toUTCString(); - - feed.item({ - title: 'Reply to ' + topicData.title + ' on ' + dateStamp, - description: postData.content, - url: nconf.get('url') + '/post/' + postData.pid, - author: postData.user ? postData.user.username : '', - date: dateStamp, - }); + function (topicData) { + topics.modifyPostsByPrivilege(topicData, userPrivileges); + + var description = topicData.posts.length ? topicData.posts[0].content : ''; + var image_url = topicData.posts.length ? topicData.posts[0].picture : ''; + var author = topicData.posts.length ? topicData.posts[0].username : ''; + + var feed = new rss({ + title: topicData.title, + description: description, + feed_url: nconf.get('url') + '/topic/' + tid + '.rss', + site_url: nconf.get('url') + '/topic/' + topicData.slug, + image_url: image_url, + author: author, + ttl: 60, + }); + var dateStamp; + + if (topicData.posts.length > 0) { + feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString(); } - }); - sendFeed(feed, res); - }); + topicData.posts.forEach(function (postData) { + if (!postData.deleted) { + dateStamp = new Date(parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10)).toUTCString(); + + feed.item({ + title: 'Reply to ' + topicData.title + ' on ' + dateStamp, + description: postData.content, + url: nconf.get('url') + '/post/' + postData.pid, + author: postData.user ? postData.user.username : '', + date: dateStamp, + }); + } + }); + + sendFeed(feed, res); + }, + ], callback); } function generateForUserTopics(req, res, callback) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return callback(); + return controllers404.send404(req, res); } var userslug = req.params.userslug; @@ -118,24 +112,21 @@ function generateForUserTopics(req, res, callback) { } user.getUserFields(uid, ['uid', 'username'], next); }, - ], function (err, userData) { - if (err) { - return callback(err); - } - - generateForTopics({ - uid: req.uid, - title: 'Topics by ' + userData.username, - description: 'A list of topics that are posted by ' + userData.username, - feed_url: '/user/' + userslug + '/topics.rss', - site_url: '/user/' + userslug + '/topics', - }, 'uid:' + userData.uid + ':topics', req, res, callback); - }); + function (userData, next) { + generateForTopics({ + uid: req.uid, + title: 'Topics by ' + userData.username, + description: 'A list of topics that are posted by ' + userData.username, + feed_url: '/user/' + userslug + '/topics.rss', + site_url: '/user/' + userslug + '/topics', + }, 'uid:' + userData.uid + ':topics', req, res, next); + }, + ], callback); } function generateForCategory(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return next(); + return controllers404.send404(req, res); } var cid = req.params.category_id; @@ -169,17 +160,15 @@ function generateForCategory(req, res, next) { site_url: '/category/' + results.category.cid, }, results.category.topics, next); }, - ], function (err, feed) { - if (err) { - return next(err); - } - sendFeed(feed, res); - }); + function (feed) { + sendFeed(feed, res); + }, + ], next); } function generateForRecent(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return next(); + return controllers404.send404(req, res); } generateForTopics({ uid: req.uid, @@ -192,7 +181,7 @@ function generateForRecent(req, res, next) { function generateForPopular(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return next(); + return controllers404.send404(req, res); } var terms = { daily: 'day', @@ -215,12 +204,10 @@ function generateForPopular(req, res, next) { site_url: '/popular/' + (req.params.term || 'daily'), }, topics, next); }, - ], function (err, feed) { - if (err) { - return next(err); - } - sendFeed(feed, res); - }); + function (feed) { + sendFeed(feed, res); + }, + ], next); } function generateForTopics(options, set, req, res, next) { @@ -233,12 +220,10 @@ function generateForTopics(options, set, req, res, next) { function (data, next) { generateTopicsFeed(options, data.topics, next); }, - ], function (err, feed) { - if (err) { - return next(err); - } - sendFeed(feed, res); - }); + function (feed) { + sendFeed(feed, res); + }, + ], next); } function generateTopicsFeed(feedOptions, feedTopics, callback) { @@ -254,7 +239,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) { feed.pubDate = new Date(parseInt(feedTopics[0].lastposttime, 10)).toUTCString(); } - async.map(feedTopics, function (topicData, next) { + async.each(feedTopics, function (topicData, next) { var feedItem = { title: topicData.title, url: nconf.get('url') + '/topic/' + topicData.slug, @@ -272,83 +257,80 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) { return next(err); } if (!mainPost) { - return next(null, feedItem); + feed.item(feedItem); + return next(); } feedItem.description = mainPost.content; feedItem.author = mainPost.user.username; - next(null, feedItem); + feed.item(feedItem); + next(); }); - }, function (err, feedItems) { - if (err) { - return callback(err); - } - feedItems.forEach(function (feedItem) { - if (feedItem) { - feed.item(feedItem); - } - }); - callback(null, feed); + }, function (err) { + callback(err, feed); }); } function generateForRecentPosts(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return next(); + return controllers404.send404(req, res); } - posts.getRecentPosts(req.uid, 0, 19, 'month', function (err, posts) { - if (err) { - return next(err); - } - - var feed = generateForPostsFeed({ - title: 'Recent Posts', - description: 'A list of recent posts', - feed_url: '/recentposts.rss', - site_url: '/recentposts', - }, posts); - - sendFeed(feed, res); - }); + async.waterfall([ + function (next) { + posts.getRecentPosts(req.uid, 0, 19, 'month', next); + }, + function (posts) { + var feed = generateForPostsFeed({ + title: 'Recent Posts', + description: 'A list of recent posts', + feed_url: '/recentposts.rss', + site_url: '/recentposts', + }, posts); + + sendFeed(feed, res); + }, + ], next); } function generateForCategoryRecentPosts(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return next(); + return controllers404.send404(req, res); } var cid = req.params.category_id; - async.parallel({ - privileges: function (next) { - privileges.categories.get(cid, req.uid, next); - }, - category: function (next) { - categories.getCategoryData(cid, next); - }, - posts: function (next) { - categories.getRecentReplies(cid, req.uid, 20, next); + async.waterfall([ + function (next) { + async.parallel({ + privileges: function (next) { + privileges.categories.get(cid, req.uid, next); + }, + category: function (next) { + categories.getCategoryData(cid, next); + }, + posts: function (next) { + categories.getRecentReplies(cid, req.uid, 20, next); + }, + }, next); }, - }, function (err, results) { - if (err) { - return next(err); - } - if (!results.category) { - return next(); - } + function (results, next) { + if (!results.category) { + return next(); + } - if (!results.privileges.read) { - return helpers.notAllowed(req, res); - } + if (!results.privileges.read) { + return helpers.notAllowed(req, res); + } - var feed = generateForPostsFeed({ - title: results.category.name + ' Recent Posts', - description: 'A list of recent posts from ' + results.category.name, - feed_url: '/category/' + cid + '/recentposts.rss', - site_url: '/category/' + cid + '/recentposts', - }, results.posts); + var feed = generateForPostsFeed({ + title: results.category.name + ' Recent Posts', + description: 'A list of recent posts from ' + results.category.name, + feed_url: '/category/' + cid + '/recentposts.rss', + site_url: '/category/' + cid + '/recentposts', + }, results.posts); - sendFeed(feed, res); - }); + sendFeed(feed, res); + }, + ], next); } function generateForPostsFeed(feedOptions, posts) { @@ -377,7 +359,7 @@ function generateForPostsFeed(feedOptions, posts) { function generateForTag(req, res, next) { if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) { - return next(); + return controllers404.send404(req, res); } var tag = validator.escape(String(req.params.tag)); var page = parseInt(req.query.page, 10) || 1; diff --git a/test/feeds.js b/test/feeds.js new file mode 100644 index 0000000000..6e6628c038 --- /dev/null +++ b/test/feeds.js @@ -0,0 +1,119 @@ +'use strict'; + +var assert = require('assert'); +var async = require('async'); +var request = require('request'); +var nconf = require('nconf'); + +var db = require('./mocks/databasemock'); +var topics = require('../src/topics'); +var categories = require('../src/categories'); +var groups = require('../src/groups'); +var user = require('../src/user'); +var meta = require('../src/meta'); +var privileges = require('../src/privileges'); + +describe('feeds', function () { + var tid; + var pid; + var fooUid; + var cid; + before(function (done) { + groups.resetCache(); + meta.config['feeds:disableRSS'] = 1; + async.series({ + category: function (next) { + categories.create({ + name: 'Test Category', + description: 'Test category created by testing script', + }, next); + }, + user: function (next) { + user.create({ username: 'foo', password: 'barbar', email: 'foo@test.com' }, next); + }, + }, function (err, results) { + if (err) { + return done(err); + } + cid = results.category.cid; + fooUid = results.user; + + topics.post({ uid: results.user, title: 'test topic title', content: 'test topic content', cid: results.category.cid }, function (err, result) { + tid = result.topicData.tid; + pid = result.postData.pid; + done(err); + }); + }); + }); + + + it('should 404', function (done) { + var feedUrls = [ + nconf.get('url') + '/topic/' + tid + '.rss', + nconf.get('url') + '/category/' + cid + '.rss', + nconf.get('url') + '/popular.rss', + nconf.get('url') + '/popular/day.rss', + nconf.get('url') + '/recentposts.rss', + nconf.get('url') + '/category/' + cid + '/recentposts.rss', + nconf.get('url') + '/user/foo/topics.rss', + nconf.get('url') + '/tags/nodebb.rss', + ]; + async.eachSeries(feedUrls, function (url, next) { + request(url, function (err, res) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + next(); + }); + }, function (err) { + assert.ifError(err); + meta.config['feeds:disableRSS'] = 0; + done(); + }); + }); + + it('should 404 if topic does not exist', function (done) { + request(nconf.get('url') + '/topic/' + 1000 + '.rss', function (err, res) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + done(); + }); + }); + + it('should redirect if we do not have read privilege', function (done) { + privileges.categories.rescind(['topics:read'], cid, 'guests', function (err) { + assert.ifError(err); + request(nconf.get('url') + '/topic/' + tid + '.rss', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + assert(body.indexOf('Login to your account') !== -1); + privileges.categories.give(['topics:read'], cid, 'guests', done); + }); + }); + }); + + it('should 404 if user is not found', function (done) { + request(nconf.get('url') + '/user/doesnotexist/topics.rss', function (err, res) { + assert.ifError(err); + assert.equal(res.statusCode, 404); + done(); + }); + }); + + it('should redirect if we do not have read privilege', function (done) { + privileges.categories.rescind(['read'], cid, 'guests', function (err) { + assert.ifError(err); + request(nconf.get('url') + '/category/' + cid + '.rss', function (err, res, body) { + assert.ifError(err); + assert.equal(res.statusCode, 200); + assert(body); + assert(body.indexOf('Login to your account') !== -1); + privileges.categories.give(['read'], cid, 'guests', done); + }); + }); + }); + + after(function (done) { + db.emptydb(done); + }); +}); From 64c7907e6c69396ac808f88804ea558846f00345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Sun, 21 May 2017 20:14:00 -0400 Subject: [PATCH 121/263] added recent.rss --- test/feeds.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/feeds.js b/test/feeds.js index 6e6628c038..4fd0bbd90e 100644 --- a/test/feeds.js +++ b/test/feeds.js @@ -51,6 +51,7 @@ describe('feeds', function () { var feedUrls = [ nconf.get('url') + '/topic/' + tid + '.rss', nconf.get('url') + '/category/' + cid + '.rss', + nconf.get('url') + '/recent.rss', nconf.get('url') + '/popular.rss', nconf.get('url') + '/popular/day.rss', nconf.get('url') + '/recentposts.rss', From d988e8a50f13462aff6e4b9e89caf17d68fff8a1 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Sun, 21 May 2017 18:20:01 -0600 Subject: [PATCH 122/263] Test minifier --- .eslintignore | 1 + src/meta/minifier.js | 2 +- test/build.js | 94 ++++++++++++++++++++++++++++++++++++++++++++ test/files/1.css | 1 + test/files/1.js | 5 +++ test/files/2.js | 3 ++ test/files/2.less | 1 + 7 files changed, 106 insertions(+), 1 deletion(-) create mode 100644 test/files/1.css create mode 100644 test/files/1.js create mode 100644 test/files/2.js create mode 100644 test/files/2.less diff --git a/.eslintignore b/.eslintignore index 3278600389..11b456699f 100644 --- a/.eslintignore +++ b/.eslintignore @@ -17,3 +17,4 @@ logs/ /coverage /build .eslintrc +test/files diff --git a/src/meta/minifier.js b/src/meta/minifier.js index da7c570ce7..7c1a83bea4 100644 --- a/src/meta/minifier.js +++ b/src/meta/minifier.js @@ -156,7 +156,7 @@ function concat(data, callback) { return callback(err); } - var output = files.join(os.EOL + ';'); + var output = files.join('\n;'); callback(null, { code: output }); }); diff --git a/test/build.js b/test/build.js index 3b6890ebe6..20d43033a9 100644 --- a/test/build.js +++ b/test/build.js @@ -1,8 +1,102 @@ 'use strict'; +var string = require('string'); +var path = require('path'); +var fs = require('fs'); var assert = require('assert'); +var mkdirp = require('mkdirp'); var db = require('./mocks/databasemock'); +var file = require('../src/file'); + +describe('minifier', function () { + before(function (done) { + mkdirp(path.join(__dirname, '../build/test'), done); + }); + + var minifier = require('../src/meta/minifier'); + var scripts = [ + path.resolve(__dirname, './files/1.js'), + path.resolve(__dirname, './files/2.js'), + ]; + it('.js.bundle() should concat scripts', function (done) { + minifier.js.bundle(scripts, false, false, function (err, bundle) { + assert.ifError(err); + assert.strictEqual( + bundle.code, + '(function (window, document) {' + + '\n\twindow.doStuff = function () {' + + '\n\t\tdocument.body.innerHTML = \'Stuff has been done\';' + + '\n\t};' + + '\n})(window, document);' + + '\n' + + '\n;function foo(name, age) {' + + '\n\treturn \'The person known as "\' + name + \'" is \' + age + \' years old\';' + + '\n}' + + '\n' + ); + done(); + }); + }); + + it('.js.bundle() should minify scripts', function (done) { + minifier.js.bundle(scripts, true, false, function (err, bundle) { + assert.ifError(err); + assert.strictEqual( + bundle.code, + '(function(n,o){n.doStuff=function(){o.body.innerHTML="Stuff has been done"}})(window,document);function foo(n,o){return\'The person known as "\'+n+\'" is \'+o+" years old"}' + ); + done(); + }); + }); + + it('.js.minifyBatch() should minify each script', function (done) { + var s = scripts.map(function (script) { + return { + srcPath: script, + destPath: path.resolve(__dirname, '../build/test', path.basename(script)), + }; + }); + minifier.js.minifyBatch(s, false, function (err) { + assert.ifError(err); + + assert(file.existsSync(s[0].destPath)); + assert(file.existsSync(s[1].destPath)); + + fs.readFile(s[0].destPath, function (err, buffer) { + assert.ifError(err); + assert.strictEqual( + buffer.toString(), + '(function(n,o){n.doStuff=function(){o.body.innerHTML="Stuff has been done"}})(window,document);' + ); + done(); + }); + }); + }); + + var styles = [ + '@import (inline) "./1.css";', + '@import "./2.less";', + ].join('\n'); + var paths = [ + path.resolve(__dirname, './files'), + ]; + it('.css.bundle() should concat styles', function (done) { + minifier.css.bundle(styles, paths, false, false, function (err, bundle) { + assert.ifError(err); + assert.strictEqual(bundle.code, '.help { margin: 10px; } .yellow { background: yellow; }\n.help {\n display: block;\n}\n.help .blue {\n background: blue;\n}\n'); + done(); + }); + }); + + it('.css.bundle() should minify styles', function (done) { + minifier.css.bundle(styles, paths, true, false, function (err, bundle) { + assert.ifError(err); + assert.strictEqual(bundle.code, '.help{margin:10px;display:block}.yellow{background:#ff0}.help .blue{background:#00f}'); + done(); + }); + }); +}); describe('Build', function () { it('should build all assets', function (done) { diff --git a/test/files/1.css b/test/files/1.css new file mode 100644 index 0000000000..840cf64b36 --- /dev/null +++ b/test/files/1.css @@ -0,0 +1 @@ +.help { margin: 10px; } .yellow { background: yellow; } \ No newline at end of file diff --git a/test/files/1.js b/test/files/1.js new file mode 100644 index 0000000000..b20055f8ee --- /dev/null +++ b/test/files/1.js @@ -0,0 +1,5 @@ +(function (window, document) { + window.doStuff = function () { + document.body.innerHTML = 'Stuff has been done'; + }; +})(window, document); diff --git a/test/files/2.js b/test/files/2.js new file mode 100644 index 0000000000..9369213316 --- /dev/null +++ b/test/files/2.js @@ -0,0 +1,3 @@ +function foo(name, age) { + return 'The person known as "' + name + '" is ' + age + ' years old'; +} diff --git a/test/files/2.less b/test/files/2.less new file mode 100644 index 0000000000..cdd5d5b5f2 --- /dev/null +++ b/test/files/2.less @@ -0,0 +1 @@ +.help { display: block; .blue { background: blue; } } \ No newline at end of file From 62546bc4fd06210fa9890746f69a6673c3a99ec8 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak Date: Sun, 21 May 2017 19:01:41 -0600 Subject: [PATCH 123/263] Build tests --- test/build.js | 96 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 91 insertions(+), 5 deletions(-) diff --git a/test/build.js b/test/build.js index 20d43033a9..215b823ce2 100644 --- a/test/build.js +++ b/test/build.js @@ -5,6 +5,7 @@ var path = require('path'); var fs = require('fs'); var assert = require('assert'); var mkdirp = require('mkdirp'); +var rimraf = require('rimraf'); var db = require('./mocks/databasemock'); var file = require('../src/file'); @@ -98,12 +99,97 @@ describe('minifier', function () { }); }); -describe('Build', function () { - it('should build all assets', function (done) { - this.timeout(50000); - var build = require('../src/meta/build'); - build.buildAll(function (err) { +describe('Build', function (done) { + var build = require('../src/meta/build'); + + before(function (done) { + rimraf(path.join(__dirname, '../build/public'), done); + }); + + it('should build plugin static dirs', function (done) { + build.build(['plugin static dirs'], function (err) { + assert.ifError(err); + assert(file.existsSync(path.join(__dirname, '../build/public/plugins/nodebb-plugin-dbsearch/dbsearch'))); + done(); + }); + }); + + it('should build requirejs modules', function (done) { + build.build(['requirejs modules'], function (err) { + assert.ifError(err); + var filename = path.join(__dirname, '../build/public/src/modules/Chart.js'); + assert(file.existsSync(filename)); + assert(fs.readFileSync(filename).toString().startsWith('/*!\n * Chart.js')); + done(); + }); + }); + + it('should build client js bundle', function (done) { + build.build(['client js bundle'], function (err) { + assert.ifError(err); + var filename = path.join(__dirname, '../build/public/nodebb.min.js'); + assert(file.existsSync(filename)); + assert(fs.readFileSync(filename).length > 1000); + done(); + }); + }); + + it('should build admin js bundle', function (done) { + build.build(['admin js bundle'], function (err) { + assert.ifError(err); + var filename = path.join(__dirname, '../build/public/acp.min.js'); + assert(file.existsSync(filename)); + assert(fs.readFileSync(filename).length > 1000); + done(); + }); + }); + + it('should build client side styles', function (done) { + build.build(['client side styles'], function (err) { + assert.ifError(err); + var filename = path.join(__dirname, '../build/public/stylesheet.css'); + assert(file.existsSync(filename)); + assert(fs.readFileSync(filename).toString().startsWith('/*! normalize.css')); + done(); + }); + }); + + it('should build admin control panel styles', function (done) { + build.build(['admin control panel styles'], function (err) { + assert.ifError(err); + var filename = path.join(__dirname, '../build/public/admin.css'); + assert(file.existsSync(filename)); + assert(fs.readFileSync(filename).toString().startsWith('@charset "UTF-8";')); + done(); + }); + }); + + it('should build templates', function (done) { + build.build(['templates'], function (err) { + assert.ifError(err); + var filename = path.join(__dirname, '../build/public/templates/admin/header.tpl'); + assert(file.existsSync(filename)); + assert(fs.readFileSync(filename).toString().startsWith('')); + done(); + }); + }); + + it('should build languages', function (done) { + build.build(['languages'], function (err) { + assert.ifError(err); + var filename = path.join(__dirname, '../build/public/language/en-GB/global.json'); + assert(file.existsSync(filename)); + var global = fs.readFileSync(filename).toString(); + assert.strictEqual(JSON.parse(global).home, 'Home'); + done(); + }); + }); + + it('should build sounds', function (done) { + build.build(['sounds'], function (err) { assert.ifError(err); + var filename = path.join(__dirname, '../build/public/sounds/fileMap.json'); + assert(file.existsSync(filename)); done(); }); }); From 12e33695226e79bb71ecbde9b8cc6622a8977bf4 Mon Sep 17 00:00:00 2001 From: Julian Lam Date: Tue, 23 May 2017 11:35:40 -0400 Subject: [PATCH 124/263] Tweaked upgrade script instructions --- src/upgrade.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/upgrade.js b/src/upgrade.js index 22c7867eeb..2bba82dd91 100644 --- a/src/upgrade.js +++ b/src/upgrade.js @@ -13,9 +13,8 @@ var file = require('../src/file'); * * 1. Copy TEMPLATE to a file name of your choice. Try to be succinct. * 2. Open up that file and change the user-friendly name (can be longer/more descriptive than the file name) - * and timestamp + * and timestamp (don't forget the timestamp!) * 3. Add your script under the "method" property - * 4. Append your filename to the array below for the next NodeBB version. */ var Upgrade = {}; From fa5026c92ee2c606030d885830233e9e9315f747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= Date: Tue, 23 May 2017 13:05:33 -0400 Subject: [PATCH 125/263] clear settings cache on all nodes on save --- src/user/settings.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/user/settings.js b/src/user/settings.js index de0da0641d..cf65c37545 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -8,6 +8,7 @@ var meta = require('../meta'); var db = require('../database'); var plugins = require('../plugins'); +var pubsub = require('../pubsub'); var LRU = require('lru-cache'); var cache = LRU({ @@ -19,6 +20,10 @@ var cache = LRU({ module.exports = function (User) { User.settingsCache = cache; + pubsub.on('user:settings:cache:del', function (uid) { + cache.del('user:' + uid + ':settings'); + }); + User.getSettings = function (uid, callback) { if (!parseInt(uid, 10)) { return onSettingsLoaded(0, {}, callback); @@ -178,6 +183,7 @@ module.exports = function (User) { }, function (next) { cache.del('user:' + uid + ':settings'); + pubsub.publish('user:settings:cache:del', uid); User.getSettings(uid, next); }, ], callback); From a12e159f1824912b08a4bbc0f3b0af366fb38dc6 Mon Sep 17 00:00:00 2001 From: pichalite Date: Tue, 23 May 2017 19:11:16 +0000 Subject: [PATCH 126/263] Delete unused file --- public/vendor/jquery/js/jquery.js | 9921 ----------------------------- 1 file changed, 9921 deletions(-) delete mode 100644 public/vendor/jquery/js/jquery.js diff --git a/public/vendor/jquery/js/jquery.js b/public/vendor/jquery/js/jquery.js deleted file mode 100644 index 7554b8b747..0000000000 --- a/public/vendor/jquery/js/jquery.js +++ /dev/null @@ -1,9921 +0,0 @@ -/*! - * jQuery JavaScript Library v1.10.2 - * http://jquery.com/ - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * - * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2013-07-03T13:48Z - */ -(function (window, undefined) { - - // Can't do this because several apps including ASP.NET trace - // the stack via arguments.caller.callee and Firefox dies if - // you try to trace through "use strict" call chains. (#13335) - // Support: Firefox 18+ - //"use strict"; - var - // The deferred used on DOM ready - readyList, - - // A central reference to the root jQuery(document) - rootjQuery, - - // Support: IE<10 - // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined` - core_strundefined = typeof undefined, - - // Use the correct document accordingly with window argument (sandbox) - location = window.location, - document = window.document, - docElem = document.documentElement, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // [[Class]] -> type pairs - class2type = {}, - - // List of deleted data cache ids, so we can reuse them - core_deletedIds = [], - - core_version = "1.10.2", - - // Save a reference to some core methods - core_concat = core_deletedIds.concat, - core_push = core_deletedIds.push, - core_slice = core_deletedIds.slice, - core_indexOf = core_deletedIds.indexOf, - core_toString = class2type.toString, - core_hasOwn = class2type.hasOwnProperty, - core_trim = core_version.trim, - - // Define a local copy of jQuery - jQuery = function (selector, context) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init(selector, context, rootjQuery); - }, - - // Used for matching numbers - core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, - - // Used for splitting on whitespace - core_rnotwhite = /\S+/g, - - // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, - rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([\da-z])/gi, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function (all, letter) { - return letter.toUpperCase(); - }, - - // The ready event handler - completed = function (event) { - - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if (document.addEventListener || event.type === "load" || document.readyState === "complete") { - detach(); - jQuery.ready(); - } - }, - // Clean-up method for dom ready events - detach = function () { - if (document.addEventListener) { - document.removeEventListener("DOMContentLoaded", completed, false); - window.removeEventListener("load", completed, false); - - } else { - document.detachEvent("onreadystatechange", completed); - window.detachEvent("onload", completed); - } - }; - - jQuery.fn = jQuery.prototype = { - // The current version of jQuery being used - jquery: core_version, - - constructor: jQuery, - init: function (selector, context, rootjQuery) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if (!selector) { - return this; - } - - // Handle HTML strings - if (typeof selector === "string") { - if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [null, selector, null]; - - } else { - match = rquickExpr.exec(selector); - } - - // Match html or make sure no context is specified for #id - if (match && (match[1] || !context)) { - - // HANDLE: $(html) -> $(array) - if (match[1]) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - jQuery.merge(this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - )); - - // HANDLE: $(html, props) - if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) { - for (match in context) { - // Properties of context are called as methods if possible - if (jQuery.isFunction(this[match])) { - this[match](context[match]); - - // ...and otherwise set as attributes - } else { - this.attr(match, context[match]); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById(match[2]); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if (elem && elem.parentNode) { - // Handle the case where IE and Opera return items - // by name instead of ID - if (elem.id !== match[2]) { - return rootjQuery.find(selector); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if (!context || context.jquery) { - return (context || rootjQuery).find(selector); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor(context).find(selector); - } - - // HANDLE: $(DOMElement) - } else if (selector.nodeType) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if (jQuery.isFunction(selector)) { - return rootjQuery.ready(selector); - } - - if (selector.selector !== undefined) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray(selector, this); - }, - - // Start with an empty selector - selector: "", - - // The default length of a jQuery object is 0 - length: 0, - - toArray: function () { - return core_slice.call(this); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function (num) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - (num < 0 ? this[this.length + num] : this[num]); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function (elems) { - - // Build a new jQuery matched element set - var ret = jQuery.merge(this.constructor(), elems); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - ret.context = this.context; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function (callback, args) { - return jQuery.each(this, callback, args); - }, - - ready: function (fn) { - // Add the callback - jQuery.ready.promise().done(fn); - - return this; - }, - - slice: function () { - return this.pushStack(core_slice.apply(this, arguments)); - }, - - first: function () { - return this.eq(0); - }, - - last: function () { - return this.eq(-1); - }, - - eq: function (i) { - var len = this.length, - j = +i + (i < 0 ? len : 0); - return this.pushStack(j >= 0 && j < len ? [this[j]] : []); - }, - - map: function (callback) { - return this.pushStack(jQuery.map(this, function (elem, i) { - return callback.call(elem, i, elem); - })); - }, - - end: function () { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: core_push, - sort: [].sort, - splice: [].splice - }; - - // Give the init function the jQuery prototype for later instantiation - jQuery.fn.init.prototype = jQuery.fn; - - jQuery.extend = jQuery.fn.extend = function () { - var src, copyIsArray, copy, name, options, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if (typeof target === "boolean") { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if (typeof target !== "object" && !jQuery.isFunction(target)) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if (length === i) { - target = this; - --i; - } - - for (; i < length; i++) { - // Only deal with non-null/undefined values - if ((options = arguments[i]) != null) { - // Extend the base object - for (name in options) { - src = target[name]; - copy = options[name]; - - // Prevent never-ending loop - if (target === copy) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) { - if (copyIsArray) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[name] = jQuery.extend(deep, clone, copy); - - // Don't bring in undefined values - } else if (copy !== undefined) { - target[name] = copy; - } - } - } - } - - // Return the modified object - return target; - }; - - jQuery.extend({ - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + (core_version + Math.random()).replace(/\D/g, ""), - - noConflict: function (deep) { - if (window.$ === jQuery) { - window.$ = _$; - } - - if (deep && window.jQuery === jQuery) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function (hold) { - if (hold) { - jQuery.readyWait++; - } else { - jQuery.ready(true); - } - }, - - // Handle when the DOM is ready - ready: function (wait) { - - // Abort if there are pending holds or we're already ready - if (wait === true ? --jQuery.readyWait : jQuery.isReady) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if (!document.body) { - return setTimeout(jQuery.ready); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if (wait !== true && --jQuery.readyWait > 0) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith(document, [jQuery]); - - // Trigger any bound ready events - if (jQuery.fn.trigger) { - jQuery(document).trigger("ready").off("ready"); - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function (obj) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function (obj) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function (obj) { - /* jshint eqeqeq: false */ - return obj != null && obj == obj.window; - }, - - isNumeric: function (obj) { - return !isNaN(parseFloat(obj)) && isFinite(obj); - }, - - type: function (obj) { - if (obj == null) { - return String(obj); - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[core_toString.call(obj)] || "object" : - typeof obj; - }, - - isPlainObject: function (obj) { - var key; - - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if (!obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow(obj)) { - return false; - } - - try { - // Not own constructor property must be Object - if (obj.constructor && !core_hasOwn.call(obj, "constructor") && !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) { - return false; - } - } catch (e) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Support: IE<9 - // Handle iteration over inherited properties before own properties. - if (jQuery.support.ownLast) { - for (key in obj) { - return core_hasOwn.call(obj, key); - } - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - for (key in obj) {} - - return key === undefined || core_hasOwn.call(obj, key); - }, - - isEmptyObject: function (obj) { - var name; - for (name in obj) { - return false; - } - return true; - }, - - error: function (msg) { - throw new Error(msg); - }, - - // data: string of html - // context (optional): If specified, the fragment will be created in this context, defaults to document - // keepScripts (optional): If true, will include scripts passed in the html string - parseHTML: function (data, context, keepScripts) { - if (!data || typeof data !== "string") { - return null; - } - if (typeof context === "boolean") { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec(data), - scripts = !keepScripts && []; - - // Single tag - if (parsed) { - return [context.createElement(parsed[1])]; - } - - parsed = jQuery.buildFragment([data], context, scripts); - if (scripts) { - jQuery(scripts).remove(); - } - return jQuery.merge([], parsed.childNodes); - }, - - parseJSON: function (data) { - // Attempt to parse using the native JSON parser first - if (window.JSON && window.JSON.parse) { - return window.JSON.parse(data); - } - - if (data === null) { - return data; - } - - if (typeof data === "string") { - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim(data); - - if (data) { - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if (rvalidchars.test(data.replace(rvalidescape, "@") - .replace(rvalidtokens, "]") - .replace(rvalidbraces, ""))) { - - return (new Function("return " + data))(); - } - } - } - - jQuery.error("Invalid JSON: " + data); - }, - - // Cross-browser xml parsing - parseXML: function (data) { - var xml, tmp; - if (!data || typeof data !== "string") { - return null; - } - try { - if (window.DOMParser) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString(data, "text/xml"); - } else { // IE - xml = new ActiveXObject("Microsoft.XMLDOM"); - xml.async = "false"; - xml.loadXML(data); - } - } catch (e) { - xml = undefined; - } - if (!xml || !xml.documentElement || xml.getElementsByTagName("parsererror").length) { - jQuery.error("Invalid XML: " + data); - } - return xml; - }, - - noop: function () {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function (data) { - if (data && jQuery.trim(data)) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - (window.execScript || function (data) { - window["eval"].call(window, data); - })(data); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function (string) { - return string.replace(rmsPrefix, "ms-").replace(rdashAlpha, fcamelCase); - }, - - nodeName: function (elem, name) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - }, - - // args is for internal usage only - each: function (obj, callback, args) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike(obj); - - if (args) { - if (isArray) { - for (; i < length; i++) { - value = callback.apply(obj[i], args); - - if (value === false) { - break; - } - } - } else { - for (i in obj) { - value = callback.apply(obj[i], args); - - if (value === false) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if (isArray) { - for (; i < length; i++) { - value = callback.call(obj[i], i, obj[i]); - - if (value === false) { - break; - } - } - } else { - for (i in obj) { - value = callback.call(obj[i], i, obj[i]); - - if (value === false) { - break; - } - } - } - } - - return obj; - }, - - // Use native String.trim function wherever possible - trim: core_trim && !core_trim.call("\uFEFF\xA0") ? function (text) { - return text == null ? - "" : - core_trim.call(text); - } : - - // Otherwise use our own trimming functionality - - function (text) { - return text == null ? - "" : - (text + "").replace(rtrim, ""); - }, - - // results is for internal usage only - makeArray: function (arr, results) { - var ret = results || []; - - if (arr != null) { - if (isArraylike(Object(arr))) { - jQuery.merge(ret, - typeof arr === "string" ? - [arr] : arr - ); - } else { - core_push.call(ret, arr); - } - } - - return ret; - }, - - inArray: function (elem, arr, i) { - var len; - - if (arr) { - if (core_indexOf) { - return core_indexOf.call(arr, elem, i); - } - - len = arr.length; - i = i ? i < 0 ? Math.max(0, len + i) : i : 0; - - for (; i < len; i++) { - // Skip accessing in sparse arrays - if (i in arr && arr[i] === elem) { - return i; - } - } - } - - return -1; - }, - - merge: function (first, second) { - var l = second.length, - i = first.length, - j = 0; - - if (typeof l === "number") { - for (; j < l; j++) { - first[i++] = second[j]; - } - } else { - while (second[j] !== undefined) { - first[i++] = second[j++]; - } - } - - first.length = i; - - return first; - }, - - grep: function (elems, callback, inv) { - var retVal, - ret = [], - i = 0, - length = elems.length; - inv = !! inv; - - // Go through the array, only saving the items - // that pass the validator function - for (; i < length; i++) { - retVal = !! callback(elems[i], i); - if (inv !== retVal) { - ret.push(elems[i]); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function (elems, callback, arg) { - var value, - i = 0, - length = elems.length, - isArray = isArraylike(elems), - ret = []; - - // Go through the array, translating each of the items to their - if (isArray) { - for (; i < length; i++) { - value = callback(elems[i], i, arg); - - if (value != null) { - ret[ret.length] = value; - } - } - - // Go through every key on the object, - } else { - for (i in elems) { - value = callback(elems[i], i, arg); - - if (value != null) { - ret[ret.length] = value; - } - } - } - - // Flatten any nested arrays - return core_concat.apply([], ret); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function (fn, context) { - var args, proxy, tmp; - - if (typeof context === "string") { - tmp = fn[context]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if (!jQuery.isFunction(fn)) { - return undefined; - } - - // Simulated bind - args = core_slice.call(arguments, 2); - proxy = function () { - return fn.apply(context || this, args.concat(core_slice.call(arguments))); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - // Multifunctional method to get and set values of a collection - // The value/s can optionally be executed if it's a function - access: function (elems, fn, key, value, chainable, emptyGet, raw) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if (jQuery.type(key) === "object") { - chainable = true; - for (i in key) { - jQuery.access(elems, fn, i, key[i], true, emptyGet, raw); - } - - // Sets one value - } else if (value !== undefined) { - chainable = true; - - if (!jQuery.isFunction(value)) { - raw = true; - } - - if (bulk) { - // Bulk operations run against the entire set - if (raw) { - fn.call(elems, value); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function (elem, key, value) { - return bulk.call(jQuery(elem), value); - }; - } - } - - if (fn) { - for (; i < length; i++) { - fn(elems[i], key, raw ? value : value.call(elems[i], i, fn(elems[i], key))); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call(elems) : - length ? fn(elems[0], key) : emptyGet; - }, - - now: function () { - return (new Date()).getTime(); - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations. - // Note: this method belongs to the css module but it's needed here for the support module. - // If support gets modularized, this method should be moved back to the css module. - swap: function (elem, options, callback, args) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for (name in options) { - old[name] = elem.style[name]; - elem.style[name] = options[name]; - } - - ret = callback.apply(elem, args || []); - - // Revert the old values - for (name in options) { - elem.style[name] = old[name]; - } - - return ret; - } - }); - - jQuery.ready.promise = function (obj) { - if (!readyList) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if (document.readyState === "complete") { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout(jQuery.ready); - - // Standards-based browsers support DOMContentLoaded - } else if (document.addEventListener) { - // Use the handy event callback - document.addEventListener("DOMContentLoaded", completed, false); - - // A fallback to window.onload, that will always work - window.addEventListener("load", completed, false); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent("onreadystatechange", completed); - - // A fallback to window.onload, that will always work - window.attachEvent("onload", completed); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch (e) {} - - if (top && top.doScroll) { - (function doScrollCheck() { - if (!jQuery.isReady) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch (e) { - return setTimeout(doScrollCheck, 50); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise(obj); - }; - - // Populate the class2type map - jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function (i, name) { - class2type["[object " + name + "]"] = name.toLowerCase(); - }); - - function isArraylike(obj) { - var length = obj.length, - type = jQuery.type(obj); - - if (jQuery.isWindow(obj)) { - return false; - } - - if (obj.nodeType === 1 && length) { - return true; - } - - return type === "array" || type !== "function" && - (length === 0 || - typeof length === "number" && length > 0 && (length - 1) in obj); - } - - // All jQuery objects should point back to these - rootjQuery = jQuery(document); - /*! - * Sizzle CSS Selector Engine v1.10.2 - * http://sizzlejs.com/ - * - * Copyright 2013 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2013-07-03 - */ - (function (window, undefined) { - - var i, - support, - cachedruns, - Expr, - getText, - isXML, - compile, - outermostContext, - sortInput, - - // Local document vars - setDocument, - document, - docElem, - documentIsHTML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - - // Instance-specific data - expando = "sizzle" + -(new Date()), - preferredDoc = window.document, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - hasDuplicate = false, - sortOrder = function (a, b) { - if (a === b) { - hasDuplicate = true; - return 0; - } - return 0; - }, - - // General-purpose constants - strundefined = typeof undefined, - MAX_NEGATIVE = 1 << 31, - - // Instance methods - hasOwn = ({}).hasOwnProperty, - arr = [], - pop = arr.pop, - push_native = arr.push, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function (elem) { - var i = 0, - len = this.length; - for (; i < len; i++) { - if (this[i] === elem) { - return i; - } - } - return -1; - }, - - booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", - - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace("w", "w#"), - - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace(3, 8) + ")*)|.*)\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rtrim = new RegExp("^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g"), - - rcomma = new RegExp("^" + whitespace + "*," + whitespace + "*"), - rcombinators = new RegExp("^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*"), - - rsibling = new RegExp(whitespace + "*[+~]"), - rattributeQuotes = new RegExp("=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g"), - - rpseudo = new RegExp(pseudos), - ridentifier = new RegExp("^" + identifier + "$"), - - matchExpr = { - "ID": new RegExp("^#(" + characterEncoding + ")"), - "CLASS": new RegExp("^\\.(" + characterEncoding + ")"), - "TAG": new RegExp("^(" + characterEncoding.replace("w", "w*") + ")"), - "ATTR": new RegExp("^" + attributes), - "PSEUDO": new RegExp("^" + pseudos), - "CHILD": new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i"), - "bool": new RegExp("^(?:" + booleans + ")$", "i"), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp("^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i") - }, - - rnative = /^[^{]+\{\s*\[native \w/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rescape = /'|\\/g, - - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = new RegExp("\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig"), - funescape = function (_, escaped, escapedWhitespace) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - // Support: Firefox - // Workaround erroneous numeric interpretation of +"0x" - return high !== high || escapedWhitespace ? - escaped : - // BMP codepoint - high < 0 ? - String.fromCharCode(high + 0x10000) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode(high >> 10 | 0xD800, high & 0x3FF | 0xDC00); - }; - - // Optimize for push.apply( _, NodeList ) - try { - push.apply( - (arr = slice.call(preferredDoc.childNodes)), - preferredDoc.childNodes - ); - // Support: Android<4.0 - // Detect silently failing push.apply - arr[preferredDoc.childNodes.length].nodeType; - } catch (e) { - push = { - apply: arr.length ? - - // Leverage slice if possible - - function (target, els) { - push_native.apply(target, slice.call(els)); - } : - - // Support: IE<9 - // Otherwise append directly - - function (target, els) { - var j = target.length, - i = 0; - // Can't trust NodeList.length - while ((target[j++] = els[i++])) {} - target.length = j - 1; - } - }; - } - - function Sizzle(selector, context, results, seed) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; - - if ((context ? context.ownerDocument || context : preferredDoc) !== document) { - setDocument(context); - } - - context = context || document; - results = results || []; - - if (!selector || typeof selector !== "string") { - return results; - } - - if ((nodeType = context.nodeType) !== 1 && nodeType !== 9) { - return []; - } - - if (documentIsHTML && !seed) { - - // Shortcuts - if ((match = rquickExpr.exec(selector))) { - // Speed-up: Sizzle("#ID") - if ((m = match[1])) { - if (nodeType === 9) { - elem = context.getElementById(m); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if (elem && elem.parentNode) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if (elem.id === m) { - results.push(elem); - return results; - } - } else { - return results; - } - } else { - // Context is not a document - if (context.ownerDocument && (elem = context.ownerDocument.getElementById(m)) && - contains(context, elem) && elem.id === m) { - results.push(elem); - return results; - } - } - - // Speed-up: Sizzle("TAG") - } else if (match[2]) { - push.apply(results, context.getElementsByTagName(selector)); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ((m = match[3]) && support.getElementsByClassName && context.getElementsByClassName) { - push.apply(results, context.getElementsByClassName(m)); - return results; - } - } - - // QSA path - if (support.qsa && (!rbuggyQSA || !rbuggyQSA.test(selector))) { - nid = old = expando; - newContext = context; - newSelector = nodeType === 9 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if (nodeType === 1 && context.nodeName.toLowerCase() !== "object") { - groups = tokenize(selector); - - if ((old = context.getAttribute("id"))) { - nid = old.replace(rescape, "\\$&"); - } else { - context.setAttribute("id", nid); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while (i--) { - groups[i] = nid + toSelector(groups[i]); - } - newContext = rsibling.test(selector) && context.parentNode || context; - newSelector = groups.join(","); - } - - if (newSelector) { - try { - push.apply(results, - newContext.querySelectorAll(newSelector) - ); - return results; - } catch (qsaError) {} finally { - if (!old) { - context.removeAttribute("id"); - } - } - } - } - } - - // All others - return select(selector.replace(rtrim, "$1"), context, results, seed); - } - - /** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ - - function createCache() { - var keys = []; - - function cache(key, value) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if (keys.push(key += " ") > Expr.cacheLength) { - // Only keep the most recent entries - delete cache[keys.shift()]; - } - return (cache[key] = value); - } - return cache; - } - - /** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ - - function markFunction(fn) { - fn[expando] = true; - return fn; - } - - /** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ - - function assert(fn) { - var div = document.createElement("div"); - - try { - return !!fn(div); - } catch (e) { - return false; - } finally { - // Remove from its parent by default - if (div.parentNode) { - div.parentNode.removeChild(div); - } - // release memory in IE - div = null; - } - } - - /** - * Adds the same handler for all of the specified attrs - * @param {String} attrs Pipe-separated list of attributes - * @param {Function} handler The method that will be applied - */ - - function addHandle(attrs, handler) { - var arr = attrs.split("|"), - i = attrs.length; - - while (i--) { - Expr.attrHandle[arr[i]] = handler; - } - } - - /** - * Checks document order of two siblings - * @param {Element} a - * @param {Element} b - * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b - */ - - function siblingCheck(a, b) { - var cur = b && a, - diff = cur && a.nodeType === 1 && b.nodeType === 1 && - (~b.sourceIndex || MAX_NEGATIVE) - - (~a.sourceIndex || MAX_NEGATIVE); - - // Use IE sourceIndex if available on both nodes - if (diff) { - return diff; - } - - // Check if b follows a - if (cur) { - while ((cur = cur.nextSibling)) { - if (cur === b) { - return -1; - } - } - } - - return a ? 1 : -1; - } - - /** - * Returns a function to use in pseudos for input types - * @param {String} type - */ - - function createInputPseudo(type) { - return function (elem) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; - } - - /** - * Returns a function to use in pseudos for buttons - * @param {String} type - */ - - function createButtonPseudo(type) { - return function (elem) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; - } - - /** - * Returns a function to use in pseudos for positionals - * @param {Function} fn - */ - - function createPositionalPseudo(fn) { - return markFunction(function (argument) { - argument = +argument; - return markFunction(function (seed, matches) { - var j, - matchIndexes = fn([], seed.length, argument), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while (i--) { - if (seed[(j = matchIndexes[i])]) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); - } - - /** - * Detect xml - * @param {Element|Object} elem An element or a document - */ - isXML = Sizzle.isXML = function (elem) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; - }; - - // Expose support vars for convenience - support = Sizzle.support = {}; - - /** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ - setDocument = Sizzle.setDocument = function (node) { - var doc = node ? node.ownerDocument || node : preferredDoc, - parent = doc.defaultView; - - // If no document and documentElement is available, return - if (doc === document || doc.nodeType !== 9 || !doc.documentElement) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - - // Support tests - documentIsHTML = !isXML(doc); - - // Support: IE>8 - // If iframe document is assigned to "document" variable and if iframe has been reloaded, - // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 - // IE6-8 do not support the defaultView property so parent will be undefined - if (parent && parent.attachEvent && parent !== parent.top) { - parent.attachEvent("onbeforeunload", function () { - setDocument(); - }); - } - - /* Attributes - ---------------------------------------------------------------------- */ - - // Support: IE<8 - // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) - support.attributes = assert(function (div) { - div.className = "i"; - return !div.getAttribute("className"); - }); - - /* getElement(s)By* - ---------------------------------------------------------------------- */ - - // Check if getElementsByTagName("*") returns only elements - support.getElementsByTagName = assert(function (div) { - div.appendChild(doc.createComment("")); - return !div.getElementsByTagName("*").length; - }); - - // Check if getElementsByClassName can be trusted - support.getElementsByClassName = assert(function (div) { - div.innerHTML = "
"; - - // Support: Safari<4 - // Catch class over-caching - div.firstChild.className = "i"; - // Support: Opera<10 - // Catch gEBCN failure to find non-leading classes - return div.getElementsByClassName("i").length === 2; - }); - - // Support: IE<10 - // Check if getElementById returns elements by name - // The broken getElementById methods don't pick up programatically-set names, - // so use a roundabout getElementsByName test - support.getById = assert(function (div) { - docElem.appendChild(div).id = expando; - return !doc.getElementsByName || !doc.getElementsByName(expando).length; - }); - - // ID find and filter - if (support.getById) { - Expr.find["ID"] = function (id, context) { - if (typeof context.getElementById !== strundefined && documentIsHTML) { - var m = context.getElementById(id); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }; - Expr.filter["ID"] = function (id) { - var attrId = id.replace(runescape, funescape); - return function (elem) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - // Support: IE6/7 - // getElementById is not reliable as a find shortcut - delete Expr.find["ID"]; - - Expr.filter["ID"] = function (id) { - var attrId = id.replace(runescape, funescape); - return function (elem) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - // Tag - Expr.find["TAG"] = support.getElementsByTagName ? - function (tag, context) { - if (typeof context.getElementsByTagName !== strundefined) { - return context.getElementsByTagName(tag); - } - } : - function (tag, context) { - var elem, - tmp = [], - i = 0, - results = context.getElementsByTagName(tag); - - // Filter out possible comments - if (tag === "*") { - while ((elem = results[i++])) { - if (elem.nodeType === 1) { - tmp.push(elem); - } - } - - return tmp; - } - return results; - }; - - // Class - Expr.find["CLASS"] = support.getElementsByClassName && function (className, context) { - if (typeof context.getElementsByClassName !== strundefined && documentIsHTML) { - return context.getElementsByClassName(className); - } - }; - - /* QSA/matchesSelector - ---------------------------------------------------------------------- */ - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21) - // We allow this because of a bug in IE8/9 that throws an error - // whenever `document.activeElement` is accessed on an iframe - // So, we allow :focus to pass through QSA all the time to avoid the IE error - // See http://bugs.jquery.com/ticket/13378 - rbuggyQSA = []; - - if ((support.qsa = rnative.test(doc.querySelectorAll))) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function (div) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explicitly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; - - // Support: IE8 - // Boolean attributes and "value" are not treated correctly - if (!div.querySelectorAll("[selected]").length) { - rbuggyQSA.push("\\[" + whitespace + "*(?:value|" + booleans + ")"); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if (!div.querySelectorAll(":checked").length) { - rbuggyQSA.push(":checked"); - } - }); - - assert(function (div) { - - // Support: Opera 10-12/IE8 - // ^= $= *= and empty values - // Should not select anything - // Support: Windows 8 Native Apps - // The type attribute is restricted during .innerHTML assignment - var input = doc.createElement("input"); - input.setAttribute("type", "hidden"); - div.appendChild(input).setAttribute("t", ""); - - if (div.querySelectorAll("[t^='']").length) { - rbuggyQSA.push("[*^$]=" + whitespace + "*(?:''|\"\")"); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if (!div.querySelectorAll(":enabled").length) { - rbuggyQSA.push(":enabled", ":disabled"); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ((support.matchesSelector = rnative.test((matches = docElem.webkitMatchesSelector || - docElem.mozMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector)))) { - - assert(function (div) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call(div, "div"); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call(div, "[s!='']:x"); - rbuggyMatches.push("!=", pseudos); - }); - } - - rbuggyQSA = rbuggyQSA.length && new RegExp(rbuggyQSA.join("|")); - rbuggyMatches = rbuggyMatches.length && new RegExp(rbuggyMatches.join("|")); - - /* Contains - ---------------------------------------------------------------------- */ - - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = rnative.test(docElem.contains) || docElem.compareDocumentPosition ? - function (a, b) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !! (bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains(bup) : - a.compareDocumentPosition && a.compareDocumentPosition(bup) & 16 - )); - } : - function (a, b) { - if (b) { - while ((b = b.parentNode)) { - if (b === a) { - return true; - } - } - } - return false; - }; - - /* Sorting - ---------------------------------------------------------------------- */ - - // Document order sorting - sortOrder = docElem.compareDocumentPosition ? - function (a, b) { - - // Flag for duplicate removal - if (a === b) { - hasDuplicate = true; - return 0; - } - - var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition(b); - - if (compare) { - // Disconnected nodes - if (compare & 1 || - (!support.sortDetached && b.compareDocumentPosition(a) === compare)) { - - // Choose the first element that is related to our preferred document - if (a === doc || contains(preferredDoc, a)) { - return -1; - } - if (b === doc || contains(preferredDoc, b)) { - return 1; - } - - // Maintain original order - return sortInput ? - (indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) : - 0; - } - - return compare & 4 ? -1 : 1; - } - - // Not directly comparable, sort on existence of method - return a.compareDocumentPosition ? -1 : 1; - } : - function (a, b) { - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [a], - bp = [b]; - - // Exit early if the nodes are identical - if (a === b) { - hasDuplicate = true; - return 0; - - // Parentless nodes are either documents or disconnected - } else if (!aup || !bup) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - sortInput ? - (indexOf.call(sortInput, a) - indexOf.call(sortInput, b)) : - 0; - - // If the nodes are siblings, we can do a quick check - } else if (aup === bup) { - return siblingCheck(a, b); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ((cur = cur.parentNode)) { - ap.unshift(cur); - } - cur = b; - while ((cur = cur.parentNode)) { - bp.unshift(cur); - } - - // Walk down the tree looking for a discrepancy - while (ap[i] === bp[i]) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck(ap[i], bp[i]) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - return doc; - }; - - Sizzle.matches = function (expr, elements) { - return Sizzle(expr, null, null, elements); - }; - - Sizzle.matchesSelector = function (elem, expr) { - // Set document vars if needed - if ((elem.ownerDocument || elem) !== document) { - setDocument(elem); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace(rattributeQuotes, "='$1']"); - - if (support.matchesSelector && documentIsHTML && - (!rbuggyMatches || !rbuggyMatches.test(expr)) && - (!rbuggyQSA || !rbuggyQSA.test(expr))) { - - try { - var ret = matches.call(elem, expr); - - // IE 9's matchesSelector returns false on disconnected nodes - if (ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11) { - return ret; - } - } catch (e) {} - } - - return Sizzle(expr, document, null, [elem]).length > 0; - }; - - Sizzle.contains = function (context, elem) { - // Set document vars if needed - if ((context.ownerDocument || context) !== document) { - setDocument(context); - } - return contains(context, elem); - }; - - Sizzle.attr = function (elem, name) { - // Set document vars if needed - if ((elem.ownerDocument || elem) !== document) { - setDocument(elem); - } - - var fn = Expr.attrHandle[name.toLowerCase()], - // Don't get fooled by Object.prototype properties (jQuery #13807) - val = fn && hasOwn.call(Expr.attrHandle, name.toLowerCase()) ? - fn(elem, name, !documentIsHTML) : - undefined; - - return val === undefined ? - support.attributes || !documentIsHTML ? - elem.getAttribute(name) : - (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - null : - val; - }; - - Sizzle.error = function (msg) { - throw new Error("Syntax error, unrecognized expression: " + msg); - }; - - /** - * Document sorting and removing duplicates - * @param {ArrayLike} results - */ - Sizzle.uniqueSort = function (results) { - var elem, - duplicates = [], - j = 0, - i = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - sortInput = !support.sortStable && results.slice(0); - results.sort(sortOrder); - - if (hasDuplicate) { - while ((elem = results[i++])) { - if (elem === results[i]) { - j = duplicates.push(i); - } - } - while (j--) { - results.splice(duplicates[j], 1); - } - } - - return results; - }; - - /** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ - getText = Sizzle.getText = function (elem) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if (!nodeType) { - // If no nodeType, this is expected to be an array - for (; - (node = elem[i]); i++) { - // Do not traverse comment nodes - ret += getText(node); - } - } else if (nodeType === 1 || nodeType === 9 || nodeType === 11) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) - if (typeof elem.textContent === "string") { - return elem.textContent; - } else { - // Traverse its children - for (elem = elem.firstChild; elem; elem = elem.nextSibling) { - ret += getText(elem); - } - } - } else if (nodeType === 3 || nodeType === 4) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; - }; - - Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - attrHandle: {}, - - find: {}, - - relative: { - ">": { - dir: "parentNode", - first: true - }, - " ": { - dir: "parentNode" - }, - "+": { - dir: "previousSibling", - first: true - }, - "~": { - dir: "previousSibling" - } - }, - - preFilter: { - "ATTR": function (match) { - match[1] = match[1].replace(runescape, funescape); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = (match[4] || match[5] || "").replace(runescape, funescape); - - if (match[2] === "~=") { - match[3] = " " + match[3] + " "; - } - - return match.slice(0, 4); - }, - - "CHILD": function (match) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if (match[1].slice(0, 3) === "nth") { - // nth-* requires argument - if (!match[3]) { - Sizzle.error(match[0]); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +(match[4] ? match[5] + (match[6] || 1) : 2 * (match[3] === "even" || match[3] === "odd")); - match[5] = +((match[7] + match[8]) || match[3] === "odd"); - - // other types prohibit arguments - } else if (match[3]) { - Sizzle.error(match[0]); - } - - return match; - }, - - "PSEUDO": function (match) { - var excess, - unquoted = !match[5] && match[2]; - - if (matchExpr["CHILD"].test(match[0])) { - return null; - } - - // Accept quoted arguments as-is - if (match[3] && match[4] !== undefined) { - match[2] = match[4]; - - // Strip excess characters from unquoted arguments - } else if (unquoted && rpseudo.test(unquoted) && - // Get excess from tokenize (recursively) - (excess = tokenize(unquoted, true)) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf(")", unquoted.length - excess) - unquoted.length)) { - - // excess is a negative index - match[0] = match[0].slice(0, excess); - match[2] = unquoted.slice(0, excess); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice(0, 3); - } - }, - - filter: { - - "TAG": function (nodeNameSelector) { - var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase(); - return nodeNameSelector === "*" ? - function () { - return true; - } : - function (elem) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function (className) { - var pattern = classCache[className + " "]; - - return pattern || - (pattern = new RegExp("(^|" + whitespace + ")" + className + "(" + whitespace + "|$)")) && - classCache(className, function (elem) { - return pattern.test(typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || ""); - }); - }, - - "ATTR": function (name, operator, check) { - return function (elem) { - var result = Sizzle.attr(elem, name); - - if (result == null) { - return operator === "!="; - } - if (!operator) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf(check) === 0 : - operator === "*=" ? check && result.indexOf(check) > -1 : - operator === "$=" ? check && result.slice(-check.length) === check : - operator === "~=" ? (" " + result + " ").indexOf(check) > -1 : - operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" : - false; - }; - }, - - "CHILD": function (type, what, argument, first, last) { - var simple = type.slice(0, 3) !== "nth", - forward = type.slice(-4) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - - function (elem) { - return !!elem.parentNode; - } : - - function (elem, context, xml) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if (parent) { - - // :(first|last|only)-(child|of-type) - if (simple) { - while (dir) { - node = elem; - while ((node = node[dir])) { - if (ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [forward ? parent.firstChild : parent.lastChild]; - - // non-xml :nth-child(...) stores cache data on `parent` - if (forward && useCache) { - // Seek `elem` from a previously-cached index - outerCache = parent[expando] || (parent[expando] = {}); - cache = outerCache[type] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[nodeIndex]; - - while ((node = ++nodeIndex && node && node[dir] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop())) { - - // When found, cache indexes on `parent` and break - if (node.nodeType === 1 && ++diff && node === elem) { - outerCache[type] = [dirruns, nodeIndex, diff]; - break; - } - } - - // Use previously-cached element index if available - } else if (useCache && (cache = (elem[expando] || (elem[expando] = {}))[type]) && cache[0] === dirruns) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ((node = ++nodeIndex && node && node[dir] || - (diff = nodeIndex = 0) || start.pop())) { - - if ((ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1) && ++diff) { - // Cache the index of each encountered element - if (useCache) { - (node[expando] || (node[expando] = {}))[type] = [dirruns, diff]; - } - - if (node === elem) { - break; - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || (diff % first === 0 && diff / first >= 0); - } - }; - }, - - "PSEUDO": function (pseudo, argument) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[pseudo] || Expr.setFilters[pseudo.toLowerCase()] || - Sizzle.error("unsupported pseudo: " + pseudo); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if (fn[expando]) { - return fn(argument); - } - - // But maintain support for old signatures - if (fn.length > 1) { - args = [pseudo, pseudo, "", argument]; - return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase()) ? - markFunction(function (seed, matches) { - var idx, - matched = fn(seed, argument), - i = matched.length; - while (i--) { - idx = indexOf.call(seed, matched[i]); - seed[idx] = !(matches[idx] = matched[i]); - } - }) : - function (elem) { - return fn(elem, 0, args); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function (selector) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile(selector.replace(rtrim, "$1")); - - return matcher[expando] ? - markFunction(function (seed, matches, context, xml) { - var elem, - unmatched = matcher(seed, null, xml, []), - i = seed.length; - - // Match elements unmatched by `matcher` - while (i--) { - if ((elem = unmatched[i])) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function (elem, context, xml) { - input[0] = elem; - matcher(input, null, xml, results); - return !results.pop(); - }; - }), - - "has": markFunction(function (selector) { - return function (elem) { - return Sizzle(selector, elem).length > 0; - }; - }), - - "contains": markFunction(function (text) { - return function (elem) { - return (elem.textContent || elem.innerText || getText(elem)).indexOf(text) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction(function (lang) { - // lang value must be a valid identifier - if (!ridentifier.test(lang || "")) { - Sizzle.error("unsupported lang: " + lang); - } - lang = lang.replace(runescape, funescape).toLowerCase(); - return function (elem) { - var elemLang; - do { - if ((elemLang = documentIsHTML ? - elem.lang : - elem.getAttribute("xml:lang") || elem.getAttribute("lang"))) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf(lang + "-") === 0; - } - } while ((elem = elem.parentNode) && elem.nodeType === 1); - return false; - }; - }), - - // Miscellaneous - "target": function (elem) { - var hash = window.location && window.location.hash; - return hash && hash.slice(1) === elem.id; - }, - - "root": function (elem) { - return elem === docElem; - }, - - "focus": function (elem) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !! (elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": function (elem) { - return elem.disabled === false; - }, - - "disabled": function (elem) { - return elem.disabled === true; - }, - - "checked": function (elem) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !! elem.checked) || (nodeName === "option" && !! elem.selected); - }, - - "selected": function (elem) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if (elem.parentNode) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function (elem) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") - for (elem = elem.firstChild; elem; elem = elem.nextSibling) { - if (elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4) { - return false; - } - } - return true; - }, - - "parent": function (elem) { - return !Expr.pseudos["empty"](elem); - }, - - // Element/input types - "header": function (elem) { - return rheader.test(elem.nodeName); - }, - - "input": function (elem) { - return rinputs.test(elem.nodeName); - }, - - "button": function (elem) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function (elem) { - var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - ((attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type); - }, - - // Position-in-collection - "first": createPositionalPseudo(function () { - return [0]; - }), - - "last": createPositionalPseudo(function (matchIndexes, length) { - return [length - 1]; - }), - - "eq": createPositionalPseudo(function (matchIndexes, length, argument) { - return [argument < 0 ? argument + length : argument]; - }), - - "even": createPositionalPseudo(function (matchIndexes, length) { - var i = 0; - for (; i < length; i += 2) { - matchIndexes.push(i); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function (matchIndexes, length) { - var i = 1; - for (; i < length; i += 2) { - matchIndexes.push(i); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function (matchIndexes, length, argument) { - var i = argument < 0 ? argument + length : argument; - for (; --i >= 0;) { - matchIndexes.push(i); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function (matchIndexes, length, argument) { - var i = argument < 0 ? argument + length : argument; - for (; ++i < length;) { - matchIndexes.push(i); - } - return matchIndexes; - }) - } - }; - - Expr.pseudos["nth"] = Expr.pseudos["eq"]; - - // Add button/input type pseudos - for (i in { - radio: true, - checkbox: true, - file: true, - password: true, - image: true - }) { - Expr.pseudos[i] = createInputPseudo(i); - } - for (i in { - submit: true, - reset: true - }) { - Expr.pseudos[i] = createButtonPseudo(i); - } - - // Easy API for creating new setFilters - - function setFilters() {} - setFilters.prototype = Expr.filters = Expr.pseudos; - Expr.setFilters = new setFilters(); - - function tokenize(selector, parseOnly) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[selector + " "]; - - if (cached) { - return parseOnly ? 0 : cached.slice(0); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while (soFar) { - - // Comma and first run - if (!matched || (match = rcomma.exec(soFar))) { - if (match) { - // Don't consume trailing commas as valid - soFar = soFar.slice(match[0].length) || soFar; - } - groups.push(tokens = []); - } - - matched = false; - - // Combinators - if ((match = rcombinators.exec(soFar))) { - matched = match.shift(); - tokens.push({ - value: matched, - // Cast descendant combinators to space - type: match[0].replace(rtrim, " ") - }); - soFar = soFar.slice(matched.length); - } - - // Filters - for (type in Expr.filter) { - if ((match = matchExpr[type].exec(soFar)) && (!preFilters[type] || - (match = preFilters[type](match)))) { - matched = match.shift(); - tokens.push({ - value: matched, - type: type, - matches: match - }); - soFar = soFar.slice(matched.length); - } - } - - if (!matched) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error(selector) : - // Cache the tokens - tokenCache(selector, groups).slice(0); - } - - function toSelector(tokens) { - var i = 0, - len = tokens.length, - selector = ""; - for (; i < len; i++) { - selector += tokens[i].value; - } - return selector; - } - - function addCombinator(matcher, combinator, base) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - - function (elem, context, xml) { - while ((elem = elem[dir])) { - if (elem.nodeType === 1 || checkNonElements) { - return matcher(elem, context, xml); - } - } - } : - - // Check against all ancestor/preceding elements - - function (elem, context, xml) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; - - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if (xml) { - while ((elem = elem[dir])) { - if (elem.nodeType === 1 || checkNonElements) { - if (matcher(elem, context, xml)) { - return true; - } - } - } - } else { - while ((elem = elem[dir])) { - if (elem.nodeType === 1 || checkNonElements) { - outerCache = elem[expando] || (elem[expando] = {}); - if ((cache = outerCache[dir]) && cache[0] === dirkey) { - if ((data = cache[1]) === true || data === cachedruns) { - return data === true; - } - } else { - cache = outerCache[dir] = [dirkey]; - cache[1] = matcher(elem, context, xml) || cachedruns; - if (cache[1] === true) { - return true; - } - } - } - } - } - }; - } - - function elementMatcher(matchers) { - return matchers.length > 1 ? - function (elem, context, xml) { - var i = matchers.length; - while (i--) { - if (!matchers[i](elem, context, xml)) { - return false; - } - } - return true; - } : - matchers[0]; - } - - function condense(unmatched, map, filter, context, xml) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for (; i < len; i++) { - if ((elem = unmatched[i])) { - if (!filter || filter(elem, context, xml)) { - newUnmatched.push(elem); - if (mapped) { - map.push(i); - } - } - } - } - - return newUnmatched; - } - - function setMatcher(preFilter, selector, matcher, postFilter, postFinder, postSelector) { - if (postFilter && !postFilter[expando]) { - postFilter = setMatcher(postFilter); - } - if (postFinder && !postFinder[expando]) { - postFinder = setMatcher(postFinder, postSelector); - } - return markFunction(function (seed, results, context, xml) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts(selector || "*", context.nodeType ? [context] : context, []), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && (seed || !selector) ? - condense(elems, preMap, preFilter, context, xml) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || (seed ? preFilter : preexisting || postFilter) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if (matcher) { - matcher(matcherIn, matcherOut, context, xml); - } - - // Apply postFilter - if (postFilter) { - temp = condense(matcherOut, postMap); - postFilter(temp, [], context, xml); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while (i--) { - if ((elem = temp[i])) { - matcherOut[postMap[i]] = !(matcherIn[postMap[i]] = elem); - } - } - } - - if (seed) { - if (postFinder || preFilter) { - if (postFinder) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while (i--) { - if ((elem = matcherOut[i])) { - // Restore matcherIn since elem is not yet a final match - temp.push((matcherIn[i] = elem)); - } - } - postFinder(null, (matcherOut = []), temp, xml); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while (i--) { - if ((elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call(seed, elem) : preMap[i]) > -1) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice(preexisting, matcherOut.length) : - matcherOut - ); - if (postFinder) { - postFinder(null, results, matcherOut, xml); - } else { - push.apply(results, matcherOut); - } - } - }); - } - - function matcherFromTokens(tokens) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[tokens[0].type], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator(function (elem) { - return elem === checkContext; - }, implicitRelative, true), - matchAnyContext = addCombinator(function (elem) { - return indexOf.call(checkContext, elem) > -1; - }, implicitRelative, true), - matchers = [ - function (elem, context, xml) { - return (!leadingRelative && (xml || context !== outermostContext)) || ( - (checkContext = context).nodeType ? - matchContext(elem, context, xml) : - matchAnyContext(elem, context, xml)); - } - ]; - - for (; i < len; i++) { - if ((matcher = Expr.relative[tokens[i].type])) { - matchers = [addCombinator(elementMatcher(matchers), matcher)]; - } else { - matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches); - - // Return special upon seeing a positional matcher - if (matcher[expando]) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for (; j < len; j++) { - if (Expr.relative[tokens[j].type]) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher(matchers), - i > 1 && toSelector( - // If the preceding token was a descendant combinator, insert an implicit any-element `*` - tokens.slice(0, i - 1).concat({ - value: tokens[i - 2].type === " " ? "*" : "" - }) - ).replace(rtrim, "$1"), - matcher, - i < j && matcherFromTokens(tokens.slice(i, j)), - j < len && matcherFromTokens((tokens = tokens.slice(j))), - j < len && toSelector(tokens) - ); - } - matchers.push(matcher); - } - } - - return elementMatcher(matchers); - } - - function matcherFromGroupMatchers(elementMatchers, setMatchers) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function (seed, context, xml, results, expandContext) { - var elem, j, matcher, - setMatched = [], - matchedCount = 0, - i = "0", - unmatched = seed && [], - outermost = expandContext != null, - contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]("*", expandContext && context.parentNode || context), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); - - if (outermost) { - outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; - } - - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for (; - (elem = elems[i]) != null; i++) { - if (byElement && elem) { - j = 0; - while ((matcher = elementMatchers[j++])) { - if (matcher(elem, context, xml)) { - results.push(elem); - break; - } - } - if (outermost) { - dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; - } - } - - // Track unmatched elements for set filters - if (bySet) { - // They will have gone through all possible matchers - if ((elem = !matcher && elem)) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if (seed) { - unmatched.push(elem); - } - } - } - - // Apply set filters to unmatched elements - matchedCount += i; - if (bySet && i !== matchedCount) { - j = 0; - while ((matcher = setMatchers[j++])) { - matcher(unmatched, setMatched, context, xml); - } - - if (seed) { - // Reintegrate element matches to eliminate the need for sorting - if (matchedCount > 0) { - while (i--) { - if (!(unmatched[i] || setMatched[i])) { - setMatched[i] = pop.call(results); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense(setMatched); - } - - // Add matches to results - push.apply(results, setMatched); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if (outermost && !seed && setMatched.length > 0 && - (matchedCount + setMatchers.length) > 1) { - - Sizzle.uniqueSort(results); - } - } - - // Override manipulation of globals by nested matchers - if (outermost) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction(superMatcher) : - superMatcher; - } - - compile = Sizzle.compile = function (selector, group /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[selector + " "]; - - if (!cached) { - // Generate a function of recursive functions that can be used to check each element - if (!group) { - group = tokenize(selector); - } - i = group.length; - while (i--) { - cached = matcherFromTokens(group[i]); - if (cached[expando]) { - setMatchers.push(cached); - } else { - elementMatchers.push(cached); - } - } - - // Cache the compiled function - cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers)); - } - return cached; - }; - - function multipleContexts(selector, contexts, results) { - var i = 0, - len = contexts.length; - for (; i < len; i++) { - Sizzle(selector, contexts[i], results); - } - return results; - } - - function select(selector, context, results, seed) { - var i, tokens, token, type, find, - match = tokenize(selector); - - if (!seed) { - // Try to minimize operations if there is only one group - if (match.length === 1) { - - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice(0); - if (tokens.length > 2 && (token = tokens[0]).type === "ID" && - support.getById && context.nodeType === 9 && documentIsHTML && - Expr.relative[tokens[1].type]) { - - context = (Expr.find["ID"](token.matches[0].replace(runescape, funescape), context) || [])[0]; - if (!context) { - return results; - } - selector = selector.slice(tokens.shift().value.length); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test(selector) ? 0 : tokens.length; - while (i--) { - token = tokens[i]; - - // Abort if we hit a combinator - if (Expr.relative[(type = token.type)]) { - break; - } - if ((find = Expr.find[type])) { - // Search, expanding context for leading sibling combinators - if ((seed = find( - token.matches[0].replace(runescape, funescape), - rsibling.test(tokens[0].type) && context.parentNode || context - ))) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice(i, 1); - selector = seed.length && toSelector(tokens); - if (!selector) { - push.apply(results, seed); - return results; - } - - break; - } - } - } - } - } - - // Compile and execute a filtering function - // Provide `match` to avoid retokenization if we modified the selector above - compile(selector, match)( - seed, - context, !documentIsHTML, - results, - rsibling.test(selector) - ); - return results; - } - - // One-time assignments - - // Sort stability - support.sortStable = expando.split("").sort(sortOrder).join("") === expando; - - // Support: Chrome<14 - // Always assume duplicates if they aren't passed to the comparison function - support.detectDuplicates = hasDuplicate; - - // Initialize against the default document - setDocument(); - - // Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) - // Detached nodes confoundingly follow *each other* - support.sortDetached = assert(function (div1) { - // Should return 1, but returns 4 (following) - return div1.compareDocumentPosition(document.createElement("div")) & 1; - }); - - // Support: IE<8 - // Prevent attribute/property "interpolation" - // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx - if (!assert(function (div) { - div.innerHTML = ""; - return div.firstChild.getAttribute("href") === "#"; - })) { - addHandle("type|href|height|width", function (elem, name, isXML) { - if (!isXML) { - return elem.getAttribute(name, name.toLowerCase() === "type" ? 1 : 2); - } - }); - } - - // Support: IE<9 - // Use defaultValue in place of getAttribute("value") - if (!support.attributes || !assert(function (div) { - div.innerHTML = ""; - div.firstChild.setAttribute("value", ""); - return div.firstChild.getAttribute("value") === ""; - })) { - addHandle("value", function (elem, name, isXML) { - if (!isXML && elem.nodeName.toLowerCase() === "input") { - return elem.defaultValue; - } - }); - } - - // Support: IE<9 - // Use getAttributeNode to fetch booleans when getAttribute lies - if (!assert(function (div) { - return div.getAttribute("disabled") == null; - })) { - addHandle(booleans, function (elem, name, isXML) { - var val; - if (!isXML) { - return (val = elem.getAttributeNode(name)) && val.specified ? - val.value : - elem[name] === true ? name.toLowerCase() : null; - } - }); - } - - jQuery.find = Sizzle; - jQuery.expr = Sizzle.selectors; - jQuery.expr[":"] = jQuery.expr.pseudos; - jQuery.unique = Sizzle.uniqueSort; - jQuery.text = Sizzle.getText; - jQuery.isXMLDoc = Sizzle.isXML; - jQuery.contains = Sizzle.contains; - - - })(window); - // String to Object options format cache - var optionsCache = {}; - - // Convert String-formatted options into Object-formatted ones and store in cache - - function createOptions(options) { - var object = optionsCache[options] = {}; - jQuery.each(options.match(core_rnotwhite) || [], function (_, flag) { - object[flag] = true; - }); - return object; - } - - /* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ - jQuery.Callbacks = function (options) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - (optionsCache[options] || createOptions(options)) : - jQuery.extend({}, options); - - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function (data) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for (; list && firingIndex < firingLength; firingIndex++) { - if (list[firingIndex].apply(data[0], data[1]) === false && options.stopOnFalse) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if (list) { - if (stack) { - if (stack.length) { - fire(stack.shift()); - } - } else if (memory) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function () { - if (list) { - // First, we save the current length - var start = list.length; - (function add(args) { - jQuery.each(args, function (_, arg) { - var type = jQuery.type(arg); - if (type === "function") { - if (!options.unique || !self.has(arg)) { - list.push(arg); - } - } else if (arg && arg.length && type !== "string") { - // Inspect recursively - add(arg); - } - }); - })(arguments); - // Do we need to add the callbacks to the - // current firing batch? - if (firing) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if (memory) { - firingStart = start; - fire(memory); - } - } - return this; - }, - // Remove a callback from the list - remove: function () { - if (list) { - jQuery.each(arguments, function (_, arg) { - var index; - while ((index = jQuery.inArray(arg, list, index)) > -1) { - list.splice(index, 1); - // Handle firing indexes - if (firing) { - if (index <= firingLength) { - firingLength--; - } - if (index <= firingIndex) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function (fn) { - return fn ? jQuery.inArray(fn, list) > -1 : !! (list && list.length); - }, - // Remove all callbacks from the list - empty: function () { - list = []; - firingLength = 0; - return this; - }, - // Have the list do nothing anymore - disable: function () { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function () { - return !list; - }, - // Lock the list in its current state - lock: function () { - stack = undefined; - if (!memory) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function () { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function (context, args) { - if (list && (!fired || stack)) { - args = args || []; - args = [context, args.slice ? args.slice() : args]; - if (firing) { - stack.push(args); - } else { - fire(args); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function () { - self.fireWith(this, arguments); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function () { - return !!fired; - } - }; - - return self; - }; - jQuery.extend({ - - Deferred: function (func) { - var tuples = [ - // action, add listener, listener list, final state - ["resolve", "done", jQuery.Callbacks("once memory"), "resolved"], - ["reject", "fail", jQuery.Callbacks("once memory"), "rejected"], - ["notify", "progress", jQuery.Callbacks("memory")] - ], - state = "pending", - promise = { - state: function () { - return state; - }, - always: function () { - deferred.done(arguments).fail(arguments); - return this; - }, - then: function ( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function (newDefer) { - jQuery.each(tuples, function (i, tuple) { - var action = tuple[0], - fn = jQuery.isFunction(fns[i]) && fns[i]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[tuple[1]](function () { - var returned = fn && fn.apply(this, arguments); - if (returned && jQuery.isFunction(returned.promise)) { - returned.promise() - .done(newDefer.resolve) - .fail(newDefer.reject) - .progress(newDefer.notify); - } else { - newDefer[action + "With"](this === promise ? newDefer.promise() : this, fn ? [returned] : arguments); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function (obj) { - return obj != null ? jQuery.extend(obj, promise) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; - - // Add list-specific methods - jQuery.each(tuples, function (i, tuple) { - var list = tuple[2], - stateString = tuple[3]; - - // promise[ done | fail | progress ] = list.add - promise[tuple[1]] = list.add; - - // Handle state - if (stateString) { - list.add(function () { - // state = [ resolved | rejected ] - state = stateString; - - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[i ^ 1][2].disable, tuples[2][2].lock); - } - - // deferred[ resolve | reject | notify ] - deferred[tuple[0]] = function () { - deferred[tuple[0] + "With"](this === deferred ? promise : this, arguments); - return this; - }; - deferred[tuple[0] + "With"] = list.fireWith; - }); - - // Make the deferred a promise - promise.promise(deferred); - - // Call given func if any - if (func) { - func.call(deferred, deferred); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function (subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = core_slice.call(arguments), - length = resolveValues.length, - - // the count of uncompleted subordinates - remaining = length !== 1 || (subordinate && jQuery.isFunction(subordinate.promise)) ? length : 0, - - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - - // Update function for both resolve and progress values - updateFunc = function (i, contexts, values) { - return function (value) { - contexts[i] = this; - values[i] = arguments.length > 1 ? core_slice.call(arguments) : value; - if (values === progressValues) { - deferred.notifyWith(contexts, values); - } else if (!(--remaining)) { - deferred.resolveWith(contexts, values); - } - }; - }, - - progressValues, progressContexts, resolveContexts; - - // add listeners to Deferred subordinates; treat others as resolved - if (length > 1) { - progressValues = new Array(length); - progressContexts = new Array(length); - resolveContexts = new Array(length); - for (; i < length; i++) { - if (resolveValues[i] && jQuery.isFunction(resolveValues[i].promise)) { - resolveValues[i].promise() - .done(updateFunc(i, resolveContexts, resolveValues)) - .fail(deferred.reject) - .progress(updateFunc(i, progressContexts, progressValues)); - } else { - --remaining; - } - } - } - - // if we're not waiting on anything, resolve the master - if (!remaining) { - deferred.resolveWith(resolveContexts, resolveValues); - } - - return deferred.promise(); - } - }); - jQuery.support = (function (support) { - - var all, a, input, select, fragment, opt, eventName, isSupported, i, - div = document.createElement("div"); - - // Setup - div.setAttribute("className", "t"); - div.innerHTML = "
{info.process.pid} {info.process.version} - {info.stats.onlineRegisteredCount} / - {info.stats.onlineGuestCount} / + {info.stats.onlineRegisteredCount} / + {info.stats.onlineGuestCount} / {info.stats.socketCount} {info.git.branch}@{info.git.hash}
a"; - - // Finish early in limited (non-browser) environments - all = div.getElementsByTagName("*") || []; - a = div.getElementsByTagName("a")[0]; - if (!a || !a.style || !all.length) { - return support; - } - - // First batch of tests - select = document.createElement("select"); - opt = select.appendChild(document.createElement("option")); - input = div.getElementsByTagName("input")[0]; - - a.style.cssText = "top:1px;float:left;opacity:.5"; - - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - support.getSetAttribute = div.className !== "t"; - - // IE strips leading whitespace when .innerHTML is used - support.leadingWhitespace = div.firstChild.nodeType === 3; - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - support.tbody = !div.getElementsByTagName("tbody").length; - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - support.htmlSerialize = !! div.getElementsByTagName("link").length; - - // Get the style information from getAttribute - // (IE uses .cssText instead) - support.style = /top/.test(a.getAttribute("style")); - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - support.hrefNormalized = a.getAttribute("href") === "/a"; - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - support.opacity = /^0.5/.test(a.style.opacity); - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - support.cssFloat = !! a.style.cssFloat; - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - support.checkOn = !! input.value; - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - support.optSelected = opt.selected; - - // Tests for enctype support on a form (#6743) - support.enctype = !! document.createElement("form").enctype; - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - support.html5Clone = document.createElement("nav").cloneNode(true).outerHTML !== "<:nav>"; - - // Will be defined later - support.inlineBlockNeedsLayout = false; - support.shrinkWrapBlocks = false; - support.pixelPosition = false; - support.deleteExpando = true; - support.noCloneEvent = true; - support.reliableMarginRight = true; - support.boxSizingReliable = true; - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode(true).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Support: IE<9 - try { - delete div.test; - } catch (e) { - support.deleteExpando = false; - } - - // Check if we can trust getAttribute("value") - input = document.createElement("input"); - input.setAttribute("value", ""); - support.input = input.getAttribute("value") === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute("type", "radio"); - support.radioValue = input.value === "t"; - - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute("checked", "t"); - input.setAttribute("name", "t"); - - fragment = document.createDocumentFragment(); - fragment.appendChild(input); - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - if (div.attachEvent) { - div.attachEvent("onclick", function () { - support.noCloneEvent = false; - }); - - div.cloneNode(true).click(); - } - - // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - for (i in { - submit: true, - change: true, - focusin: true - }) { - div.setAttribute(eventName = "on" + i, "t"); - - support[i + "Bubbles"] = eventName in window || div.attributes[eventName].expando === false; - } - - div.style.backgroundClip = "content-box"; - div.cloneNode(true).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - // Support: IE<9 - // Iteration over object's inherited properties before its own. - for (i in jQuery(support)) { - break; - } - support.ownLast = i !== "0"; - - // Run tests that need a body at doc ready - jQuery(function () { - var container, marginDiv, tds, - divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", - body = document.getElementsByTagName("body")[0]; - - if (!body) { - // Return for frameset docs that don't have a body - return; - } - - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; - - body.appendChild(container).appendChild(div); - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "
t
"; - tds = div.getElementsByTagName("td"); - tds[0].style.cssText = "padding:0;margin:0;border:0;display:none"; - isSupported = (tds[0].offsetHeight === 0); - - tds[0].style.display = ""; - tds[1].style.display = "none"; - - // Support: IE8 - // Check if empty table cells still have offsetWidth/Height - support.reliableHiddenOffsets = isSupported && (tds[0].offsetHeight === 0); - - // Check box-sizing and margin behavior. - div.innerHTML = ""; - div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; - - // Workaround failing boxSizing test due to offsetWidth returning wrong value - // with some non-1 values of body zoom, ticket #13543 - jQuery.swap(body, body.style.zoom != null ? { - zoom: 1 - } : {}, function () { - support.boxSizing = div.offsetWidth === 4; - }); - - // Use window.getComputedStyle because jsdom on node.js will break without it. - if (window.getComputedStyle) { - support.pixelPosition = (window.getComputedStyle(div, null) || {}).top !== "1%"; - support.boxSizingReliable = (window.getComputedStyle(div, null) || { - width: "4px" - }).width === "4px"; - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild(document.createElement("div")); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; - - support.reliableMarginRight = !parseFloat((window.getComputedStyle(marginDiv, null) || {}).marginRight); - } - - if (typeof div.style.zoom !== core_strundefined) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.innerHTML = ""; - div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; - support.inlineBlockNeedsLayout = (div.offsetWidth === 3); - - // Support: IE6 - // Check if elements with layout shrink-wrap their children - div.style.display = "block"; - div.innerHTML = "
"; - div.firstChild.style.width = "5px"; - support.shrinkWrapBlocks = (div.offsetWidth !== 3); - - if (support.inlineBlockNeedsLayout) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; - } - } - - body.removeChild(container); - - // Null elements to avoid leaks in IE - container = div = tds = marginDiv = null; - }); - - // Null elements to avoid leaks in IE - all = select = fragment = opt = a = input = null; - - return support; - })({}); - - var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; - - function internalData(elem, name, data, pvt /* Internal Use Only */ ) { - if (!jQuery.acceptData(elem)) { - return; - } - - var ret, thisCache, - internalKey = jQuery.expando, - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[internalKey] : elem[internalKey] && internalKey; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ((!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string") { - return; - } - - if (!id) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if (isNode) { - id = elem[internalKey] = core_deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; - } - } - - if (!cache[id]) { - // Avoid exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - cache[id] = isNode ? {} : { - toJSON: jQuery.noop - }; - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if (typeof name === "object" || typeof name === "function") { - if (pvt) { - cache[id] = jQuery.extend(cache[id], name); - } else { - cache[id].data = jQuery.extend(cache[id].data, name); - } - } - - thisCache = cache[id]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if (!pvt) { - if (!thisCache.data) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if (data !== undefined) { - thisCache[jQuery.camelCase(name)] = data; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if (typeof name === "string") { - - // First Try to find as-is property data - ret = thisCache[name]; - - // Test for null|undefined property data - if (ret == null) { - - // Try to find the camelCased property - ret = thisCache[jQuery.camelCase(name)]; - } - } else { - ret = thisCache; - } - - return ret; - } - - function internalRemoveData(elem, name, pvt) { - if (!jQuery.acceptData(elem)) { - return; - } - - var thisCache, i, - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[jQuery.expando] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if (!cache[id]) { - return; - } - - if (name) { - - thisCache = pvt ? cache[id] : cache[id].data; - - if (thisCache) { - - // Support array or space separated string names for data keys - if (!jQuery.isArray(name)) { - - // try the string as a key before any manipulation - if (name in thisCache) { - name = [name]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase(name); - if (name in thisCache) { - name = [name]; - } else { - name = name.split(" "); - } - } - } else { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = name.concat(jQuery.map(name, jQuery.camelCase)); - } - - i = name.length; - while (i--) { - delete thisCache[name[i]]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if (pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache)) { - return; - } - } - } - - // See jQuery.data for more information - if (!pvt) { - delete cache[id].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if (!isEmptyDataObject(cache[id])) { - return; - } - } - - // Destroy the cache - if (isNode) { - jQuery.cleanData([elem], true); - - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - /* jshint eqeqeq: false */ - } else if (jQuery.support.deleteExpando || cache != cache.window) { - /* jshint eqeqeq: true */ - delete cache[id]; - - // When all else fails, null - } else { - cache[id] = null; - } - } - - jQuery.extend({ - cache: {}, - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "applet": true, - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" - }, - - hasData: function (elem) { - elem = elem.nodeType ? jQuery.cache[elem[jQuery.expando]] : elem[jQuery.expando]; - return !!elem && !isEmptyDataObject(elem); - }, - - data: function (elem, name, data) { - return internalData(elem, name, data); - }, - - removeData: function (elem, name) { - return internalRemoveData(elem, name); - }, - - // For internal use only. - _data: function (elem, name, data) { - return internalData(elem, name, data, true); - }, - - _removeData: function (elem, name) { - return internalRemoveData(elem, name, true); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function (elem) { - // Do not set data on non-element because it will not be cleared (#8335). - if (elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9) { - return false; - } - - var noData = elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]; - - // nodes accept data unless otherwise specified; rejection can be conditional - return !noData || noData !== true && elem.getAttribute("classid") === noData; - } - }); - - jQuery.fn.extend({ - data: function (key, value) { - var attrs, name, - data = null, - i = 0, - elem = this[0]; - - // Special expections of .data basically thwart jQuery.access, - // so implement the relevant behavior ourselves - - // Gets all values - if (key === undefined) { - if (this.length) { - data = jQuery.data(elem); - - if (elem.nodeType === 1 && !jQuery._data(elem, "parsedAttrs")) { - attrs = elem.attributes; - for (; i < attrs.length; i++) { - name = attrs[i].name; - - if (name.indexOf("data-") === 0) { - name = jQuery.camelCase(name.slice(5)); - - dataAttr(elem, name, data[name]); - } - } - jQuery._data(elem, "parsedAttrs", true); - } - } - - return data; - } - - // Sets multiple values - if (typeof key === "object") { - return this.each(function () { - jQuery.data(this, key); - }); - } - - return arguments.length > 1 ? - - // Sets one value - this.each(function () { - jQuery.data(this, key, value); - }) : - - // Gets one value - // Try to fetch any internally stored data first - elem ? dataAttr(elem, key, jQuery.data(elem, key)) : null; - }, - - removeData: function (key) { - return this.each(function () { - jQuery.removeData(this, key); - }); - } - }); - - function dataAttr(elem, key, data) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if (data === undefined && elem.nodeType === 1) { - - var name = "data-" + key.replace(rmultiDash, "-$1").toLowerCase(); - - data = elem.getAttribute(name); - - if (typeof data === "string") { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test(data) ? jQuery.parseJSON(data) : - data; - } catch (e) {} - - // Make sure we set the data so it isn't changed later - jQuery.data(elem, key, data); - - } else { - data = undefined; - } - } - - return data; - } - - // checks a cache object for emptiness - - function isEmptyDataObject(obj) { - var name; - for (name in obj) { - - // if the public data object is empty, the private is still empty - if (name === "data" && jQuery.isEmptyObject(obj[name])) { - continue; - } - if (name !== "toJSON") { - return false; - } - } - - return true; - } - jQuery.extend({ - queue: function (elem, type, data) { - var queue; - - if (elem) { - type = (type || "fx") + "queue"; - queue = jQuery._data(elem, type); - - // Speed up dequeue by getting out quickly if this is just a lookup - if (data) { - if (!queue || jQuery.isArray(data)) { - queue = jQuery._data(elem, type, jQuery.makeArray(data)); - } else { - queue.push(data); - } - } - return queue || []; - } - }, - - dequeue: function (elem, type) { - type = type || "fx"; - - var queue = jQuery.queue(elem, type), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks(elem, type), - next = function () { - jQuery.dequeue(elem, type); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if (fn === "inprogress") { - fn = queue.shift(); - startLength--; - } - - if (fn) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if (type === "fx") { - queue.unshift("inprogress"); - } - - // clear up the last queue stop function - delete hooks.stop; - fn.call(elem, next, hooks); - } - - if (!startLength && hooks) { - hooks.empty.fire(); - } - }, - - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function (elem, type) { - var key = type + "queueHooks"; - return jQuery._data(elem, key) || jQuery._data(elem, key, { - empty: jQuery.Callbacks("once memory").add(function () { - jQuery._removeData(elem, type + "queue"); - jQuery._removeData(elem, key); - }) - }); - } - }); - - jQuery.fn.extend({ - queue: function (type, data) { - var setter = 2; - - if (typeof type !== "string") { - data = type; - type = "fx"; - setter--; - } - - if (arguments.length < setter) { - return jQuery.queue(this[0], type); - } - - return data === undefined ? - this : - this.each(function () { - var queue = jQuery.queue(this, type, data); - - // ensure a hooks for this queue - jQuery._queueHooks(this, type); - - if (type === "fx" && queue[0] !== "inprogress") { - jQuery.dequeue(this, type); - } - }); - }, - dequeue: function (type) { - return this.each(function () { - jQuery.dequeue(this, type); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function (time, type) { - time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; - type = type || "fx"; - - return this.queue(type, function (next, hooks) { - var timeout = setTimeout(next, time); - hooks.stop = function () { - clearTimeout(timeout); - }; - }); - }, - clearQueue: function (type) { - return this.queue(type || "fx", []); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function (type, obj) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function () { - if (!(--count)) { - defer.resolveWith(elements, [elements]); - } - }; - - if (typeof type !== "string") { - obj = type; - type = undefined; - } - type = type || "fx"; - - while (i--) { - tmp = jQuery._data(elements[i], type + "queueHooks"); - if (tmp && tmp.empty) { - count++; - tmp.empty.add(resolve); - } - } - resolve(); - return defer.promise(obj); - } - }); - var nodeHook, boolHook, - rclass = /[\t\r\n\f]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - getSetInput = jQuery.support.input; - - jQuery.fn.extend({ - attr: function (name, value) { - return jQuery.access(this, jQuery.attr, name, value, arguments.length > 1); - }, - - removeAttr: function (name) { - return this.each(function () { - jQuery.removeAttr(this, name); - }); - }, - - prop: function (name, value) { - return jQuery.access(this, jQuery.prop, name, value, arguments.length > 1); - }, - - removeProp: function (name) { - name = jQuery.propFix[name] || name; - return this.each(function () { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[name] = undefined; - delete this[name]; - } catch (e) {} - }); - }, - - addClass: function (value) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; - - if (jQuery.isFunction(value)) { - return this.each(function (j) { - jQuery(this).addClass(value.call(this, j, this.className)); - }); - } - - if (proceed) { - // The disjunction here is for better compressibility (see removeClass) - classes = (value || "").match(core_rnotwhite) || []; - - for (; i < len; i++) { - elem = this[i]; - cur = elem.nodeType === 1 && (elem.className ? - (" " + elem.className + " ").replace(rclass, " ") : - " " - ); - - if (cur) { - j = 0; - while ((clazz = classes[j++])) { - if (cur.indexOf(" " + clazz + " ") < 0) { - cur += clazz + " "; - } - } - elem.className = jQuery.trim(cur); - - } - } - } - - return this; - }, - - removeClass: function (value) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; - - if (jQuery.isFunction(value)) { - return this.each(function (j) { - jQuery(this).removeClass(value.call(this, j, this.className)); - }); - } - if (proceed) { - classes = (value || "").match(core_rnotwhite) || []; - - for (; i < len; i++) { - elem = this[i]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && (elem.className ? - (" " + elem.className + " ").replace(rclass, " ") : - "" - ); - - if (cur) { - j = 0; - while ((clazz = classes[j++])) { - // Remove *all* instances - while (cur.indexOf(" " + clazz + " ") >= 0) { - cur = cur.replace(" " + clazz + " ", " "); - } - } - elem.className = value ? jQuery.trim(cur) : ""; - } - } - } - - return this; - }, - - toggleClass: function (value, stateVal) { - var type = typeof value; - - if (typeof stateVal === "boolean" && type === "string") { - return stateVal ? this.addClass(value) : this.removeClass(value); - } - - if (jQuery.isFunction(value)) { - return this.each(function (i) { - jQuery(this).toggleClass(value.call(this, i, this.className, stateVal), stateVal); - }); - } - - return this.each(function () { - if (type === "string") { - // toggle individual class names - var className, - i = 0, - self = jQuery(this), - classNames = value.match(core_rnotwhite) || []; - - while ((className = classNames[i++])) { - // check each className given, space separated list - if (self.hasClass(className)) { - self.removeClass(className); - } else { - self.addClass(className); - } - } - - // Toggle whole class name - } else if (type === core_strundefined || type === "boolean") { - if (this.className) { - // store className if set - jQuery._data(this, "__className__", this.className); - } - - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data(this, "__className__") || ""; - } - }); - }, - - hasClass: function (selector) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for (; i < l; i++) { - if (this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf(className) >= 0) { - return true; - } - } - - return false; - }, - - val: function (value) { - var ret, hooks, isFunction, - elem = this[0]; - - if (!arguments.length) { - if (elem) { - hooks = jQuery.valHooks[elem.type] || jQuery.valHooks[elem.nodeName.toLowerCase()]; - - if (hooks && "get" in hooks && (ret = hooks.get(elem, "value")) !== undefined) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction(value); - - return this.each(function (i) { - var val; - - if (this.nodeType !== 1) { - return; - } - - if (isFunction) { - val = value.call(this, i, jQuery(this).val()); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if (val == null) { - val = ""; - } else if (typeof val === "number") { - val += ""; - } else if (jQuery.isArray(val)) { - val = jQuery.map(val, function (value) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[this.type] || jQuery.valHooks[this.nodeName.toLowerCase()]; - - // If set returns undefined, fall back to normal setting - if (!hooks || !("set" in hooks) || hooks.set(this, val, "value") === undefined) { - this.value = val; - } - }); - } - }); - - jQuery.extend({ - valHooks: { - option: { - get: function (elem) { - // Use proper attribute retrieval(#6932, #12072) - var val = jQuery.find.attr(elem, "value"); - return val != null ? - val : - elem.text; - } - }, - select: { - get: function (elem) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for (; i < max; i++) { - option = options[i]; - - // oldIE doesn't update selected after form reset (#2551) - if ((option.selected || i === index) && - // Don't return options that are disabled or in a disabled optgroup - (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && - (!option.parentNode.disabled || !jQuery.nodeName(option.parentNode, "optgroup"))) { - - // Get the specific value for the option - value = jQuery(option).val(); - - // We don't need an array for one selects - if (one) { - return value; - } - - // Multi-Selects return an array - values.push(value); - } - } - - return values; - }, - - set: function (elem, value) { - var optionSet, option, - options = elem.options, - values = jQuery.makeArray(value), - i = options.length; - - while (i--) { - option = options[i]; - if ((option.selected = jQuery.inArray(jQuery(option).val(), values) >= 0)) { - optionSet = true; - } - } - - // force browsers to behave consistently when non-matching value is set - if (!optionSet) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attr: function (elem, name, value) { - var hooks, ret, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if (!elem || nType === 3 || nType === 8 || nType === 2) { - return; - } - - // Fallback to prop when attributes are not supported - if (typeof elem.getAttribute === core_strundefined) { - return jQuery.prop(elem, name, value); - } - - // All attributes are lowercase - // Grab necessary hook if one is defined - if (nType !== 1 || !jQuery.isXMLDoc(elem)) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[name] || - (jQuery.expr.match.bool.test(name) ? boolHook : nodeHook); - } - - if (value !== undefined) { - - if (value === null) { - jQuery.removeAttr(elem, name); - - } else if (hooks && "set" in hooks && (ret = hooks.set(elem, value, name)) !== undefined) { - return ret; - - } else { - elem.setAttribute(name, value + ""); - return value; - } - - } else if (hooks && "get" in hooks && (ret = hooks.get(elem, name)) !== null) { - return ret; - - } else { - ret = jQuery.find.attr(elem, name); - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; - } - }, - - removeAttr: function (elem, value) { - var name, propName, - i = 0, - attrNames = value && value.match(core_rnotwhite); - - if (attrNames && elem.nodeType === 1) { - while ((name = attrNames[i++])) { - propName = jQuery.propFix[name] || name; - - // Boolean attributes get special treatment (#10870) - if (jQuery.expr.match.bool.test(name)) { - // Set corresponding property to false - if (getSetInput && getSetAttribute || !ruseDefault.test(name)) { - elem[propName] = false; - // Support: IE<9 - // Also clear defaultChecked/defaultSelected (if appropriate) - } else { - elem[jQuery.camelCase("default-" + name)] = - elem[propName] = false; - } - - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr(elem, name, ""); - } - - elem.removeAttribute(getSetAttribute ? name : propName); - } - } - }, - - attrHooks: { - type: { - set: function (elem, value) { - if (!jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input")) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute("type", value); - if (val) { - elem.value = val; - } - return value; - } - } - } - }, - - propFix: { - "for": "htmlFor", - "class": "className" - }, - - prop: function (elem, name, value) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if (!elem || nType === 3 || nType === 8 || nType === 2) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc(elem); - - if (notxml) { - // Fix name and attach hooks - name = jQuery.propFix[name] || name; - hooks = jQuery.propHooks[name]; - } - - if (value !== undefined) { - return hooks && "set" in hooks && (ret = hooks.set(elem, value, name)) !== undefined ? - ret : - (elem[name] = value); - - } else { - return hooks && "get" in hooks && (ret = hooks.get(elem, name)) !== null ? - ret : - elem[name]; - } - }, - - propHooks: { - tabIndex: { - get: function (elem) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - // Use proper attribute retrieval(#12072) - var tabindex = jQuery.find.attr(elem, "tabindex"); - - return tabindex ? - parseInt(tabindex, 10) : - rfocusable.test(elem.nodeName) || rclickable.test(elem.nodeName) && elem.href ? - 0 : -1; - } - } - } - }); - - // Hooks for boolean attributes - boolHook = { - set: function (elem, value, name) { - if (value === false) { - // Remove boolean attributes when set to false - jQuery.removeAttr(elem, name); - } else if (getSetInput && getSetAttribute || !ruseDefault.test(name)) { - // IE<8 needs the *property* name - elem.setAttribute(!getSetAttribute && jQuery.propFix[name] || name, name); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[jQuery.camelCase("default-" + name)] = elem[name] = true; - } - - return name; - } - }; - jQuery.each(jQuery.expr.match.bool.source.match(/\w+/g), function (i, name) { - var getter = jQuery.expr.attrHandle[name] || jQuery.find.attr; - - jQuery.expr.attrHandle[name] = getSetInput && getSetAttribute || !ruseDefault.test(name) ? - function (elem, name, isXML) { - var fn = jQuery.expr.attrHandle[name], - ret = isXML ? - undefined : - /* jshint eqeqeq: false */ - (jQuery.expr.attrHandle[name] = undefined) != - getter(elem, name, isXML) ? - - name.toLowerCase() : - null; - jQuery.expr.attrHandle[name] = fn; - return ret; - } : - function (elem, name, isXML) { - return isXML ? - undefined : - elem[jQuery.camelCase("default-" + name)] ? - name.toLowerCase() : - null; - }; - }); - - // fix oldIE attroperties - if (!getSetInput || !getSetAttribute) { - jQuery.attrHooks.value = { - set: function (elem, value, name) { - if (jQuery.nodeName(elem, "input")) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set(elem, value, name); - } - } - }; - } - - // IE6/7 do not support getting/setting some attributes with get/setAttribute - if (!getSetAttribute) { - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = { - set: function (elem, value, name) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode(name); - if (!ret) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute(name)) - ); - } - - ret.value = value += ""; - - // Break association with cloned elements by also using setAttribute (#9646) - return name === "value" || value === elem.getAttribute(name) ? - value : - undefined; - } - }; - jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = - // Some attributes are constructed with empty-string values when not defined - - function (elem, name, isXML) { - var ret; - return isXML ? - undefined : - (ret = elem.getAttributeNode(name)) && ret.value !== "" ? - ret.value : - null; - }; - jQuery.valHooks.button = { - get: function (elem, name) { - var ret = elem.getAttributeNode(name); - return ret && ret.specified ? - ret.value : - undefined; - }, - set: nodeHook.set - }; - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - set: function (elem, value, name) { - nodeHook.set(elem, value === "" ? false : value, name); - } - }; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each(["width", "height"], function (i, name) { - jQuery.attrHooks[name] = { - set: function (elem, value) { - if (value === "") { - elem.setAttribute(name, "auto"); - return value; - } - } - }; - }); - } - - - // Some attributes require a special call on IE - // http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx - if (!jQuery.support.hrefNormalized) { - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each(["href", "src"], function (i, name) { - jQuery.propHooks[name] = { - get: function (elem) { - return elem.getAttribute(name, 4); - } - }; - }); - } - - if (!jQuery.support.style) { - jQuery.attrHooks.style = { - get: function (elem) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function (elem, value) { - return (elem.style.cssText = value + ""); - } - }; - } - - // Safari mis-reports the default selected property of an option - // Accessing the parent's selectedIndex property fixes it - if (!jQuery.support.optSelected) { - jQuery.propHooks.selected = { - get: function (elem) { - var parent = elem.parentNode; - - if (parent) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if (parent.parentNode) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }; - } - - jQuery.each([ - "tabIndex", - "readOnly", - "maxLength", - "cellSpacing", - "cellPadding", - "rowSpan", - "colSpan", - "useMap", - "frameBorder", - "contentEditable" - ], function () { - jQuery.propFix[this.toLowerCase()] = this; - }); - - // IE6/7 call enctype encoding - if (!jQuery.support.enctype) { - jQuery.propFix.enctype = "encoding"; - } - - // Radios and checkboxes getter/setter - jQuery.each(["radio", "checkbox"], function () { - jQuery.valHooks[this] = { - set: function (elem, value) { - if (jQuery.isArray(value)) { - return (elem.checked = jQuery.inArray(jQuery(elem).val(), value) >= 0); - } - } - }; - if (!jQuery.support.checkOn) { - jQuery.valHooks[this].get = function (elem) { - // Support: Webkit - // "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - }; - } - }); - var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - - function returnTrue() { - return true; - } - - function returnFalse() { - return false; - } - - function safeActiveElement() { - try { - return document.activeElement; - } catch (err) {} - } - - /* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ - jQuery.event = { - - global: {}, - - add: function (elem, types, handler, data, selector) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data(elem); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if (!elemData) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if (handler.handler) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if (!handler.guid) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if (!(events = elemData.events)) { - events = elemData.events = {}; - } - if (!(eventHandle = elemData.handle)) { - eventHandle = elemData.handle = function (e) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply(eventHandle.elem, arguments) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - types = (types || "").match(core_rnotwhite) || [""]; - t = types.length; - while (t--) { - tmp = rtypenamespace.exec(types[t]) || []; - type = origType = tmp[1]; - namespaces = (tmp[2] || "").split(".").sort(); - - // There *must* be a type, no attaching namespace-only handlers - if (!type) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[type] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = (selector ? special.delegateType : special.bindType) || type; - - // Update special based on newly reset type - special = jQuery.event.special[type] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test(selector), - namespace: namespaces.join(".") - }, handleObjIn); - - // Init the event handler queue if we're the first - if (!(handlers = events[type])) { - handlers = events[type] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if (!special.setup || special.setup.call(elem, data, namespaces, eventHandle) === false) { - // Bind the global event handler to the element - if (elem.addEventListener) { - elem.addEventListener(type, eventHandle, false); - - } else if (elem.attachEvent) { - elem.attachEvent("on" + type, eventHandle); - } - } - } - - if (special.add) { - special.add.call(elem, handleObj); - - if (!handleObj.handler.guid) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if (selector) { - handlers.splice(handlers.delegateCount++, 0, handleObj); - } else { - handlers.push(handleObj); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[type] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - // Detach an event or set of events from an element - remove: function (elem, types, handler, selector, mappedTypes) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData(elem) && jQuery._data(elem); - - if (!elemData || !(events = elemData.events)) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = (types || "").match(core_rnotwhite) || [""]; - t = types.length; - while (t--) { - tmp = rtypenamespace.exec(types[t]) || []; - type = origType = tmp[1]; - namespaces = (tmp[2] || "").split(".").sort(); - - // Unbind all events (on this namespace, if provided) for the element - if (!type) { - for (type in events) { - jQuery.event.remove(elem, type + types[t], handler, selector, true); - } - continue; - } - - special = jQuery.event.special[type] || {}; - type = (selector ? special.delegateType : special.bindType) || type; - handlers = events[type] || []; - tmp = tmp[2] && new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)"); - - // Remove matching events - origCount = j = handlers.length; - while (j--) { - handleObj = handlers[j]; - - if ((mappedTypes || origType === handleObj.origType) && - (!handler || handler.guid === handleObj.guid) && - (!tmp || tmp.test(handleObj.namespace)) && - (!selector || selector === handleObj.selector || selector === "**" && handleObj.selector)) { - handlers.splice(j, 1); - - if (handleObj.selector) { - handlers.delegateCount--; - } - if (special.remove) { - special.remove.call(elem, handleObj); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if (origCount && !handlers.length) { - if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) { - jQuery.removeEvent(elem, type, elemData.handle); - } - - delete events[type]; - } - } - - // Remove the expando if it's no longer used - if (jQuery.isEmptyObject(events)) { - delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData(elem, "events"); - } - }, - - trigger: function (event, data, elem, onlyHandlers) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [elem || document], - type = core_hasOwn.call(event, "type") ? event.type : event, - namespaces = core_hasOwn.call(event, "namespace") ? event.namespace.split(".") : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if (elem.nodeType === 3 || elem.nodeType === 8) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if (rfocusMorph.test(type + jQuery.event.triggered)) { - return; - } - - if (type.indexOf(".") >= 0) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[jQuery.expando] ? - event : - new jQuery.Event(type, typeof event === "object" && event); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)") : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if (!event.target) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [event] : - jQuery.makeArray(data, [event]); - - // Allow special events to draw outside the lines - special = jQuery.event.special[type] || {}; - if (!onlyHandlers && special.trigger && special.trigger.apply(elem, data) === false) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if (!onlyHandlers && !special.noBubble && !jQuery.isWindow(elem)) { - - bubbleType = special.delegateType || type; - if (!rfocusMorph.test(bubbleType + type)) { - cur = cur.parentNode; - } - for (; cur; cur = cur.parentNode) { - eventPath.push(cur); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if (tmp === (elem.ownerDocument || document)) { - eventPath.push(tmp.defaultView || tmp.parentWindow || window); - } - } - - // Fire handlers on the event path - i = 0; - while ((cur = eventPath[i++]) && !event.isPropagationStopped()) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = (jQuery._data(cur, "events") || {})[event.type] && jQuery._data(cur, "handle"); - if (handle) { - handle.apply(cur, data); - } - - // Native handler - handle = ontype && cur[ontype]; - if (handle && jQuery.acceptData(cur) && handle.apply && handle.apply(cur, data) === false) { - event.preventDefault(); - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if (!onlyHandlers && !event.isDefaultPrevented()) { - - if ((!special._default || special._default.apply(eventPath.pop(), data) === false) && - jQuery.acceptData(elem)) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if (ontype && elem[type] && !jQuery.isWindow(elem)) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ontype]; - - if (tmp) { - elem[ontype] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[type](); - } catch (e) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; - - if (tmp) { - elem[ontype] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function (event) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix(event); - - var i, ret, handleObj, matched, j, - handlerQueue = [], - args = core_slice.call(arguments), - handlers = (jQuery._data(this, "events") || {})[event.type] || [], - special = jQuery.event.special[event.type] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if (special.preDispatch && special.preDispatch.call(this, event) === false) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call(this, event, handlers); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ((matched = handlerQueue[i++]) && !event.isPropagationStopped()) { - event.currentTarget = matched.elem; - - j = 0; - while ((handleObj = matched.handlers[j++]) && !event.isImmediatePropagationStopped()) { - - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if (!event.namespace_re || event.namespace_re.test(handleObj.namespace)) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ((jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler) - .apply(matched.elem, args); - - if (ret !== undefined) { - if ((event.result = ret) === false) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if (special.postDispatch) { - special.postDispatch.call(this, event); - } - - return event.result; - }, - - handlers: function (event, handlers) { - var sel, handleObj, matches, i, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if (delegateCount && cur.nodeType && (!event.button || event.type !== "click")) { - - /* jshint eqeqeq: false */ - for (; cur != this; cur = cur.parentNode || this) { - /* jshint eqeqeq: true */ - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if (cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click")) { - matches = []; - for (i = 0; i < delegateCount; i++) { - handleObj = handlers[i]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if (matches[sel] === undefined) { - matches[sel] = handleObj.needsContext ? - jQuery(sel, this).index(cur) >= 0 : - jQuery.find(sel, this, null, [cur]).length; - } - if (matches[sel]) { - matches.push(handleObj); - } - } - if (matches.length) { - handlerQueue.push({ - elem: cur, - handlers: matches - }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if (delegateCount < handlers.length) { - handlerQueue.push({ - elem: this, - handlers: handlers.slice(delegateCount) - }); - } - - return handlerQueue; - }, - - fix: function (event) { - if (event[jQuery.expando]) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[type]; - - if (!fixHook) { - this.fixHooks[type] = fixHook = - rmouseEvent.test(type) ? this.mouseHooks : - rkeyEvent.test(type) ? this.keyHooks : {}; - } - copy = fixHook.props ? this.props.concat(fixHook.props) : this.props; - - event = new jQuery.Event(originalEvent); - - i = copy.length; - while (i--) { - prop = copy[i]; - event[prop] = originalEvent[prop]; - } - - // Support: IE<9 - // Fix target property (#1925) - if (!event.target) { - event.target = originalEvent.srcElement || document; - } - - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if (event.target.nodeType === 3) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !! event.metaKey; - - return fixHook.filter ? fixHook.filter(event, originalEvent) : event; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function (event, original) { - - // Add which for key events - if (event.which == null) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function (event, original) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if (event.pageX == null && original.clientX != null) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = original.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Add relatedTarget, if necessary - if (!event.relatedTarget && fromElement) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if (!event.which && button !== undefined) { - event.which = (button & 1 ? 1 : (button & 2 ? 3 : (button & 4 ? 2 : 0))); - } - - return event; - } - }, - - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function () { - if (this !== safeActiveElement() && this.focus) { - try { - this.focus(); - return false; - } catch (e) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function () { - if (this === safeActiveElement() && this.blur) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function () { - if (jQuery.nodeName(this, "input") && this.type === "checkbox" && this.click) { - this.click(); - return false; - } - }, - - // For cross-browser consistency, don't fire native .click() on links - _default: function (event) { - return jQuery.nodeName(event.target, "a"); - } - }, - - beforeunload: { - postDispatch: function (event) { - - // Even when returnValue equals to undefined Firefox will still show alert - if (event.result !== undefined) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - simulate: function (type, elem, event, bubble) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, { - type: type, - isSimulated: true, - originalEvent: {} - } - ); - if (bubble) { - jQuery.event.trigger(e, null, elem); - } else { - jQuery.event.dispatch.call(elem, e); - } - if (e.isDefaultPrevented()) { - event.preventDefault(); - } - } - }; - - jQuery.removeEvent = document.removeEventListener ? - function (elem, type, handle) { - if (elem.removeEventListener) { - elem.removeEventListener(type, handle, false); - } - } : - function (elem, type, handle) { - var name = "on" + type; - - if (elem.detachEvent) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if (typeof elem[name] === core_strundefined) { - elem[name] = null; - } - - elem.detachEvent(name, handle); - } - }; - - jQuery.Event = function (src, props) { - // Allow instantiation without the 'new' keyword - if (!(this instanceof jQuery.Event)) { - return new jQuery.Event(src, props); - } - - // Event object - if (src && src.type) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if (props) { - jQuery.extend(this, props); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[jQuery.expando] = true; - }; - - // jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding - // http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html - jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function () { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - if (!e) { - return; - } - - // If preventDefault exists, run it on the original event - if (e.preventDefault) { - e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function () { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - if (!e) { - return; - } - // If stopPropagation exists, run it on the original event - if (e.stopPropagation) { - e.stopPropagation(); - } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function () { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - } - }; - - // Create mouseenter/leave events using mouseover/out and event-time checks - jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" - }, function (orig, fix) { - jQuery.event.special[orig] = { - delegateType: fix, - bindType: fix, - - handle: function (event) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if (!related || (related !== target && !jQuery.contains(target, related))) { - event.type = handleObj.origType; - ret = handleObj.handler.apply(this, arguments); - event.type = fix; - } - return ret; - } - }; - }); - - // IE submit delegation - if (!jQuery.support.submitBubbles) { - - jQuery.event.special.submit = { - setup: function () { - // Only need this for delegated form submit events - if (jQuery.nodeName(this, "form")) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add(this, "click._submit keypress._submit", function (e) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName(elem, "input") || jQuery.nodeName(elem, "button") ? elem.form : undefined; - if (form && !jQuery._data(form, "submitBubbles")) { - jQuery.event.add(form, "submit._submit", function (event) { - event._submit_bubble = true; - }); - jQuery._data(form, "submitBubbles", true); - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function (event) { - // If form was submitted by the user, bubble the event up the tree - if (event._submit_bubble) { - delete event._submit_bubble; - if (this.parentNode && !event.isTrigger) { - jQuery.event.simulate("submit", this.parentNode, event, true); - } - } - }, - - teardown: function () { - // Only need this for delegated form submit events - if (jQuery.nodeName(this, "form")) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove(this, "._submit"); - } - }; - } - - // IE change delegation and checkbox/radio fix - if (!jQuery.support.changeBubbles) { - - jQuery.event.special.change = { - - setup: function () { - - if (rformElems.test(this.nodeName)) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if (this.type === "checkbox" || this.type === "radio") { - jQuery.event.add(this, "propertychange._change", function (event) { - if (event.originalEvent.propertyName === "checked") { - this._just_changed = true; - } - }); - jQuery.event.add(this, "click._change", function (event) { - if (this._just_changed && !event.isTrigger) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate("change", this, event, true); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add(this, "beforeactivate._change", function (e) { - var elem = e.target; - - if (rformElems.test(elem.nodeName) && !jQuery._data(elem, "changeBubbles")) { - jQuery.event.add(elem, "change._change", function (event) { - if (this.parentNode && !event.isSimulated && !event.isTrigger) { - jQuery.event.simulate("change", this.parentNode, event, true); - } - }); - jQuery._data(elem, "changeBubbles", true); - } - }); - }, - - handle: function (event) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if (this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox")) { - return event.handleObj.handler.apply(this, arguments); - } - }, - - teardown: function () { - jQuery.event.remove(this, "._change"); - - return !rformElems.test(this.nodeName); - } - }; - } - - // Create "bubbling" focus and blur events - if (!jQuery.support.focusinBubbles) { - jQuery.each({ - focus: "focusin", - blur: "focusout" - }, function (orig, fix) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function (event) { - jQuery.event.simulate(fix, event.target, jQuery.event.fix(event), true); - }; - - jQuery.event.special[fix] = { - setup: function () { - if (attaches++ === 0) { - document.addEventListener(orig, handler, true); - } - }, - teardown: function () { - if (--attaches === 0) { - document.removeEventListener(orig, handler, true); - } - } - }; - }); - } - - jQuery.fn.extend({ - - on: function (types, selector, data, fn, /*INTERNAL*/ one) { - var type, origFn; - - // Types can be a map of types/handlers - if (typeof types === "object") { - // ( types-Object, selector, data ) - if (typeof selector !== "string") { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for (type in types) { - this.on(type, selector, data, types[type], one); - } - return this; - } - - if (data == null && fn == null) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if (fn == null) { - if (typeof selector === "string") { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if (fn === false) { - fn = returnFalse; - } else if (!fn) { - return this; - } - - if (one === 1) { - origFn = fn; - fn = function (event) { - // Can use an empty set, since event contains the info - jQuery().off(event); - return origFn.apply(this, arguments); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || (origFn.guid = jQuery.guid++); - } - return this.each(function () { - jQuery.event.add(this, types, fn, data, selector); - }); - }, - one: function (types, selector, data, fn) { - return this.on(types, selector, data, fn, 1); - }, - off: function (types, selector, fn) { - var handleObj, type; - if (types && types.preventDefault && types.handleObj) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery(types.delegateTarget).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if (typeof types === "object") { - // ( types-object [, selector] ) - for (type in types) { - this.off(type, selector, types[type]); - } - return this; - } - if (selector === false || typeof selector === "function") { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if (fn === false) { - fn = returnFalse; - } - return this.each(function () { - jQuery.event.remove(this, types, fn, selector); - }); - }, - - trigger: function (type, data) { - return this.each(function () { - jQuery.event.trigger(type, data, this); - }); - }, - triggerHandler: function (type, data) { - var elem = this[0]; - if (elem) { - return jQuery.event.trigger(type, data, elem, true); - } - } - }); - var isSimple = /^.[^:#\[\.,]*$/, - rparentsprev = /^(?:parents|prev(?:Until|All))/, - rneedsContext = jQuery.expr.match.needsContext, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - - jQuery.fn.extend({ - find: function (selector) { - var i, - ret = [], - self = this, - len = self.length; - - if (typeof selector !== "string") { - return this.pushStack(jQuery(selector).filter(function () { - for (i = 0; i < len; i++) { - if (jQuery.contains(self[i], this)) { - return true; - } - } - })); - } - - for (i = 0; i < len; i++) { - jQuery.find(selector, self[i], ret); - } - - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack(len > 1 ? jQuery.unique(ret) : ret); - ret.selector = this.selector ? this.selector + " " + selector : selector; - return ret; - }, - - has: function (target) { - var i, - targets = jQuery(target, this), - len = targets.length; - - return this.filter(function () { - for (i = 0; i < len; i++) { - if (jQuery.contains(this, targets[i])) { - return true; - } - } - }); - }, - - not: function (selector) { - return this.pushStack(winnow(this, selector || [], true)); - }, - - filter: function (selector) { - return this.pushStack(winnow(this, selector || [], false)); - }, - - is: function (selector) { - return !!winnow( - this, - - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - typeof selector === "string" && rneedsContext.test(selector) ? - jQuery(selector) : - selector || [], - false - ).length; - }, - - closest: function (selectors, context) { - var cur, - i = 0, - l = this.length, - ret = [], - pos = rneedsContext.test(selectors) || typeof selectors !== "string" ? - jQuery(selectors, context || this.context) : - 0; - - for (; i < l; i++) { - for (cur = this[i]; cur && cur !== context; cur = cur.parentNode) { - // Always skip document fragments - if (cur.nodeType < 11 && (pos ? - pos.index(cur) > -1 : - - // Don't pass non-elements to Sizzle - cur.nodeType === 1 && - jQuery.find.matchesSelector(cur, selectors))) { - - cur = ret.push(cur); - break; - } - } - } - - return this.pushStack(ret.length > 1 ? jQuery.unique(ret) : ret); - }, - - // Determine the position of an element within - // the matched set of elements - index: function (elem) { - - // No argument, return index in parent - if (!elem) { - return (this[0] && this[0].parentNode) ? this.first().prevAll().length : -1; - } - - // index in selector - if (typeof elem === "string") { - return jQuery.inArray(this[0], jQuery(elem)); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this); - }, - - add: function (selector, context) { - var set = typeof selector === "string" ? - jQuery(selector, context) : - jQuery.makeArray(selector && selector.nodeType ? [selector] : selector), - all = jQuery.merge(this.get(), set); - - return this.pushStack(jQuery.unique(all)); - }, - - addBack: function (selector) { - return this.add(selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); - } - }); - - function sibling(cur, dir) { - do { - cur = cur[dir]; - } while (cur && cur.nodeType !== 1); - - return cur; - } - - jQuery.each({ - parent: function (elem) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function (elem) { - return jQuery.dir(elem, "parentNode"); - }, - parentsUntil: function (elem, i, until) { - return jQuery.dir(elem, "parentNode", until); - }, - next: function (elem) { - return sibling(elem, "nextSibling"); - }, - prev: function (elem) { - return sibling(elem, "previousSibling"); - }, - nextAll: function (elem) { - return jQuery.dir(elem, "nextSibling"); - }, - prevAll: function (elem) { - return jQuery.dir(elem, "previousSibling"); - }, - nextUntil: function (elem, i, until) { - return jQuery.dir(elem, "nextSibling", until); - }, - prevUntil: function (elem, i, until) { - return jQuery.dir(elem, "previousSibling", until); - }, - siblings: function (elem) { - return jQuery.sibling((elem.parentNode || {}).firstChild, elem); - }, - children: function (elem) { - return jQuery.sibling(elem.firstChild); - }, - contents: function (elem) { - return jQuery.nodeName(elem, "iframe") ? - elem.contentDocument || elem.contentWindow.document : - jQuery.merge([], elem.childNodes); - } - }, function (name, fn) { - jQuery.fn[name] = function (until, selector) { - var ret = jQuery.map(this, fn, until); - - if (name.slice(-5) !== "Until") { - selector = until; - } - - if (selector && typeof selector === "string") { - ret = jQuery.filter(selector, ret); - } - - if (this.length > 1) { - // Remove duplicates - if (!guaranteedUnique[name]) { - ret = jQuery.unique(ret); - } - - // Reverse order for parents* and prev-derivatives - if (rparentsprev.test(name)) { - ret = ret.reverse(); - } - } - - return this.pushStack(ret); - }; - }); - - jQuery.extend({ - filter: function (expr, elems, not) { - var elem = elems[0]; - - if (not) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 && elem.nodeType === 1 ? - jQuery.find.matchesSelector(elem, expr) ? [elem] : [] : - jQuery.find.matches(expr, jQuery.grep(elems, function (elem) { - return elem.nodeType === 1; - })); - }, - - dir: function (elem, dir, until) { - var matched = [], - cur = elem[dir]; - - while (cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery(cur).is(until))) { - if (cur.nodeType === 1) { - matched.push(cur); - } - cur = cur[dir]; - } - return matched; - }, - - sibling: function (n, elem) { - var r = []; - - for (; n; n = n.nextSibling) { - if (n.nodeType === 1 && n !== elem) { - r.push(n); - } - } - - return r; - } - }); - - // Implement the identical functionality for filter and not - - function winnow(elements, qualifier, not) { - if (jQuery.isFunction(qualifier)) { - return jQuery.grep(elements, function (elem, i) { - /* jshint -W018 */ - return !!qualifier.call(elem, i, elem) !== not; - }); - - } - - if (qualifier.nodeType) { - return jQuery.grep(elements, function (elem) { - return (elem === qualifier) !== not; - }); - - } - - if (typeof qualifier === "string") { - if (isSimple.test(qualifier)) { - return jQuery.filter(qualifier, elements, not); - } - - qualifier = jQuery.filter(qualifier, elements); - } - - return jQuery.grep(elements, function (elem) { - return (jQuery.inArray(elem, qualifier) >= 0) !== not; - }); - } - - function createSafeFragment(document) { - var list = nodeNames.split("|"), - safeFrag = document.createDocumentFragment(); - - if (safeFrag.createElement) { - while (list.length) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; - } - - var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rtbody = /\s*$/g, - - // We have to close these tags to support XHTML (#13200) - wrapMap = { - option: [1, ""], - legend: [1, "
", "
"], - area: [1, "", ""], - param: [1, "", ""], - thead: [1, "", "
"], - tr: [2, "", "
"], - col: [2, "", "
"], - td: [3, "", "
"], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: jQuery.support.htmlSerialize ? [0, "", ""] : [1, "X
", "
"] - }, - safeFragment = createSafeFragment(document), - fragmentDiv = safeFragment.appendChild(document.createElement("div")); - - wrapMap.optgroup = wrapMap.option; - wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; - wrapMap.th = wrapMap.td; - - jQuery.fn.extend({ - text: function (value) { - return jQuery.access(this, function (value) { - return value === undefined ? - jQuery.text(this) : - this.empty().append((this[0] && this[0].ownerDocument || document).createTextNode(value)); - }, null, value, arguments.length); - }, - - append: function () { - return this.domManip(arguments, function (elem) { - if (this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9) { - var target = manipulationTarget(this, elem); - target.appendChild(elem); - } - }); - }, - - prepend: function () { - return this.domManip(arguments, function (elem) { - if (this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9) { - var target = manipulationTarget(this, elem); - target.insertBefore(elem, target.firstChild); - } - }); - }, - - before: function () { - return this.domManip(arguments, function (elem) { - if (this.parentNode) { - this.parentNode.insertBefore(elem, this); - } - }); - }, - - after: function () { - return this.domManip(arguments, function (elem) { - if (this.parentNode) { - this.parentNode.insertBefore(elem, this.nextSibling); - } - }); - }, - - // keepData is for internal use only--do not document - remove: function (selector, keepData) { - var elem, - elems = selector ? jQuery.filter(selector, this) : this, - i = 0; - - for (; - (elem = elems[i]) != null; i++) { - - if (!keepData && elem.nodeType === 1) { - jQuery.cleanData(getAll(elem)); - } - - if (elem.parentNode) { - if (keepData && jQuery.contains(elem.ownerDocument, elem)) { - setGlobalEval(getAll(elem, "script")); - } - elem.parentNode.removeChild(elem); - } - } - - return this; - }, - - empty: function () { - var elem, - i = 0; - - for (; - (elem = this[i]) != null; i++) { - // Remove element nodes and prevent memory leaks - if (elem.nodeType === 1) { - jQuery.cleanData(getAll(elem, false)); - } - - // Remove any remaining nodes - while (elem.firstChild) { - elem.removeChild(elem.firstChild); - } - - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if (elem.options && jQuery.nodeName(elem, "select")) { - elem.options.length = 0; - } - } - - return this; - }, - - clone: function (dataAndEvents, deepDataAndEvents) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map(function () { - return jQuery.clone(this, dataAndEvents, deepDataAndEvents); - }); - }, - - html: function (value) { - return jQuery.access(this, function (value) { - var elem = this[0] || {}, - i = 0, - l = this.length; - - if (value === undefined) { - return elem.nodeType === 1 ? - elem.innerHTML.replace(rinlinejQuery, "") : - undefined; - } - - // See if we can take a shortcut and just use innerHTML - if (typeof value === "string" && !rnoInnerhtml.test(value) && - (jQuery.support.htmlSerialize || !rnoshimcache.test(value)) && - (jQuery.support.leadingWhitespace || !rleadingWhitespace.test(value)) && !wrapMap[(rtagName.exec(value) || ["", ""])[1].toLowerCase()]) { - - value = value.replace(rxhtmlTag, "<$1>"); - - try { - for (; i < l; i++) { - // Remove element nodes and prevent memory leaks - elem = this[i] || {}; - if (elem.nodeType === 1) { - jQuery.cleanData(getAll(elem, false)); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch (e) {} - } - - if (elem) { - this.empty().append(value); - } - }, null, value, arguments.length); - }, - - replaceWith: function () { - var - // Snapshot the DOM in case .domManip sweeps something relevant into its fragment - args = jQuery.map(this, function (elem) { - return [elem.nextSibling, elem.parentNode]; - }), - i = 0; - - // Make the changes, replacing each context element with the new content - this.domManip(arguments, function (elem) { - var next = args[i++], - parent = args[i++]; - - if (parent) { - // Don't use the snapshot next if it has moved (#13810) - if (next && next.parentNode !== parent) { - next = this.nextSibling; - } - jQuery(this).remove(); - parent.insertBefore(elem, next); - } - // Allow new content to include elements from the context set - }, true); - - // Force removal if there was no new content (e.g., from empty arguments) - return i ? this : this.remove(); - }, - - detach: function (selector) { - return this.remove(selector, true); - }, - - domManip: function (args, callback, allowIntersection) { - - // Flatten any nested arrays - args = core_concat.apply([], args); - - var first, node, hasScripts, - scripts, doc, fragment, - i = 0, - l = this.length, - set = this, - iNoClone = l - 1, - value = args[0], - isFunction = jQuery.isFunction(value); - - // We can't cloneNode fragments that contain checked, in WebKit - if (isFunction || !(l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test(value))) { - return this.each(function (index) { - var self = set.eq(index); - if (isFunction) { - args[0] = value.call(this, index, self.html()); - } - self.domManip(args, callback, allowIntersection); - }); - } - - if (l) { - fragment = jQuery.buildFragment(args, this[0].ownerDocument, false, !allowIntersection && this); - first = fragment.firstChild; - - if (fragment.childNodes.length === 1) { - fragment = first; - } - - if (first) { - scripts = jQuery.map(getAll(fragment, "script"), disableScript); - hasScripts = scripts.length; - - // Use the original fragment for the last item instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for (; i < l; i++) { - node = fragment; - - if (i !== iNoClone) { - node = jQuery.clone(node, true, true); - - // Keep references to cloned scripts for later restoration - if (hasScripts) { - jQuery.merge(scripts, getAll(node, "script")); - } - } - - callback.call(this[i], node, i); - } - - if (hasScripts) { - doc = scripts[scripts.length - 1].ownerDocument; - - // Reenable scripts - jQuery.map(scripts, restoreScript); - - // Evaluate executable scripts on first document insertion - for (i = 0; i < hasScripts; i++) { - node = scripts[i]; - if (rscriptType.test(node.type || "") && !jQuery._data(node, "globalEval") && jQuery.contains(doc, node)) { - - if (node.src) { - // Hope ajax is available... - jQuery._evalUrl(node.src); - } else { - jQuery.globalEval((node.text || node.textContent || node.innerHTML || "").replace(rcleanScript, "")); - } - } - } - } - - // Fix #11809: Avoid leaking memory - fragment = first = null; - } - } - - return this; - } - }); - - // Support: IE<8 - // Manipulating tables requires a tbody - - function manipulationTarget(elem, content) { - return jQuery.nodeName(elem, "table") && - jQuery.nodeName(content.nodeType === 1 ? content : content.firstChild, "tr") ? - - elem.getElementsByTagName("tbody")[0] || - elem.appendChild(elem.ownerDocument.createElement("tbody")) : - elem; - } - - // Replace/restore the type attribute of script elements for safe DOM manipulation - - function disableScript(elem) { - elem.type = (jQuery.find.attr(elem, "type") !== null) + "/" + elem.type; - return elem; - } - - function restoreScript(elem) { - var match = rscriptTypeMasked.exec(elem.type); - if (match) { - elem.type = match[1]; - } else { - elem.removeAttribute("type"); - } - return elem; - } - - // Mark scripts as having already been evaluated - - function setGlobalEval(elems, refElements) { - var elem, - i = 0; - for (; - (elem = elems[i]) != null; i++) { - jQuery._data(elem, "globalEval", !refElements || jQuery._data(refElements[i], "globalEval")); - } - } - - function cloneCopyEvent(src, dest) { - - if (dest.nodeType !== 1 || !jQuery.hasData(src)) { - return; - } - - var type, i, l, - oldData = jQuery._data(src), - curData = jQuery._data(dest, oldData), - events = oldData.events; - - if (events) { - delete curData.handle; - curData.events = {}; - - for (type in events) { - for (i = 0, l = events[type].length; i < l; i++) { - jQuery.event.add(dest, type, events[type][i]); - } - } - } - - // make the cloned public data object a copy from the original - if (curData.data) { - curData.data = jQuery.extend({}, curData.data); - } - } - - function fixCloneNodeIssues(src, dest) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if (dest.nodeType !== 1) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if (!jQuery.support.noCloneEvent && dest[jQuery.expando]) { - data = jQuery._data(dest); - - for (e in data.events) { - jQuery.removeEvent(dest, e, data.handle); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute(jQuery.expando); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if (nodeName === "script" && dest.text !== src.text) { - disableScript(dest).text = src.text; - restoreScript(dest); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if (nodeName === "object") { - if (dest.parentNode) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if (jQuery.support.html5Clone && (src.innerHTML && !jQuery.trim(dest.innerHTML))) { - dest.innerHTML = src.innerHTML; - } - - } else if (nodeName === "input" && manipulation_rcheckableType.test(src.type)) { - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if (dest.value !== src.value) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if (nodeName === "option") { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if (nodeName === "input" || nodeName === "textarea") { - dest.defaultValue = src.defaultValue; - } - } - - jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" - }, function (name, original) { - jQuery.fn[name] = function (selector) { - var elems, - i = 0, - ret = [], - insert = jQuery(selector), - last = insert.length - 1; - - for (; i <= last; i++) { - elems = i === last ? this : this.clone(true); - jQuery(insert[i])[original](elems); - - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - core_push.apply(ret, elems.get()); - } - - return this.pushStack(ret); - }; - }); - - function getAll(context, tag) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName(tag || "*") : - typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll(tag || "*") : - undefined; - - if (!found) { - for (found = [], elems = context.childNodes || context; - (elem = elems[i]) != null; i++) { - if (!tag || jQuery.nodeName(elem, tag)) { - found.push(elem); - } else { - jQuery.merge(found, getAll(elem, tag)); - } - } - } - - return tag === undefined || tag && jQuery.nodeName(context, tag) ? - jQuery.merge([context], found) : - found; - } - - // Used in buildFragment, fixes the defaultChecked property - - function fixDefaultChecked(elem) { - if (manipulation_rcheckableType.test(elem.type)) { - elem.defaultChecked = elem.checked; - } - } - - jQuery.extend({ - clone: function (elem, dataAndEvents, deepDataAndEvents) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains(elem.ownerDocument, elem); - - if (jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test("<" + elem.nodeName + ">")) { - clone = elem.cloneNode(true); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild(clone = fragmentDiv.firstChild); - } - - if ((!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && - (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem)) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll(clone); - srcElements = getAll(elem); - - // Fix all IE cloning issues - for (i = 0; - (node = srcElements[i]) != null; ++i) { - // Ensure that the destination node is not null; Fixes #9587 - if (destElements[i]) { - fixCloneNodeIssues(node, destElements[i]); - } - } - } - - // Copy the events from the original to the clone - if (dataAndEvents) { - if (deepDataAndEvents) { - srcElements = srcElements || getAll(elem); - destElements = destElements || getAll(clone); - - for (i = 0; - (node = srcElements[i]) != null; i++) { - cloneCopyEvent(node, destElements[i]); - } - } else { - cloneCopyEvent(elem, clone); - } - } - - // Preserve script evaluation history - destElements = getAll(clone, "script"); - if (destElements.length > 0) { - setGlobalEval(destElements, !inPage && getAll(elem, "script")); - } - - destElements = srcElements = node = null; - - // Return the cloned set - return clone; - }, - - buildFragment: function (elems, context, scripts, selection) { - var j, elem, contains, - tmp, tag, tbody, wrap, - l = elems.length, - - // Ensure a safe fragment - safe = createSafeFragment(context), - - nodes = [], - i = 0; - - for (; i < l; i++) { - elem = elems[i]; - - if (elem || elem === 0) { - - // Add nodes directly - if (jQuery.type(elem) === "object") { - jQuery.merge(nodes, elem.nodeType ? [elem] : elem); - - // Convert non-html into a text node - } else if (!rhtml.test(elem)) { - nodes.push(context.createTextNode(elem)); - - // Convert html into DOM nodes - } else { - tmp = tmp || safe.appendChild(context.createElement("div")); - - // Deserialize a standard representation - tag = (rtagName.exec(elem) || ["", ""])[1].toLowerCase(); - wrap = wrapMap[tag] || wrapMap._default; - - tmp.innerHTML = wrap[1] + elem.replace(rxhtmlTag, "<$1>") + wrap[2]; - - // Descend through wrappers to the right content - j = wrap[0]; - while (j--) { - tmp = tmp.lastChild; - } - - // Manually add leading whitespace removed by IE - if (!jQuery.support.leadingWhitespace && rleadingWhitespace.test(elem)) { - nodes.push(context.createTextNode(rleadingWhitespace.exec(elem)[0])); - } - - // Remove IE's autoinserted from table fragments - if (!jQuery.support.tbody) { - - // String was a , *may* have spurious - elem = tag === "table" && !rtbody.test(elem) ? - tmp.firstChild : - - // String was a bare or - wrap[1] === "
" && !rtbody.test(elem) ? - tmp : - 0; - - j = elem && elem.childNodes.length; - while (j--) { - if (jQuery.nodeName((tbody = elem.childNodes[j]), "tbody") && !tbody.childNodes.length) { - elem.removeChild(tbody); - } - } - } - - jQuery.merge(nodes, tmp.childNodes); - - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; - - // Fix #12392 for oldIE - while (tmp.firstChild) { - tmp.removeChild(tmp.firstChild); - } - - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } - - // Fix #11356: Clear elements from fragment - if (tmp) { - safe.removeChild(tmp); - } - - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if (!jQuery.support.appendChecked) { - jQuery.grep(getAll(nodes, "input"), fixDefaultChecked); - } - - i = 0; - while ((elem = nodes[i++])) { - - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if (selection && jQuery.inArray(elem, selection) !== -1) { - continue; - } - - contains = jQuery.contains(elem.ownerDocument, elem); - - // Append to fragment - tmp = getAll(safe.appendChild(elem), "script"); - - // Preserve script evaluation history - if (contains) { - setGlobalEval(tmp); - } - - // Capture executables - if (scripts) { - j = 0; - while ((elem = tmp[j++])) { - if (rscriptType.test(elem.type || "")) { - scripts.push(elem); - } - } - } - } - - tmp = null; - - return safe; - }, - - cleanData: function (elems, /* internal */ acceptData) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - deleteExpando = jQuery.support.deleteExpando, - special = jQuery.event.special; - - for (; - (elem = elems[i]) != null; i++) { - - if (acceptData || jQuery.acceptData(elem)) { - - id = elem[internalKey]; - data = id && cache[id]; - - if (data) { - if (data.events) { - for (type in data.events) { - if (special[type]) { - jQuery.event.remove(elem, type); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent(elem, type, data.handle); - } - } - } - - // Remove cache only if it was not already removed by jQuery.event.remove - if (cache[id]) { - - delete cache[id]; - - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if (deleteExpando) { - delete elem[internalKey]; - - } else if (typeof elem.removeAttribute !== core_strundefined) { - elem.removeAttribute(internalKey); - - } else { - elem[internalKey] = null; - } - - core_deletedIds.push(id); - } - } - } - } - }, - - _evalUrl: function (url) { - return jQuery.ajax({ - url: url, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); - } - }); - jQuery.fn.extend({ - wrapAll: function (html) { - if (jQuery.isFunction(html)) { - return this.each(function (i) { - jQuery(this).wrapAll(html.call(this, i)); - }); - } - - if (this[0]) { - // The elements to wrap the target around - var wrap = jQuery(html, this[0].ownerDocument).eq(0).clone(true); - - if (this[0].parentNode) { - wrap.insertBefore(this[0]); - } - - wrap.map(function () { - var elem = this; - - while (elem.firstChild && elem.firstChild.nodeType === 1) { - elem = elem.firstChild; - } - - return elem; - }).append(this); - } - - return this; - }, - - wrapInner: function (html) { - if (jQuery.isFunction(html)) { - return this.each(function (i) { - jQuery(this).wrapInner(html.call(this, i)); - }); - } - - return this.each(function () { - var self = jQuery(this), - contents = self.contents(); - - if (contents.length) { - contents.wrapAll(html); - - } else { - self.append(html); - } - }); - }, - - wrap: function (html) { - var isFunction = jQuery.isFunction(html); - - return this.each(function (i) { - jQuery(this).wrapAll(isFunction ? html.call(this, i) : html); - }); - }, - - unwrap: function () { - return this.parent().each(function () { - if (!jQuery.nodeName(this, "body")) { - jQuery(this).replaceWith(this.childNodes); - } - }).end(); - } - }); - var iframe, getStyles, curCSS, - ralpha = /alpha\([^)]*\)/i, - ropacity = /opacity\s*=\s*([^)]*)/, - rposition = /^(top|right|bottom|left)$/, - // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" - // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rmargin = /^margin/, - rnumsplit = new RegExp("^(" + core_pnum + ")(.*)$", "i"), - rnumnonpx = new RegExp("^(" + core_pnum + ")(?!px)[a-z%]+$", "i"), - rrelNum = new RegExp("^([+-])=(" + core_pnum + ")", "i"), - elemdisplay = { - BODY: "block" - }, - - cssShow = { - position: "absolute", - visibility: "hidden", - display: "block" - }, - cssNormalTransform = { - letterSpacing: 0, - fontWeight: 400 - }, - - cssExpand = ["Top", "Right", "Bottom", "Left"], - cssPrefixes = ["Webkit", "O", "Moz", "ms"]; - - // return a css property mapped to a potentially vendor prefixed property - - function vendorPropName(style, name) { - - // shortcut for names that are not vendor prefixed - if (name in style) { - return name; - } - - // check for vendor prefixed names - var capName = name.charAt(0).toUpperCase() + name.slice(1), - origName = name, - i = cssPrefixes.length; - - while (i--) { - name = cssPrefixes[i] + capName; - if (name in style) { - return name; - } - } - - return origName; - } - - function isHidden(elem, el) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css(elem, "display") === "none" || !jQuery.contains(elem.ownerDocument, elem); - } - - function showHide(elements, show) { - var display, elem, hidden, - values = [], - index = 0, - length = elements.length; - - for (; index < length; index++) { - elem = elements[index]; - if (!elem.style) { - continue; - } - - values[index] = jQuery._data(elem, "olddisplay"); - display = elem.style.display; - if (show) { - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if (!values[index] && display === "none") { - elem.style.display = ""; - } - - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if (elem.style.display === "" && isHidden(elem)) { - values[index] = jQuery._data(elem, "olddisplay", css_defaultDisplay(elem.nodeName)); - } - } else { - - if (!values[index]) { - hidden = isHidden(elem); - - if (display && display !== "none" || !hidden) { - jQuery._data(elem, "olddisplay", hidden ? display : jQuery.css(elem, "display")); - } - } - } - } - - // Set the display of most of the elements in a second loop - // to avoid the constant reflow - for (index = 0; index < length; index++) { - elem = elements[index]; - if (!elem.style) { - continue; - } - if (!show || elem.style.display === "none" || elem.style.display === "") { - elem.style.display = show ? values[index] || "" : "none"; - } - } - - return elements; - } - - jQuery.fn.extend({ - css: function (name, value) { - return jQuery.access(this, function (elem, name, value) { - var len, styles, - map = {}, - i = 0; - - if (jQuery.isArray(name)) { - styles = getStyles(elem); - len = name.length; - - for (; i < len; i++) { - map[name[i]] = jQuery.css(elem, name[i], false, styles); - } - - return map; - } - - return value !== undefined ? - jQuery.style(elem, name, value) : - jQuery.css(elem, name); - }, name, value, arguments.length > 1); - }, - show: function () { - return showHide(this, true); - }, - hide: function () { - return showHide(this); - }, - toggle: function (state) { - if (typeof state === "boolean") { - return state ? this.show() : this.hide(); - } - - return this.each(function () { - if (isHidden(this)) { - jQuery(this).show(); - } else { - jQuery(this).hide(); - } - }); - } - }); - - jQuery.extend({ - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function (elem, computed) { - if (computed) { - // We should always get a number back from opacity - var ret = curCSS(elem, "opacity"); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Don't automatically add "px" to these possibly-unitless properties - cssNumber: { - "columnCount": true, - "fillOpacity": true, - "fontWeight": true, - "lineHeight": true, - "opacity": true, - "order": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: { - // normalize float css property - "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" - }, - - // Get and set the style property on a DOM Node - style: function (elem, name, value, extra) { - // Don't set styles on text and comment nodes - if (!elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = jQuery.camelCase(name), - style = elem.style; - - name = jQuery.cssProps[origName] || (jQuery.cssProps[origName] = vendorPropName(style, origName)); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[name] || jQuery.cssHooks[origName]; - - // Check if we're setting a value - if (value !== undefined) { - type = typeof value; - - // convert relative number strings (+= or -=) to relative numbers. #7345 - if (type === "string" && (ret = rrelNum.exec(value))) { - value = (ret[1] + 1) * ret[2] + parseFloat(jQuery.css(elem, name)); - // Fixes bug #9237 - type = "number"; - } - - // Make sure that NaN and null values aren't set. See: #7116 - if (value == null || type === "number" && isNaN(value)) { - return; - } - - // If a number was passed in, add 'px' to the (except for certain CSS properties) - if (type === "number" && !jQuery.cssNumber[origName]) { - value += "px"; - } - - // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, - // but it would mean to define eight (for every problematic property) identical functions - if (!jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0) { - style[name] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if (!hooks || !("set" in hooks) || (value = hooks.set(elem, value, extra)) !== undefined) { - - // Wrapped to prevent IE from throwing errors when 'invalid' values are provided - // Fixes bug #5509 - try { - style[name] = value; - } catch (e) {} - } - - } else { - // If a hook was provided get the non-computed value from there - if (hooks && "get" in hooks && (ret = hooks.get(elem, false, extra)) !== undefined) { - return ret; - } - - // Otherwise just get the value from the style object - return style[name]; - } - }, - - css: function (elem, name, extra, styles) { - var num, val, hooks, - origName = jQuery.camelCase(name); - - // Make sure that we're working with the right name - name = jQuery.cssProps[origName] || (jQuery.cssProps[origName] = vendorPropName(elem.style, origName)); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[name] || jQuery.cssHooks[origName]; - - // If a hook was provided get the computed value from there - if (hooks && "get" in hooks) { - val = hooks.get(elem, true, extra); - } - - // Otherwise, if a way to get the computed value exists, use that - if (val === undefined) { - val = curCSS(elem, name, styles); - } - - //convert "normal" to computed value - if (val === "normal" && name in cssNormalTransform) { - val = cssNormalTransform[name]; - } - - // Return, converting to number if forced or a qualifier was provided and val looks numeric - if (extra === "" || extra) { - num = parseFloat(val); - return extra === true || jQuery.isNumeric(num) ? num || 0 : val; - } - return val; - } - }); - - // NOTE: we've included the "window" in window.getComputedStyle - // because jsdom on node.js will break without it. - if (window.getComputedStyle) { - getStyles = function (elem) { - return window.getComputedStyle(elem, null); - }; - - curCSS = function (elem, name, _computed) { - var width, minWidth, maxWidth, - computed = _computed || getStyles(elem), - - // getPropertyValue is only needed for .css('filter') in IE9, see #12537 - ret = computed ? computed.getPropertyValue(name) || computed[name] : undefined, - style = elem.style; - - if (computed) { - - if (ret === "" && !jQuery.contains(elem.ownerDocument, elem)) { - ret = jQuery.style(elem, name); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right - // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels - // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values - if (rnumnonpx.test(ret) && rmargin.test(name)) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret; - }; - } else if (document.documentElement.currentStyle) { - getStyles = function (elem) { - return elem.currentStyle; - }; - - curCSS = function (elem, name, _computed) { - var left, rs, rsLeft, - computed = _computed || getStyles(elem), - ret = computed ? computed[name] : undefined, - style = elem.style; - - // Avoid setting ret to empty string here - // so we don't default to auto - if (ret == null && style && style[name]) { - ret = style[name]; - } - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - // but not position css attributes, as those are proportional to the parent element instead - // and we can't measure the parent instead because it might trigger a "stacking dolls" problem - if (rnumnonpx.test(ret) && !rposition.test(name)) { - - // Remember the original values - left = style.left; - rs = elem.runtimeStyle; - rsLeft = rs && rs.left; - - // Put in the new values to get a computed value out - if (rsLeft) { - rs.left = elem.currentStyle.left; - } - style.left = name === "fontSize" ? "1em" : ret; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - if (rsLeft) { - rs.left = rsLeft; - } - } - - return ret === "" ? "auto" : ret; - }; - } - - function setPositiveNumber(elem, value, subtract) { - var matches = rnumsplit.exec(value); - return matches ? - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max(0, matches[1] - (subtract || 0)) + (matches[2] || "px") : - value; - } - - function augmentWidthOrHeight(elem, name, extra, isBorderBox, styles) { - var i = extra === (isBorderBox ? "border" : "content") ? - // If we already have the right measurement, avoid augmentation - 4 : - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, - - val = 0; - - for (; i < 4; i += 2) { - // both box models exclude margin, so add it if we want it - if (extra === "margin") { - val += jQuery.css(elem, extra + cssExpand[i], true, styles); - } - - if (isBorderBox) { - // border-box includes padding, so remove it if we want content - if (extra === "content") { - val -= jQuery.css(elem, "padding" + cssExpand[i], true, styles); - } - - // at this point, extra isn't border nor margin, so remove border - if (extra !== "margin") { - val -= jQuery.css(elem, "border" + cssExpand[i] + "Width", true, styles); - } - } else { - // at this point, extra isn't content, so add padding - val += jQuery.css(elem, "padding" + cssExpand[i], true, styles); - - // at this point, extra isn't content nor padding, so add border - if (extra !== "padding") { - val += jQuery.css(elem, "border" + cssExpand[i] + "Width", true, styles); - } - } - } - - return val; - } - - function getWidthOrHeight(elem, name, extra) { - - // Start with offset property, which is equivalent to the border-box value - var valueIsBorderBox = true, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - styles = getStyles(elem), - isBorderBox = jQuery.support.boxSizing && jQuery.css(elem, "boxSizing", false, styles) === "border-box"; - - // some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if (val <= 0 || val == null) { - // Fall back to computed then uncomputed css if necessary - val = curCSS(elem, name, styles); - if (val < 0 || val == null) { - val = elem.style[name]; - } - - // Computed unit is not pixels. Stop here and return. - if (rnumnonpx.test(val)) { - return val; - } - - // we need the check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && (jQuery.support.boxSizingReliable || val === elem.style[name]); - - // Normalize "", auto, and prepare for extra - val = parseFloat(val) || 0; - } - - // use the active box-sizing model to add/subtract irrelevant styles - return (val + - augmentWidthOrHeight( - elem, - name, - extra || (isBorderBox ? "border" : "content"), - valueIsBorderBox, - styles - ) - ) + "px"; - } - - // Try to determine the default display value of an element - - function css_defaultDisplay(nodeName) { - var doc = document, - display = elemdisplay[nodeName]; - - if (!display) { - display = actualDisplay(nodeName, doc); - - // If the simple way fails, read from inside an iframe - if (display === "none" || !display) { - // Use the already-created iframe if possible - iframe = (iframe || - jQuery("