diff --git a/.codeclimate.yml b/.codeclimate.yml index 33d591df48..63c7d5cab1 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -17,6 +17,14 @@ checks: similar-code: config: threshold: 65 +plugins: + duplication: + enabled: true + config: + languages: + javascript: + mass_threshold: 110 + count_threshold: 3 exclude_paths: - "public/vendor/*" - "test/*" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index dfd9ca2d29..a1eba6cfb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,115 @@ +#### v1.16.1 (2021-01-06) + +##### Chores + +* increase test timeout (0d7dfeeb) +* incrementing version number - v1.16.1-beta.0 (5fcf3ea6) +* add deprecation notice to topic thumb tpl value (05d8b3c3) +* minor reordering of lines (8e5a413e) +* incrementing version number - v1.16.0 (6d01fd50) +* update changelog for v1.16.0 (1437c62f) +* **deps:** + * update dependency eslint to v7.17.0 (18ae7cf7) + * update dependency eslint to v7.16.0 (2610dfcf) + * update actions/setup-node action to v2 (#9115) (55a55ea2) +* **api:** add deprecation notices re: #9123 (cdff8d28) + +##### New Features + +* #9173, show installed plugin versions in ./nodebb plugins (8c31afae) +* added note that you can now upload videos (4d6ddf6d) +* automatically attempt socket.io reconnection on ajaxify (e5edbc6f) +* #9135, don't try to reconnect forever (c1ecfd1e) +* add confirmation modal when assigning admin:admins-mods privilege (d90aa958) +* allow dashes in privilege group names (5b8558e9) +* allow multiple privileges to be defined for a given admin socket call (3aa5beb8) +* rename admin middleware header hook (fcc1e24a) +* explicitly add filter:admin/header.build hook (75b1bbd0) +* fix more tests, add more routes, update api test suite (cb32e32a) +* add registration/complete route, fix some other tests (14c51e3c) +* add missing schemas for various ACP settings routes (9de35ec5) +* add missing schema for category update and deletion (d6de9253) +* add schema for api ping routes (d85181e0) +* normalize paths before comparison (df8d62ba) +* additional test to ensure any new routes added to express have a corresponding schema doc (dbe85630) +* update html-to-text closes https://github.com/NodeBB/NodeBB/pull/8810 (a2152dd1) +* **api:** + * closes #9123 category and topic routes migrated to Write API (edb8da1e) + * #9123, migrate rest of the getObject controllers to Write API (9ecfac9b) + * #9123, migrate /api/post/pid/:pid to Write API (e267f295) + * group ownership API route, switch client-side to use API route (32e36f7b) + * add schema for groups update route (98550d61) + * added schema for email unsubscribe token (4fc13377) +* **acp:** + * admin tags privilege (223f0a55) + * admins-mods privilege (fb46a8d9) + * added new admin privilege for groups management (da191341) + +##### Bug Fixes + +* #9130, remove timestamp prefix from thumbnail names in API response (171017c3) +* #9166 missing relative path in topic thumbs modal and topic list (b9ba44ed) +* #9163, fix total connection count on ACP (1968bf50) +* genericise .necro-post, bump persona to latest (041d45c3) +* #9126, skip base64 and long values (33290850) +* #9127, use assets path (3121215e) +* inability for admins with setting privilege to save plugin settings (a555f024) +* #9149, server-side handling of disableChatMessageEditing (895e3d93) +* #9149, incorrect client-side `disableChatMessageEditing` value for admins/gmods (d27815a8) +* #9151, dont use service worker for posts requests (20c1b684) +* #9150, fix selector so it doesn't add img-responsive to profile pics (183cabe9) +* tests (28740360) +* dont show deleted posts in navigator (931105e6) +* bug in api path existence test (501a7b77) +* #9136, fix move topic/post timeout errors (2ef72a94) +* bad assignment logic in middleware.renderHeader (34ccabe3) +* #9113, wrong path separator used in thumbs.get (da4f9118) +* email testing and settings change from ACP (2be396ff) +* removing ability to specify deprecated topic 'thumb' on topic creation (713f029d) +* #9129, event is fired on socket.io (b369dc88) +* subfolder handling in tests (bbd97ccb) +* .flat() not defined in v10, added debug router to exclusion list (6062039d) +* all tests, wrap up work (f416dc17) +* two more routes (9c2de86a) +* api tests (b9a61d2d) +* don't return deleted: 0 for ephemeral groups (600807fb) +* send fewer items to client-side for ACP settings/email page (438fa5c8) +* errors in write-api schema (c079051b) +* broken tests from last round of fixes (990f1077) +* bad error message for request body api test (a9629357) +* modify backreference test to not check router.all() calls (7fc329de) +* add missing token generation route to write api spec (eef052c1) +* trigger action:posts.edited (b7b588f5) +* **deps:** + * update dependency autoprefixer to v10.2.0 (e445ae5a) + * update socket.io packages to v3.0.5 (fd045c67) + * update dependency nodebb-theme-persona to v10.3.16 (87e333b4) + * update dependency benchpressjs to v2.4.0 (4524f825) + * update dependency nodebb-theme-persona to v10.3.15 (189be9e0) + * update dependency nodebb-widget-essentials to v5.0.2 (1dd1d3b0) + * update dependency nodebb-widget-essentials to v5.0.1 (#9144) (f55dddb2) + * update dependency nodebb-plugin-composer-default to v6.5.5 (6d980d26) + * update dependency sharp to v0.27.0 (4919e596) + * update dependency nodebb-theme-persona to v10.3.12 (37b35f7d) + * update dependency nodebb-theme-persona to v10.3.11 (db4c6863) +* **tests:** handle nested allOf blocks (77a5adb6) +* **api:** + * failing test due to missing file (3959a7bd) + * tests (80ee3dfb) +* **pwa:** #9127 service-worker.js missing on subfolder installs (b8d4709e) + +##### Refactors + +* **openapi:** update TopicObject component to reference TopicObjectSlim in its schema (fb3f3f72) +* **api:** + * deprecated groups update socket in favour of API lib (1cd2689c) + * update group deletion calls to use write API (e640a41a) +* schema backreference test to use map instead of reduce, properly check write-api routes (878ee067) + +##### Tests + +* changed test a bit to see what is going on (5f038dff) + #### v1.16.0 (2020-12-17) ##### Breaking Changes diff --git a/install/package.json b/install/package.json index 68ac6d74cb..5d386ae87e 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "1.16.1", + "version": "1.16.2-beta.0", "homepage": "http://www.nodebb.org", "repository": { "type": "git", @@ -39,11 +39,11 @@ "ace-builds": "^1.4.9", "archiver": "^5.0.0", "async": "^3.2.0", - "autoprefixer": "10.2.0", + "autoprefixer": "10.2.1", "bcryptjs": "2.4.3", "benchpressjs": "2.4.0", "body-parser": "^1.19.0", - "bootbox": "4.4.0", + "bootbox": "5.5.2", "bootstrap": "^3.4.1", "chart.js": "^2.9.3", "cli-graph": "^3.2.2", @@ -101,8 +101,8 @@ "nodebb-plugin-spam-be-gone": "0.7.7", "nodebb-rewards-essentials": "0.1.4", "nodebb-theme-lavender": "5.0.17", - "nodebb-theme-persona": "10.3.17", - "nodebb-theme-slick": "1.3.7", + "nodebb-theme-persona": "10.3.19", + "nodebb-theme-slick": "1.3.8", "nodebb-theme-vanilla": "11.3.10", "nodebb-widget-essentials": "5.0.2", "nodemailer": "^6.4.6", @@ -128,11 +128,11 @@ "sharp": "0.27.0", "sitemap": "^6.1.0", "slideout": "1.0.1", - "socket.io": "3.0.5", + "socket.io": "3.1.0", "socket.io-adapter-cluster": "^1.0.1", - "socket.io-client": "3.0.5", + "socket.io-client": "3.1.0", "socket.io-redis": "6.0.1", - "sortablejs": "1.10.2", + "sortablejs": "1.13.0", "spdx-license-list": "^6.1.0", "spider-detector": "2.0.0", "textcomplete": "^0.17.1", @@ -146,6 +146,7 @@ "winston": "3.3.3", "xml": "^1.0.1", "xregexp": "^4.3.0", + "yargs": "16.2.0", "zxcvbn": "^4.4.2" }, "devDependencies": { @@ -153,12 +154,12 @@ "@commitlint/cli": "11.0.0", "@commitlint/config-angular": "11.0.0", "coveralls": "3.1.0", - "eslint": "7.17.0", + "eslint": "7.18.0", "eslint-config-airbnb-base": "14.2.1", "eslint-plugin-import": "2.22.1", "grunt": "1.3.0", "grunt-contrib-watch": "1.1.0", - "husky": "4.3.6", + "husky": "4.3.8", "jsdom": "16.4.0", "lint-staged": "10.5.3", "mocha": "8.2.1", diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 078a2d95c9..730b6e0fa3 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "لقد شاركت بالتصويت ، ألا تذكر؟", "reputation-system-disabled": "نظام السمعة معطل", "downvoting-disabled": "التصويتات السلبية معطلة", diff --git a/public/language/ar/post-queue.json b/public/language/ar/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/ar/post-queue.json +++ b/public/language/ar/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/bg/error.json b/public/language/bg/error.json index 0a6461682d..b15f9f6ba4 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Можете да изтривате съобщенията си в разговорите до %1 секунда/и след пускането им", "chat-deleted-already": "Това съобщение вече е изтрито.", "chat-restored-already": "Това съобщение вече е възстановено.", + "chat-room-does-not-exist": "Стаята за разговори не съществува.", "already-voting-for-this-post": "Вече сте дали глас за тази публикация.", "reputation-system-disabled": "Системата за репутация е изключена.", "downvoting-disabled": "Отрицателното гласуване е изключено", diff --git a/public/language/bg/post-queue.json b/public/language/bg/post-queue.json index c5ae725e4d..73647030b1 100644 --- a/public/language/bg/post-queue.json +++ b/public/language/bg/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Опашка за публикации", "description": "Няма публикации в опашката.
За да включите тази функционалност, идете в Настройки → Публикуване → Опашка за публикации и включете Опашката за публикации.", @@ -7,5 +8,11 @@ "content": "Съдържание", "posted": "Публикувано", "reply-to": "Отговор на „%1“", - "content-editable": "Можете да щракнете върху всеки от текстовете, за да ги редактирате преди публикуване." + "content-editable": "Щракнете върху съдържание, за да го редактирате", + "category-editable": "Щракнете върху категория, за да я редактирате", + "title-editable": "Щракнете върху заглавие, за да го редактирате", + "reply": "Отговор", + "topic": "Тема", + "accept": "Приемане", + "reject": "Отказване" } \ No newline at end of file diff --git a/public/language/bn/error.json b/public/language/bn/error.json index 6fd47fffaf..0937cab25f 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "সম্মাননা ব্যাবস্থা নিস্ক্রীয় রাখা হয়েছে", "downvoting-disabled": "ঋণাত্মক ভোট নিস্ক্রীয় রাখা হয়েছে।", diff --git a/public/language/bn/post-queue.json b/public/language/bn/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/bn/post-queue.json +++ b/public/language/bn/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/cs/error.json b/public/language/cs/error.json index fbedf86258..13659387da 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Je vám umožněno odstranit konverzační zprávy pod dobu %1 sekund/y po jejich odeslání", "chat-deleted-already": "Tato konverzační zpráva již byla odstraněna.", "chat-restored-already": "Tato konverzační zpráva již byla obnovena.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Již jste v tomto příspěvku hlasoval.", "reputation-system-disabled": "Systém reputací je zakázán.", "downvoting-disabled": "Systém nesouhlasu je zakázán", diff --git a/public/language/cs/post-queue.json b/public/language/cs/post-queue.json index e707996b17..301dc9d3a4 100644 --- a/public/language/cs/post-queue.json +++ b/public/language/cs/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Fronta příspěvků", "description": "Nejsou žádné příspěvky ve frontě. Pro povolení této funkčnosti, přejděte do Nastavení – Příspěvky – Fronta příspěvků a povolte Fronta příspěvků.", @@ -7,5 +8,11 @@ "content": "Obsah", "posted": "Přidáno", "reply-to": "Odpovědět na \"%1\"", - "content-editable": "Kvůli úpravám a před odesláním příspěvku můžete klikat na obsah." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/da/error.json b/public/language/da/error.json index 9f2b9c8eff..95cf4f8595 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Vurderingssystem er slået fra.", "downvoting-disabled": "Nedvurdering er slået fra", diff --git a/public/language/da/post-queue.json b/public/language/da/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/da/post-queue.json +++ b/public/language/da/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/de/error.json b/public/language/de/error.json index e20858a89d..e8672cd322 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Du darfst Chat-Nachrichten nur bis zu %1 Sekunde(n) nach der erstellung löschen", "chat-deleted-already": "Diese Chatnachricht wurde bereits gelöscht.", "chat-restored-already": "Diese Chatnachricht wurde bereits wiederhergestellt.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Du hast diesen Beitrag bereits bewertet.", "reputation-system-disabled": "Das Reputationssystem ist deaktiviert.", "downvoting-disabled": "Downvotes sind deaktiviert.", diff --git a/public/language/de/global.json b/public/language/de/global.json index d4292a1566..76edc31104 100644 --- a/public/language/de/global.json +++ b/public/language/de/global.json @@ -62,7 +62,7 @@ "downvoters": "Downvoter", "downvoted": "Negativ bewertet", "views": "Aufrufe", - "posters": "Posters", + "posters": "Kommentatoren", "reputation": "Ansehen", "lastpost": "Letzter Beitrag", "firstpost": "Erster Beitrag", diff --git a/public/language/de/post-queue.json b/public/language/de/post-queue.json index 1e32f5e785..2184fbd04b 100644 --- a/public/language/de/post-queue.json +++ b/public/language/de/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Beitragswarteschlange", "description": "Es gibt keine Beiträge in der Warteschlange.
Um dieses Feature zu aktivieren, gehe auf Einstellungen → Posts → Beitragswarteschlange und aktiviere Beitragswarteschlange.", @@ -7,5 +8,11 @@ "content": "Inhalt", "posted": "Gepostet", "reply-to": "Auf \"%1\" antworten", - "content-editable": "Du kannst auf den einzelnen Inhalt klicken um ihn zu ändern bevor du ihn postest." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/el/error.json b/public/language/el/error.json index 695f0f3d1e..5ab9cc626c 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Το σύστημα φήμης έχει απενεργοποιηθεί.", "downvoting-disabled": "Η καταψήφιση έχει απενεργοποιηθεί", diff --git a/public/language/el/post-queue.json b/public/language/el/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/el/post-queue.json +++ b/public/language/el/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 7a08489cb5..2c507278f3 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -158,6 +158,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index ebf6de4ec7..2f070ca81b 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", diff --git a/public/language/en-US/post-queue.json b/public/language/en-US/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/en-US/post-queue.json +++ b/public/language/en-US/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json index ebf6de4ec7..2f070ca81b 100644 --- a/public/language/en-x-pirate/error.json +++ b/public/language/en-x-pirate/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", diff --git a/public/language/en-x-pirate/post-queue.json b/public/language/en-x-pirate/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/en-x-pirate/post-queue.json +++ b/public/language/en-x-pirate/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/es/error.json b/public/language/es/error.json index 91c3a067e4..9efa34bfbb 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Sólo se te permite borrar mensajes de chat durante %1 segundo(s) después de enviar el mensaje", "chat-deleted-already": "Este mensaje de chat ya ha sido borrado.", "chat-restored-already": "Este mensaje de chat ya ha sido restaurado.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Ya has votado a este mensaje.", "reputation-system-disabled": "El sistema de reputación está deshabilitado.", "downvoting-disabled": "La votación negativa está deshabilitada.", diff --git a/public/language/es/post-queue.json b/public/language/es/post-queue.json index 533fb96f44..78e2ea1f91 100644 --- a/public/language/es/post-queue.json +++ b/public/language/es/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Cola de Mensajes", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Contenido", "posted": "Publicado", "reply-to": "Responder a %1", - "content-editable": "Puedes hacer click en contenido individual para editar antes de enviarlo." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/et/error.json b/public/language/et/error.json index 081c2623d0..7b33fd341e 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Sa oled juba hääletanud sellel postitusel.", "reputation-system-disabled": "Reputatsiooni süsteem ei ole aktiveeritud", "downvoting-disabled": "Negatiivsete häälte andmine ei ole võimaldatud", diff --git a/public/language/et/post-queue.json b/public/language/et/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/et/post-queue.json +++ b/public/language/et/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json index 5b695d287f..adf72bfc3c 100644 --- a/public/language/fa-IR/error.json +++ b/public/language/fa-IR/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "شما قادر هستید پیام های چت را فقط بعد از %1 ثانیه پاک کنید", "chat-deleted-already": "این پیام قبلا حذف شده است", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "شما قبلا به این پست رای داده اید.", "reputation-system-disabled": "سیستم اعتبار غیر فعال شده است", "downvoting-disabled": "رأی منفی غیر فعال شده است", diff --git a/public/language/fa-IR/post-queue.json b/public/language/fa-IR/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/fa-IR/post-queue.json +++ b/public/language/fa-IR/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/fi/error.json b/public/language/fi/error.json index 7828030e84..04aa2a363d 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", diff --git a/public/language/fi/post-queue.json b/public/language/fi/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/fi/post-queue.json +++ b/public/language/fi/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/fr/error.json b/public/language/fr/error.json index 9c712fd0a7..e852a346f9 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Vous n'êtes autorisé à supprimer des messages que pendant %1 seconde(s) après les avoir postés", "chat-deleted-already": "Ce message a déjà été supprimé.", "chat-restored-already": "Ce message de discussion a déjà été restauré.", + "chat-room-does-not-exist": "Le salon de discussion n'existe pas.", "already-voting-for-this-post": "Vous avez déjà voté pour ce message.", "reputation-system-disabled": "Le système de réputation est désactivé", "downvoting-disabled": "Les votes négatifs ne sont pas autorisés", diff --git a/public/language/fr/post-queue.json b/public/language/fr/post-queue.json index d350c6918c..0ffcfa9c26 100644 --- a/public/language/fr/post-queue.json +++ b/public/language/fr/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "File d’attente des messages", "description": "Aucun messages dans la file d'attente.
Pour activer cette fonctionnalité, accédez aux Paramètres → Messages → File d'attente et activez la file d'attente.", @@ -7,5 +8,11 @@ "content": "Contenu", "posted": "Posté", "reply-to": "Répondre à \"%1\"", - "content-editable": "Vous pouvez cliquer sur le contenu pour le modifier avant de le poster." + "content-editable": "Cliquez sur le contenu pour modifier", + "category-editable": "Cliquez sur la catégorie pour modifier", + "title-editable": "Cliquez sur le titre pour modifier", + "reply": "Répondre", + "topic": "Sujet", + "accept": "Accepter", + "reject": "Refuser" } \ No newline at end of file diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 41356ebc76..3b6f53ad2c 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Xa votache esta mensaxe.", "reputation-system-disabled": "O sistema de reputación está deshabilitado", "downvoting-disabled": "Os votos negativos están deshabilitados", diff --git a/public/language/gl/post-queue.json b/public/language/gl/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/gl/post-queue.json +++ b/public/language/gl/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/he/admin/extend/rewards.json b/public/language/he/admin/extend/rewards.json index aebf2d1986..efffa7057f 100644 --- a/public/language/he/admin/extend/rewards.json +++ b/public/language/he/admin/extend/rewards.json @@ -1,10 +1,10 @@ { "rewards": "תגמולים", "condition-if-users": "אם משתמש", - "condition-is": "Is:", + "condition-is": "לערך:", "condition-then": "תגמל ב:", "max-claims": "מספר פעמים בה ניתן לדרוש תגמול", - "zero-infinite": "הזן 0 לאינסוף", + "zero-infinite": "הזן 0 ללא הגבלה", "delete": "מחק", "enable": "הפעל", "disable": "השבת", diff --git a/public/language/he/admin/menu.json b/public/language/he/admin/menu.json index ed2932db94..55365b28a9 100644 --- a/public/language/he/admin/menu.json +++ b/public/language/he/admin/menu.json @@ -19,7 +19,7 @@ "settings/general": "כללי", "settings/homepage": "דף הבית", "settings/navigation": "ניווט", - "settings/reputation": "מוניטין ודיווחים", + "settings/reputation": "דיווחים ומוניטין", "settings/email": "דוא\"ל", "settings/user": "משתמשים", "settings/group": "קבוצות", @@ -27,7 +27,7 @@ "settings/uploads": "העלאות", "settings/languages": "שפות", "settings/post": "פוסטים", - "settings/chat": "צ'אטים", + "settings/chat": "צ'אט", "settings/pagination": "עמודים", "settings/tags": "תגיות", "settings/notifications": "התראות", diff --git a/public/language/he/error.json b/public/language/he/error.json index 3f07d4829d..9a2def1d5f 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "הנך רשאי למחוק הודעת צ'אט עד %1 דק(ות) מרגע פרסום התגובה.", "chat-deleted-already": "הודעות הצ'אט הזו כבר נמחקה.", "chat-restored-already": "הודעות הצ'אט הזו כבר שוחזרה.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "הצבעת כבר בנושא זה", "reputation-system-disabled": "מערכת המוניטין לא פעילה.", "downvoting-disabled": "היכולת להצביע נגד לא פעילה", diff --git a/public/language/he/post-queue.json b/public/language/he/post-queue.json index 1aef0f967d..eeef23a806 100644 --- a/public/language/he/post-queue.json +++ b/public/language/he/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "פוסטים ממתינים", "description": "אין פוסטים בתור.
כדי לאפשר את תור ההרשמה, גשו להגדרות → פוסט → תור פוסטים ואפשרו את תור פוסט.", @@ -7,5 +8,11 @@ "content": "תוכן", "posted": "נשלח", "reply-to": "תגובה ל %1", - "content-editable": "אתה יכול ללחוץ על התוכן כדי לערוך אותו." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/hr/error.json b/public/language/hr/error.json index 3daf75a236..454b558e2b 100644 --- a/public/language/hr/error.json +++ b/public/language/hr/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "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", diff --git a/public/language/hr/post-queue.json b/public/language/hr/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/hr/post-queue.json +++ b/public/language/hr/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/hu/error.json b/public/language/hu/error.json index 708f3c775d..8314e3bfbb 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Hírnév funkció kikapcsolva.", "downvoting-disabled": "Leszavazás funkció kikapcsolva", diff --git a/public/language/hu/post-queue.json b/public/language/hu/post-queue.json index b533498eb0..cd12778bfb 100644 --- a/public/language/hu/post-queue.json +++ b/public/language/hu/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Hozzászólási várósor", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Tartalom", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/id/error.json b/public/language/id/error.json index 0a0ac11943..234e745cf4 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Sistem reputasi ditiadakan.", "downvoting-disabled": "Downvoting ditiadakan", diff --git a/public/language/id/post-queue.json b/public/language/id/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/id/post-queue.json +++ b/public/language/id/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/it/error.json b/public/language/it/error.json index 1218441858..632af35768 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Sei l'unico che ha il permesso di eliminare i messaggi per %1 secondi(o) dopo il loro invio", "chat-deleted-already": "Il messaggio è già stato eliminato.", "chat-restored-already": "Questo messaggio della chat è già stato ripristinato.", + "chat-room-does-not-exist": "La stanza chat non esiste.", "already-voting-for-this-post": "Hai già votato per questo post", "reputation-system-disabled": "Il sistema di reputazione è disabilitato.", "downvoting-disabled": "Votata negativamente è disabilitato", diff --git a/public/language/it/flags.json b/public/language/it/flags.json index 0cdb0f43c8..455907a628 100644 --- a/public/language/it/flags.json +++ b/public/language/it/flags.json @@ -13,7 +13,7 @@ "filter-active": "Ci sono uno o più filtri attivi in questa lista di segnalazioni", "filter-reset": "Rimuovi Filtri", "filters": "Opzioni Filtri", - "filter-reporterId": "Segnalatore UID", + "filter-reporterId": "UID segnalatore", "filter-targetUid": "UID segnalato", "filter-type": "Tipo Segnalazione", "filter-type-all": "Tutto il Contenuto", diff --git a/public/language/it/post-queue.json b/public/language/it/post-queue.json index 09552abd32..b052203795 100644 --- a/public/language/it/post-queue.json +++ b/public/language/it/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post in attesa", "description": "Non ci sono post nella coda della post.
Per abilitare questa funzione, vai in Impostazioni → Post → Codice post e abilita Post in attesa.", @@ -7,5 +8,11 @@ "content": "Contenuto", "posted": "Pubblicato", "reply-to": "Rispondi a \"%1\"", - "content-editable": "Puoi cliccare su singoli contenuti per modificarli prima della pubblicazione." + "content-editable": "Clicca sul contenuto da modificare", + "category-editable": "Clicca sulla categoria da modificare", + "title-editable": "Clicca sul titolo da modificare", + "reply": "Rispondi", + "topic": "Discussione", + "accept": "Accetta", + "reject": "Rifiuta" } \ No newline at end of file diff --git a/public/language/ja/error.json b/public/language/ja/error.json index c4eac6aeca..76be962e2d 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "投稿後、あなたは %1秒間(s)だけチャットメッセージを削除することを許可されています", "chat-deleted-already": "このチャットメッセージは既に削除されています", "chat-restored-already": "このチャットメッセージは既に削除されています", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "あなたはすでにこの投稿を評価しました。", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", diff --git a/public/language/ja/post-queue.json b/public/language/ja/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/ja/post-queue.json +++ b/public/language/ja/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/ko/error.json b/public/language/ko/error.json index f879589785..ec8d5dbde4 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "채팅 메시지를 게시한 뒤 %1초 뒤부터 삭제가 가능합니다.", "chat-deleted-already": "이미 삭제된 대화 메시지입니다.", "chat-restored-already": "이 채팅 메시지는 이미 복원되었습니다.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "이미 이 포스트에 투표하셨습니다.", "reputation-system-disabled": "평판 시스템이 비활성화되어있습니다.", "downvoting-disabled": "비추천 기능이 비활성 상태입니다.", diff --git a/public/language/ko/post-queue.json b/public/language/ko/post-queue.json index e5ec934ddf..247cf71dc7 100644 --- a/public/language/ko/post-queue.json +++ b/public/language/ko/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "게시 대기열", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "컨텐츠", "posted": "작성됨", "reply-to": "'%1'에 대한 답글", - "content-editable": "게시하기 전에 콘텐츠를 클릭하여 편집 할 수 있습니다." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/lt/error.json b/public/language/lt/error.json index 3598ac9193..0e7956288d 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Trinti žinutes galima tik %1 sekundę(-es/-ių) po paskelbimo", "chat-deleted-already": "Ši žinutė buvo pašalinta", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Jūs jau balsavote už šį pranešimą.", "reputation-system-disabled": "Reputacijos sistema išjungta.", "downvoting-disabled": "Downvoting yra išjungtas", diff --git a/public/language/lt/post-queue.json b/public/language/lt/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/lt/post-queue.json +++ b/public/language/lt/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/lv/error.json b/public/language/lv/error.json index ce80b1aee3..532521c87b 100644 --- a/public/language/lv/error.json +++ b/public/language/lv/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Pēc publicēšanas ir atļauts tikai %1 sekundes laika izdzēst sarunu", "chat-deleted-already": "Saruna jau ir izdzēsta.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Tu jau balsoji par šo rakstu.", "reputation-system-disabled": "Ranga punktu sistēma ir atspējota.", "downvoting-disabled": "Balsošana \"pret\" ir atspējota", diff --git a/public/language/lv/post-queue.json b/public/language/lv/post-queue.json index 4f68aea318..176a68626f 100644 --- a/public/language/lv/post-queue.json +++ b/public/language/lv/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Rakstu apstiprināšanas rinda", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Saturs", "posted": "Datums", "reply-to": "Atbildēt \"%1\"", - "content-editable": "Pirms publicēšanas var noklikšķināt uz saturu, lai to rediģētu." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/ms/error.json b/public/language/ms/error.json index 5106040b82..9380e709da 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Sistem reputasi dilumpuhkan.", "downvoting-disabled": "Undi turun dilumpuhkan", diff --git a/public/language/ms/post-queue.json b/public/language/ms/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/ms/post-queue.json +++ b/public/language/ms/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/nb/error.json b/public/language/nb/error.json index 755ebfd9b2..1c793058ab 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Ryktesystemet er deaktivert.", "downvoting-disabled": "Nedstemming er deaktivert", diff --git a/public/language/nb/post-queue.json b/public/language/nb/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/nb/post-queue.json +++ b/public/language/nb/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/nl/error.json b/public/language/nl/error.json index ea797e2ea1..ad6434ef7d 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Het is slechts toegestaan om binnen %1 seconde(n) na plaatsen van het chat bericht, deze te verwijderen.", "chat-deleted-already": "Dit chat bericht is al verwijderd.", "chat-restored-already": "Dit chat bericht is al hersteld.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Je hebt al gestemd voor deze post.", "reputation-system-disabled": "Reputatie systeem is uitgeschakeld.", "downvoting-disabled": "Negatief stemmen is uitgeschakeld", diff --git a/public/language/nl/post-queue.json b/public/language/nl/post-queue.json index 7a9877a785..e9642a2e2c 100644 --- a/public/language/nl/post-queue.json +++ b/public/language/nl/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Berichtenwachtrij", "description": "Er zijn geen berichten in de wachtrij.
Om deze functionaliteit in te schakelen, ga naar Instellingen → Bericht → Berichtenwachtrij en schakel Berichtenwachtrij in.", @@ -7,5 +8,11 @@ "content": "Inhoud", "posted": "Geplaatst", "reply-to": "Antwoord naar \"%1\"", - "content-editable": "Je kunt op individuele inhoud klikken om dit aan te passen voor het plaatsen." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/pl/error.json b/public/language/pl/error.json index b0c6f69db0..94094454a5 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Możesz skasować komunikat czatu tylko przez %1 sekund(y) po napisaniu.", "chat-deleted-already": "Ten komunikat czatu jest już skasowany", "chat-restored-already": "Ta wiadomość została już przywrócona", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Już zagłosowałeś na ten post", "reputation-system-disabled": "System reputacji jest wyłączony.", "downvoting-disabled": "Negatywna ocena postów jest wyłączona", diff --git a/public/language/pl/post-queue.json b/public/language/pl/post-queue.json index 270290e155..5661a4b8fb 100644 --- a/public/language/pl/post-queue.json +++ b/public/language/pl/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Kolejka postów", "description": "Nie ma żadnych postów w kolejce.
Aby włączyć tą funkcję, idź do: Ustawienia → Post → Kolejka postów i włącz kolejkę postów.", @@ -7,5 +8,11 @@ "content": "Zawartość", "posted": "Napisano", "reply-to": "Odpowiedz do \"%1\"", - "content-editable": "Możesz kliknąć na danej treści, by edytować ją przed publikacją." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json index 4f65019307..64eca8de40 100644 --- a/public/language/pt-BR/error.json +++ b/public/language/pt-BR/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Você só pode deletar mensagens de chat %1 segundo(s) após postar", "chat-deleted-already": "Essa mensagem de chat já foi deletada", "chat-restored-already": "Essa mensagem de chat já foi restaurada.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Você já votou neste post.", "reputation-system-disabled": "O sistema de reputação está desabilitado.", "downvoting-disabled": "Negativação está desabilitada", diff --git a/public/language/pt-BR/post-queue.json b/public/language/pt-BR/post-queue.json index 3dd2f9342b..dd6fd18d3d 100644 --- a/public/language/pt-BR/post-queue.json +++ b/public/language/pt-BR/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Fila de Posts", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Conteúdo", "posted": "Postado", "reply-to": "Resposta para \"%1\"", - "content-editable": "Você pode clicar em um post individual para editar antes de postar." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json index ddc52e0b16..2e02d2b49a 100644 --- a/public/language/pt-PT/error.json +++ b/public/language/pt-PT/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Só tens permissão para apagar mensagens do chat %1 segundo(s) depois de publicares", "chat-deleted-already": "Esta mensagem já foi apagada.", "chat-restored-already": "Esta mensagem já foi restaurada.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Já votaste nesta publicação.", "reputation-system-disabled": "O sistema de reputação está desativado.", "downvoting-disabled": "Os votos negativos estão desativados", diff --git a/public/language/pt-PT/post-queue.json b/public/language/pt-PT/post-queue.json index c592cc1e4f..06526a5a65 100644 --- a/public/language/pt-PT/post-queue.json +++ b/public/language/pt-PT/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Publicações por Aprovar", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Conteúdo", "posted": "Publicada", "reply-to": "Responder a \"%1\"", - "content-editable": "Podes clicar no conteúdo da publicação para o editares antes de a publicares." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/ro/error.json b/public/language/ro/error.json index 4b494f2870..2fc5f32255 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Sistemul de reputație este dezactivat.", "downvoting-disabled": "Votarea negativă este dezactivată", diff --git a/public/language/ro/post-queue.json b/public/language/ro/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/ro/post-queue.json +++ b/public/language/ro/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/ru/admin/manage/groups.json b/public/language/ru/admin/manage/groups.json index f645e2443a..420c13e5e5 100644 --- a/public/language/ru/admin/manage/groups.json +++ b/public/language/ru/admin/manage/groups.json @@ -9,7 +9,7 @@ "private": "Закрытая", "edit": "Редактировать", "delete": "Удалить", - "privileges": "Privileges", + "privileges": "Права доступа", "download-csv": "CSV", "search-placeholder": "Поиск", "create": "Создать группу", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index 6cfcdda27e..7dadc271ec 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Вам разрешено удалять сообщения чата за% 1 секунду после публикации", "chat-deleted-already": "Это сообщение чата уже удалено.", "chat-restored-already": "Это сообщение чата уже было восстановлено.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Вы уже проголосовали за это сообщение.", "reputation-system-disabled": "Система репутации отключена.", "downvoting-disabled": "Понижение рейтинга отключено", diff --git a/public/language/ru/post-queue.json b/public/language/ru/post-queue.json index 48d59429e6..5669d90214 100644 --- a/public/language/ru/post-queue.json +++ b/public/language/ru/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Очередь на публикацию", "description": "В очереди на публикацию нет сообщений.
Чтобы включить эту функцию, перейдите в Настройки → Сообщения → Очередь на публикацию и включите Очередь на публикацию.", @@ -7,5 +8,11 @@ "content": "Содержимое", "posted": "Время", "reply-to": "Ответ \"%1\"", - "content-editable": "Вы можете отредактировать содержимое сообщения перед публикацией" + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/rw/error.json b/public/language/rw/error.json index 56efaaab99..9edf183390 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Ibijyanye n'itangwa ry'amanota ntibyemerewe. ", "downvoting-disabled": "Kwambura amanota ntibyemerewe", diff --git a/public/language/rw/post-queue.json b/public/language/rw/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/rw/post-queue.json +++ b/public/language/rw/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/sc/error.json b/public/language/sc/error.json index ebf6de4ec7..2f070ca81b 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", "downvoting-disabled": "Downvoting is disabled", diff --git a/public/language/sc/post-queue.json b/public/language/sc/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/sc/post-queue.json +++ b/public/language/sc/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/sk/error.json b/public/language/sk/error.json index b4dc7108c1..6549f41019 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Je Vám umožnené odstrániť správy konverzácie po dobu %1 sekúnd po ich odoslaní", "chat-deleted-already": "Táto správa konverzácie už bola odstránená.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Za tento príspevok ste už hlasovali.", "reputation-system-disabled": "Systém reputácie je zablokovaný.", "downvoting-disabled": "Hlasovanie proti je zablokované", diff --git a/public/language/sk/post-queue.json b/public/language/sk/post-queue.json index ec4966fd70..03869036fd 100644 --- a/public/language/sk/post-queue.json +++ b/public/language/sk/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Príspevky vo fronte", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Obsah", "posted": "Pridané", "reply-to": "Odpovedať na \"%1\"", - "content-editable": "Kvôli úpravám a pred odoslaním príspevku môžete klikať na obsah." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/sl/error.json b/public/language/sl/error.json index a186c812ba..3ad8de943d 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Za to objavo ste že glasovali.", "reputation-system-disabled": "Sistem za ugled je onemogočen.", "downvoting-disabled": "Negativno glasovanje je onemogočeno.", diff --git a/public/language/sl/post-queue.json b/public/language/sl/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/sl/post-queue.json +++ b/public/language/sl/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/sr/error.json b/public/language/sr/error.json index d2500b60d0..ca0bf5e0a4 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Време у којем вам је дозвољено брисање порука ћаскања након објављивања: %1 сек.", "chat-deleted-already": "Ова порука ћаскања је већ избрисана.", "chat-restored-already": "Ова порука ћаскања је већ обновљена.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Већ сте гласали за ову поруку.", "reputation-system-disabled": "Угледи су онемогућени.", "downvoting-disabled": "Негативно гласање је онемогућено", diff --git a/public/language/sr/post-queue.json b/public/language/sr/post-queue.json index 78c828fee3..1b4646fd58 100644 --- a/public/language/sr/post-queue.json +++ b/public/language/sr/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Садржај", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/sv/error.json b/public/language/sv/error.json index 2a245b4fdf..1eed2fdf5f 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Du kan endast radera chattmeddelanden %1 sekunder efter att du skrivit dem", "chat-deleted-already": "Detta chattmeddelande har redan raderats.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Du har redan röstat på det här inlägget.", "reputation-system-disabled": "Ryktessystemet är inaktiverat.", "downvoting-disabled": "Nedröstning är inaktiverat", diff --git a/public/language/sv/post-queue.json b/public/language/sv/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/sv/post-queue.json +++ b/public/language/sv/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/th/error.json b/public/language/th/error.json index f21336e4ed..e130c86aea 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", "chat-deleted-already": "This chat message has already been deleted.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "คุณได้โหวตโพสต์นี้แล้ว", "reputation-system-disabled": "ระบบชื่อเสียงถูกปิดใช้งาน", "downvoting-disabled": "\"การโหวตลง\" ถูกปิดใช้งาน", diff --git a/public/language/th/post-queue.json b/public/language/th/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/th/post-queue.json +++ b/public/language/th/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/tr/error.json b/public/language/tr/error.json index bff70f0089..49ee07e936 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Gönderildikten sonra yalnızca %1 saniye mesajı(ları) silmene izin verilir", "chat-deleted-already": "Bu sohbet mesajı zaten silinmiş.", "chat-restored-already": "Bu sohbet mesajı zaten geri yüklendi.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Bu gönderi için zaten oy verdin.", "reputation-system-disabled": "İtibar sistemi devre dışı.", "downvoting-disabled": "Aşagı oylama kapatılmış", diff --git a/public/language/tr/post-queue.json b/public/language/tr/post-queue.json index 1e8054f526..1ebd1b5579 100644 --- a/public/language/tr/post-queue.json +++ b/public/language/tr/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "İleti Kuyruğu", "description": "İleti kuyruğunda hiçbir ileti bulunmamaktadır.
Bu özelliği aktifleştirmek için, şuraya gidin Ayarlar → İleti → İleti Kuyruğu ve şu özelliği aktifleştirin: İleti Kuyruğu.", @@ -7,5 +8,11 @@ "content": "İçerik", "posted": "Gönderildi", "reply-to": "\"%1\"'e Cevap Ver", - "content-editable": "Göndermeden önce her bir içeriğe tıklayıp düzenleyebilirsiniz." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 8229219d14..5763195b48 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Ви можете видаляти повідомлення чату лише через %1 секунд після публікації", "chat-deleted-already": "Це повідомлення чату вже було видалено.", "chat-restored-already": "Це чат повідомлення вже було відновлене", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Ви вже проголосували за цей пост.", "reputation-system-disabled": "Система репутацій вимкнена.", "downvoting-disabled": "Голосування проти вимкнено", diff --git a/public/language/uk/post-queue.json b/public/language/uk/post-queue.json index 298a366ecf..0a0d13c6c7 100644 --- a/public/language/uk/post-queue.json +++ b/public/language/uk/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Черга Постів", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Зміст", "posted": "Опубліковано", "reply-to": "Відповідь для \"%1\"", - "content-editable": "Ви можете натиснути на окремий вміст, щоб редагувати його перед публікацією." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/vi/error.json b/public/language/vi/error.json index e0f6e0c346..6d915c09d6 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "Bạn chỉ được phép xóa cuộc trò chuyện này sau %1 giây(s) sau khi viết bài.", "chat-deleted-already": "Cuộc trò chuyện này đã được xóa.", "chat-restored-already": "This chat message has already been restored.", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "Bạn đã bỏ phiếu cho bài viết này", "reputation-system-disabled": "Hệ thống tín nhiệm đã bị vô hiệu hóa.", "downvoting-disabled": "Downvote đã bị tắt", diff --git a/public/language/vi/post-queue.json b/public/language/vi/post-queue.json index 2742e3a7af..bfaa367870 100644 --- a/public/language/vi/post-queue.json +++ b/public/language/vi/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "Post Queue", "description": "There are no posts in the post queue.
To enable this feature, go to Settings → Post → Post Queue and enable Post Queue.", @@ -7,5 +8,11 @@ "content": "Content", "posted": "Posted", "reply-to": "Reply to \"%1\"", - "content-editable": "You can click on individual content to edit before posting." + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index 9d586203bf..036c5581f1 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "您只能在发布 %1 秒后删除聊天信息", "chat-deleted-already": "聊天消息已经被删除", "chat-restored-already": "此聊天消息已经恢复。\n", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "您已为此帖回复投过票了。", "reputation-system-disabled": "声望系统已禁用。", "downvoting-disabled": "踩已被禁用", diff --git a/public/language/zh-CN/post-queue.json b/public/language/zh-CN/post-queue.json index 53f7208248..a63312d82b 100644 --- a/public/language/zh-CN/post-queue.json +++ b/public/language/zh-CN/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "发布队列", "description": "发布队列中暂无新帖。启用此特性,请前往设置→发布→发布队列,然后启用发布队列。", @@ -7,5 +8,11 @@ "content": "内容", "posted": "发布", "reply-to": "回复\"%1\"", - "content-editable": "发帖前,您可以点击内容进行编辑。" + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json index 509624f544..3ec3687bb7 100644 --- a/public/language/zh-TW/error.json +++ b/public/language/zh-TW/error.json @@ -133,6 +133,7 @@ "chat-delete-duration-expired": "您只能在發佈 %1 秒後刪除聊天訊息", "chat-deleted-already": "聊天訊息已經被刪除", "chat-restored-already": "此聊天訊息已經恢復。", + "chat-room-does-not-exist": "Chat room does not exist.", "already-voting-for-this-post": "您已讚過此貼文回覆了。", "reputation-system-disabled": "聲望系統已停用。", "downvoting-disabled": "倒讚已被停用", diff --git a/public/language/zh-TW/post-queue.json b/public/language/zh-TW/post-queue.json index 9907d7d81a..43491ab280 100644 --- a/public/language/zh-TW/post-queue.json +++ b/public/language/zh-TW/post-queue.json @@ -1,3 +1,4 @@ + { "post-queue": "貼文隊列", "description": "貼文隊列中暫無新貼文。啟用此功能,請前往設定→貼文→貼文隊列,然後啟用貼文隊列。", @@ -7,5 +8,11 @@ "content": "內容", "posted": "發佈", "reply-to": "回覆\"%1\"", - "content-editable": "發文前,您可以點擊內容進行編輯。" + "content-editable": "Click on content to edit", + "category-editable": "Click on category to edit", + "title-editable": "Click on title to edit", + "reply": "Reply", + "topic": "Topic", + "accept": "Accept", + "reject": "Reject" } \ No newline at end of file diff --git a/public/openapi/read/user/userslug/categories.yaml b/public/openapi/read/user/userslug/categories.yaml index b3d7b5feb5..3860dad988 100644 --- a/public/openapi/read/user/userslug/categories.yaml +++ b/public/openapi/read/user/userslug/categories.yaml @@ -58,4 +58,5 @@ get: type: string title: type: string + - $ref: ../../../components/schemas/Breadcrumbs.yaml#/Breadcrumbs - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 2b6266d64c..c1ea408afa 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -42,6 +42,8 @@ tags: paths: /ping: $ref: 'write/ping.yaml' + /utilities/login: + $ref: 'write/login.yaml' /users/: $ref: 'write/users.yaml' /users/{uid}: @@ -102,10 +104,16 @@ paths: $ref: 'write/posts/pid.yaml' /posts/{pid}/state: $ref: 'write/posts/pid/state.yaml' + /posts/{pid}/move: + $ref: 'write/posts/pid/move.yaml' /posts/{pid}/vote: $ref: 'write/posts/pid/vote.yaml' /posts/{pid}/bookmark: $ref: 'write/posts/pid/bookmark.yaml' + /posts/{pid}/diffs: + $ref: 'write/posts/pid/diffs.yaml' + /posts/{pid}/diffs/{since}: + $ref: 'write/posts/pid/diffs/since.yaml' /admin/settings/{setting}: $ref: 'write/admin/settings/setting.yaml' /files/: diff --git a/public/openapi/write/login.yaml b/public/openapi/write/login.yaml new file mode 100644 index 0000000000..43ba2f8cb9 --- /dev/null +++ b/public/openapi/write/login.yaml @@ -0,0 +1,30 @@ +post: + tags: + - utilities + summary: verify login credentials + description: | + This route accepts a username/password or email/password pair (dependent on forum settings), returning a standard user object if credentials are validated successfully. + requestBody: + content: + application/json: + schema: + type: object + properties: + username: + type: string + example: admin + password: + type: string + example: '123456' + responses: + '200': + description: credentials successfully validated + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../components/schemas/Status.yaml#/Status + response: + $ref: ../components/schemas/UserObject.yaml#/UserObjectSlim \ No newline at end of file diff --git a/public/openapi/write/posts/pid/diffs.yaml b/public/openapi/write/posts/pid/diffs.yaml new file mode 100644 index 0000000000..40d4201185 --- /dev/null +++ b/public/openapi/write/posts/pid/diffs.yaml @@ -0,0 +1,41 @@ +get: + tags: + - posts + summary: get post edit history + description: This operation retrieves a post's edit history + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + responses: + '200': + description: Post history successfully retrieved. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + timestamps: + type: array + items: + type: number + revisions: + type: array + items: + type: object + properties: + timestamp: + type: number + username: + type: string + editable: + type: boolean \ No newline at end of file diff --git a/public/openapi/write/posts/pid/diffs/since.yaml b/public/openapi/write/posts/pid/diffs/since.yaml new file mode 100644 index 0000000000..8db8c6f4ac --- /dev/null +++ b/public/openapi/write/posts/pid/diffs/since.yaml @@ -0,0 +1,65 @@ +get: + tags: + - posts + summary: get single post edit history + description: This operation retrieves a post's edit history + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + - in: path + name: since + schema: + type: number + required: true + description: a valid UNIX timestamp + example: 0 + responses: + '200': + description: Post history successfully retrieved. + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + $ref: ../../../../components/schemas/PostObject.yaml#/PostObject +put: + tags: + - posts + summary: revert a post + description: This operation reverts a post to an earlier version. The revert process will append a new history item to the post's edit history. + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + - in: path + name: since + schema: + type: number + required: true + description: a valid UNIX timestamp + example: 0 + responses: + '200': + description: Post successfully reverted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} \ No newline at end of file diff --git a/public/openapi/write/posts/pid/move.yaml b/public/openapi/write/posts/pid/move.yaml new file mode 100644 index 0000000000..7198554ffb --- /dev/null +++ b/public/openapi/write/posts/pid/move.yaml @@ -0,0 +1,36 @@ +put: + tags: + - posts + summary: move a post + description: This operation moves a post to a different topic. + parameters: + - in: path + name: pid + schema: + type: number + required: true + description: a valid post id + example: 5 + requestBody: + content: + application/json: + schema: + type: object + properties: + tid: + type: number + description: a valid topic id + example: 4 + responses: + '200': + description: Post successfully moved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} \ No newline at end of file diff --git a/public/src/client/topic/diffs.js b/public/src/client/topic/diffs.js index 007182ae8e..3b310f6d85 100644 --- a/public/src/client/topic/diffs.js +++ b/public/src/client/topic/diffs.js @@ -1,6 +1,6 @@ 'use strict'; -define('forum/topic/diffs', ['forum/topic/images'], function () { +define('forum/topic/diffs', ['api', 'forum/topic/images'], function (api) { var Diffs = {}; Diffs.open = function (pid) { @@ -10,11 +10,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; - socket.emit('posts.getDiffs', { pid: pid }, function (err, data) { - if (err) { - return app.alertError(err.message); - } - + api.get(`/posts/${pid}/diffs`, {}).then((data) => { app.parseAndTranslate('partials/modals/post_history', { diffs: data.revisions.map(function (revision) { var timestamp = parseInt(revision.timestamp, 10); @@ -56,7 +52,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { revertEl.prop('disabled', true); }); }); - }); + }).catch(app.alertError); }; Diffs.load = function (pid, since, postContainer) { @@ -64,11 +60,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { return; } - socket.emit('posts.showPostAt', { pid: pid, since: since }, function (err, data) { - if (err) { - return app.alertError(err.message); - } - + api.get(`/posts/${pid}/diffs/${since}`, {}).then((data) => { data.deleted = !!parseInt(data.deleted, 10); app.parseAndTranslate('partials/posts_list', 'posts', { @@ -76,7 +68,7 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { }, function (html) { postContainer.empty().append(html); }); - }); + }).catch(app.alertError); }; Diffs.restore = function (pid, since, modal) { @@ -84,14 +76,10 @@ define('forum/topic/diffs', ['forum/topic/images'], function () { return; } - socket.emit('posts.restoreDiff', { pid: pid, since: since }, function (err) { - if (err) { - return app.alertError(err); - } - + api.put(`/posts/${pid}/diffs/${since}`, {}).then(() => { modal.modal('hide'); app.alertSuccess('[[topic:diffs.post-restored]]'); - }); + }).catch(app.alertError); }; return Diffs; diff --git a/public/src/client/topic/move-post.js b/public/src/client/topic/move-post.js index 16eb4590fb..8a5e68fe32 100644 --- a/public/src/client/topic/move-post.js +++ b/public/src/client/topic/move-post.js @@ -2,8 +2,8 @@ define('forum/topic/move-post', [ - 'components', 'postSelect', 'translator', 'alerts', -], function (components, postSelect, translator, alerts) { + 'components', 'postSelect', 'translator', 'alerts', 'api', +], function (components, postSelect, translator, alerts, api) { var MovePost = {}; var moveModal; @@ -100,10 +100,10 @@ define('forum/topic/move-post', [ if (!ajaxify.data.template.topic || !data.tid) { return; } - socket.emit('posts.movePosts', { pids: data.pids, tid: data.tid }, function (err) { - if (err) { - return app.alertError(err.message); - } + + Promise.all(data.pids.map(pid => api.put(`/posts/${pid}/move`, { + tid: data.tid, + }))).then(() => { data.pids.forEach(function (pid) { components.get('post', 'pid', pid).fadeOut(500, function () { $(this).remove(); @@ -111,7 +111,7 @@ define('forum/topic/move-post', [ }); closeMoveModal(); - }); + }).catch(app.alertError); } function closeMoveModal() { diff --git a/public/src/client/topic/posts.js b/public/src/client/topic/posts.js index 8e14941524..e0dd634049 100644 --- a/public/src/client/topic/posts.js +++ b/public/src/client/topic/posts.js @@ -210,7 +210,9 @@ define('forum/topic/posts', [ html.insertBefore(before); // Now restore the relative position the user was on prior to new post insertion - $(window).scrollTop(scrollTop + ($(document).height() - height)); + if (scrollTop > 0) { + $(window).scrollTop(scrollTop + ($(document).height() - height)); + } } else { components.get('topic').append(html); } diff --git a/public/src/modules/categorySearch.js b/public/src/modules/categorySearch.js index 5e8f2fcd26..ed78a5ebe0 100644 --- a/public/src/modules/categorySearch.js +++ b/public/src/modules/categorySearch.js @@ -14,18 +14,23 @@ define('categorySearch', function () { var toggleVisibility = searchEl.parent('[component="category/dropdown"]').length > 0 || searchEl.parent('[component="category-selector"]').length > 0; - var categoryEls = el.find('[component="category/list"] [data-cid]'); + var listEl = el.find('[component="category/list"]'); + var clonedList = listEl.clone(); + var categoryEls = clonedList.find('[data-cid]'); + el.on('show.bs.dropdown', function () { + var cidToParentCid = {}; + function revealParents(cid) { - var parentCid = el.find('[component="category/list"] [data-cid="' + cid + '"]').attr('data-parent-cid'); + var parentCid = cidToParentCid[cid]; if (parentCid) { - el.find('[component="category/list"] [data-cid="' + parentCid + '"]').removeClass('hidden'); + clonedList.find('[data-cid="' + parentCid + '"]').removeClass('hidden'); revealParents(parentCid); } } function revealChildren(cid) { - var els = el.find('[component="category/list"] [data-parent-cid="' + cid + '"]'); + var els = clonedList.find('[data-parent-cid="' + cid + '"]'); els.each(function (index, el) { var $el = $(el); $el.removeClass('hidden'); @@ -39,12 +44,14 @@ define('categorySearch', function () { var cids = []; categoryEls.each(function () { var liEl = $(this); - var isMatch = liEl.attr('data-name').toLowerCase().indexOf(val) !== -1; + var isMatch = cids.length < 100 && (!val || (val.length > 1 && liEl.attr('data-name').toLowerCase().indexOf(val) !== -1)); if (noMatch && isMatch) { noMatch = false; } if (isMatch && val) { - cids.push(liEl.attr('data-cid')); + var cid = liEl.attr('data-cid'); + cids.push(cid); + cidToParentCid[cid] = parseInt(liEl.attr('data-parent-cid'), 10); } liEl.toggleClass('hidden', !isMatch).find('[component="category-markup"]').css({ 'font-weight': val && isMatch ? 'bold' : 'normal' }); }); @@ -54,6 +61,7 @@ define('categorySearch', function () { revealChildren(cid); }); + listEl.html(clonedList.html()); el.find('[component="category/list"] [component="category/no-matches"]').toggleClass('hidden', !noMatch); } if (toggleVisibility) { @@ -65,7 +73,7 @@ define('categorySearch', function () { ev.preventDefault(); ev.stopPropagation(); }); - searchEl.find('input').val('').on('keyup', updateList); + searchEl.find('input').val('').on('keyup', utils.debounce(updateList, 200)); updateList(); }); el.on('shown.bs.dropdown', function () { diff --git a/public/src/modules/topicList.js b/public/src/modules/topicList.js index 8ddf566d48..ff3d67e8b0 100644 --- a/public/src/modules/topicList.js +++ b/public/src/modules/topicList.js @@ -247,7 +247,7 @@ define('topicList', [ } if (!topicSelect.getSelectedTids().length) { - infinitescroll.removeExtra(topicListEl.find('[component="category/topic"]'), direction, config.topicsPerPage * 3); + infinitescroll.removeExtra(topicListEl.find('[component="category/topic"]'), direction, Math.max(60, config.topicsPerPage * 3)); } html.find('.timeago').timeago(); diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 2fd50d5703..30ec2e1979 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -286,6 +286,9 @@ var out = translated; translatedArgs.forEach(function (arg, i) { var escaped = arg.replace(/%(?=\d)/g, '%').replace(/\\,/g, ','); + // fix double escaped translation keys, see https://github.com/NodeBB/NodeBB/issues/9206 + escaped = escaped.replace(/&lsqb;/g, '[') + .replace(/&rsqb;/g, ']'); out = out.replace(new RegExp('%' + (i + 1), 'g'), escaped); }); return out; diff --git a/src/api/posts.js b/src/api/posts.js index c01b377340..301df87a2e 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -4,6 +4,7 @@ const validator = require('validator'); const _ = require('lodash'); const utils = require('../utils'); +const user = require('../user'); const posts = require('../posts'); const topics = require('../topics'); const groups = require('../groups'); @@ -12,6 +13,7 @@ const events = require('../events'); const privileges = require('../privileges'); const apiHelpers = require('./helpers'); const websockets = require('../socket.io'); +const socketHelpers = require('../socket.io/helpers'); const postsAPI = module.exports; @@ -194,6 +196,27 @@ async function isMainAndLastPost(pid) { }; } +postsAPI.move = async function (caller, data) { + const canMove = await Promise.all([ + privileges.topics.isAdminOrMod(data.tid, caller.uid), + privileges.posts.canMove(data.pid, caller.uid), + ]); + if (!canMove.every(Boolean)) { + throw new Error('[[error:no-privileges]]'); + } + + await topics.movePostToTopic(caller.uid, data.pid, data.tid); + + const [postDeleted, topicDeleted] = await Promise.all([ + posts.getPostField(data.pid, 'deleted'), + topics.getTopicField(data.tid, 'deleted'), + ]); + + if (!postDeleted && !topicDeleted) { + socketHelpers.sendNotificationToPostOwner(data.pid, caller.uid, 'move', 'notifications:moved_your_post'); + } +}; + postsAPI.upvote = async function (caller, data) { return await apiHelpers.postCommand(caller, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data); }; @@ -213,3 +236,61 @@ postsAPI.bookmark = async function (caller, data) { postsAPI.unbookmark = async function (caller, data) { return await apiHelpers.postCommand(caller, 'unbookmark', 'bookmarked', '', data); }; + +async function diffsPrivilegeCheck(pid, uid) { + const [deleted, privilegesData] = await Promise.all([ + posts.getPostField(pid, 'deleted'), + privileges.posts.get([pid], uid), + ]); + + const allowed = privilegesData[0]['posts:history'] && (deleted ? privilegesData[0]['posts:view_deleted'] : true); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } +} + +postsAPI.getDiffs = async (caller, data) => { + await diffsPrivilegeCheck(data.pid, caller.uid); + const timestamps = await posts.diffs.list(data.pid); + const post = await posts.getPostFields(data.pid, ['timestamp', 'uid']); + + const diffs = await posts.diffs.get(data.pid); + const uids = diffs.map(diff => diff.uid || null); + uids.push(post.uid); + let usernames = await user.getUsersFields(uids, ['username']); + usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null)); + + let canEdit = true; + try { + await user.isPrivilegedOrSelf(caller.uid, post.uid); + } catch (e) { + canEdit = false; + } + + timestamps.push(post.timestamp); + + return { + timestamps: timestamps, + revisions: timestamps.map((timestamp, idx) => ({ + timestamp: timestamp, + username: usernames[idx], + })), + editable: canEdit, + }; +}; + +postsAPI.loadDiff = async (caller, data) => { + await diffsPrivilegeCheck(data.pid, caller.uid); + return await posts.diffs.load(data.pid, data.since, caller.uid); +}; + +postsAPI.restoreDiff = async (caller, data) => { + const cid = await posts.getCidByPid(data.pid); + const canEdit = await privileges.categories.can('edit', cid, caller.uid); + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + const edit = await posts.diffs.restore(data.pid, data.since, caller.uid, apiHelpers.buildReqObject(caller)); + websockets.in('topic_' + edit.topic.tid).emit('event:post_edited', edit); +}; diff --git a/src/cli/index.js b/src/cli/index.js index 8d53f069f7..88683d24ba 100644 --- a/src/cli/index.js +++ b/src/cli/index.js @@ -1,3 +1,5 @@ +/* eslint-disable import/order */ + 'use strict'; const fs = require('fs'); @@ -35,13 +37,13 @@ try { try { fs.accessSync(path.join(paths.nodeModules, 'semver/package.json'), fs.constants.R_OK); - var semver = require('semver'); - var defaultPackage = require('../../install/package.json'); + const semver = require('semver'); + const defaultPackage = require('../../install/package.json'); - var checkVersion = function (packageName) { - var version = JSON.parse(fs.readFileSync(path.join(paths.nodeModules, packageName, 'package.json'), 'utf8')).version; + const checkVersion = function (packageName) { + const version = JSON.parse(fs.readFileSync(path.join(paths.nodeModules, packageName, 'package.json'), 'utf8')).version; if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) { - var e = new TypeError('Incorrect dependency version: ' + packageName); + const e = new TypeError('Incorrect dependency version: ' + packageName); e.code = 'DEP_WRONG_VERSION'; throw e; } @@ -67,14 +69,13 @@ try { } require('colors'); -// eslint-disable-next-line -var nconf = require('nconf'); -// eslint-disable-next-line -var program = require('commander'); +const nconf = require('nconf'); +const { program } = require('commander'); +const yargs = require('yargs'); -var pkg = require('../../package.json'); -var file = require('../file'); -var prestart = require('../prestart'); +const pkg = require('../../package.json'); +const file = require('../file'); +const prestart = require('../prestart'); program .name('./nodebb') @@ -86,19 +87,23 @@ program .option('-d, --dev', 'Development mode, including verbose logging', false) .option('-l, --log', 'Log subprocess output to console', false); -nconf.argv().env({ +// provide a yargs object ourselves +// otherwise yargs will consume `--help` or `help` +// and `nconf` will exit with useless usage info +const opts = yargs(process.argv.slice(2)).help(false).exitProcess(false); +nconf.argv(opts).env({ separator: '__', }); -var env = program.dev ? 'development' : (process.env.NODE_ENV || 'production'); +const env = program.dev ? 'development' : (process.env.NODE_ENV || 'production'); process.env.NODE_ENV = env; global.env = env; prestart.setupWinston(); // Alternate configuration file support -var configFile = path.resolve(paths.baseDir, nconf.get('config') || 'config.json'); -var configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database')); +const configFile = path.resolve(paths.baseDir, nconf.get('config') || 'config.json'); +const configExists = file.existsSync(configFile) || (nconf.get('url') && nconf.get('secret') && nconf.get('database')); prestart.loadConfig(configFile); prestart.versionCheck(); @@ -195,7 +200,7 @@ program require('./manage').build(targets.length ? targets : true, options); }) .on('--help', function () { - require('./manage').buildTargets(); + require('../meta/aliases').buildTargets(); }); program .command('activate [plugin]') @@ -223,7 +228,7 @@ program }); // reset -var resetCommand = program.command('reset'); +const resetCommand = program.command('reset'); resetCommand .description('Reset plugins, themes, settings, etc') @@ -233,7 +238,7 @@ resetCommand .option('-s, --settings', 'Reset settings to their default values') .option('-a, --all', 'All of the above') .action(function (options) { - var valid = ['theme', 'plugin', 'widgets', 'settings', 'all'].some(function (x) { + const valid = ['theme', 'plugin', 'widgets', 'settings', 'all'].some(function (x) { return options[x]; }); if (!valid) { @@ -295,10 +300,11 @@ program return program.help(); } - var command = program.commands.find(function (command) { return command._name === name; }); + const command = program.commands.find(function (command) { return command._name === name; }); if (command) { command.help(); } else { + console.log(`error: unknown command '${command}'.`); program.help(); } }); @@ -311,4 +317,4 @@ if (process.argv.length === 2) { program.executables = false; -program.parse(process.argv); +program.parse(); diff --git a/src/cli/manage.js b/src/cli/manage.js index c986a2725f..acce6810c9 100644 --- a/src/cli/manage.js +++ b/src/cli/manage.js @@ -2,7 +2,6 @@ const winston = require('winston'); const childProcess = require('child_process'); -const _ = require('lodash'); const CliGraph = require('cli-graph'); const build = require('../meta/build'); @@ -13,27 +12,6 @@ const analytics = require('../analytics'); const reset = require('./reset'); const { pluginNamePattern, themeNamePattern } = require('../constants'); -function buildTargets() { - var aliases = build.aliases; - var length = 0; - var output = Object.keys(aliases).map(function (name) { - var arr = aliases[name]; - if (name.length > length) { - length = name.length; - } - - return [name, arr.join(', ')]; - }).map(function (tuple) { - return ' ' + _.padEnd('"' + tuple[0] + '"', length + 2).magenta + ' | ' + tuple[1]; - }).join('\n'); - console.log( - '\n\n Build targets:\n' + - ('\n ' + _.padEnd('Target', length + 2) + ' | Aliases').green + - '\n ------------------------------------------------------\n'.blue + - output + '\n' - ); -} - async function activate(plugin) { if (themeNamePattern.test(plugin)) { await reset.reset({ @@ -63,11 +41,10 @@ async function activate(plugin) { type: 'plugin-activate', text: plugin, }); - process.exit(0); } catch (err) { winston.error('An error occurred during plugin activation\n' + err.stack); - throw err; } + process.exit(0); } async function listPlugins() { @@ -176,7 +153,6 @@ async function buildWrapper(targets, options) { } exports.build = buildWrapper; -exports.buildTargets = buildTargets; exports.activate = activate; exports.listPlugins = listPlugins; exports.listEvents = listEvents; diff --git a/src/controllers/accounts/categories.js b/src/controllers/accounts/categories.js index a1253ef57e..1f0f434f09 100644 --- a/src/controllers/accounts/categories.js +++ b/src/controllers/accounts/categories.js @@ -3,6 +3,7 @@ const user = require('../../user'); const categories = require('../../categories'); const accountHelpers = require('./helpers'); +const helpers = require('../helpers'); const categoriesController = module.exports; @@ -25,5 +26,9 @@ categoriesController.get = async function (req, res, next) { }); userData.categories = categoriesData; userData.title = '[[pages:account/watched_categories, ' + userData.username + ']]'; + userData.breadcrumbs = helpers.buildBreadcrumbs([ + { text: userData.username, url: '/user/' + userData.userslug }, + { text: '[[pages:categories]]' }, + ]); res.render('account/categories', userData); }; diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index eec386bb8f..48ed0393e6 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -226,7 +226,7 @@ authenticationController.login = function (req, res, next) { plugins.hooks.fire('filter:login.check', { req: req, res: res, userData: req.body }, (err) => { if (err) { - return helpers.noScriptErrors(req, res, err.message, 403); + return (res.locals.noScriptErrors || helpers.noScriptErrors)(req, res, err.message, 403); } if (req.body.username && utils.isEmailValid(req.body.username) && loginWith.includes('email')) { async.waterfall([ @@ -235,14 +235,14 @@ authenticationController.login = function (req, res, next) { }, function (username, next) { req.body.username = username || req.body.username; - continueLogin(req, res, next); + (res.locals.continueLogin || continueLogin)(req, res, next); }, ], next); } else if (loginWith.includes('username') && !validator.isEmail(req.body.username)) { - continueLogin(req, res, next); + (res.locals.continueLogin || continueLogin)(req, res, next); } else { err = '[[error:wrong-login-type-' + loginWith + ']]'; - helpers.noScriptErrors(req, res, err, 500); + (res.locals.noScriptErrors || helpers.noScriptErrors)(req, res, err, 400); } }); }; diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 517035f0f8..5cd77f4487 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -461,6 +461,11 @@ helpers.generateError = (statusCode, message) => { payload.status.message = message || 'HTTPS is required for requests to the write api, please re-send your request via HTTPS'; break; + case 429: + payload.status.code = 'too-many-requests'; + payload.status.message = message || 'You have made too many requests, please try again later'; + break; + case 500: payload.status.code = 'internal-server-error'; payload.status.message = message || payload.status.message; diff --git a/src/controllers/home.js b/src/controllers/home.js index 4b69e719c7..d8fa7a1d83 100644 --- a/src/controllers/home.js +++ b/src/controllers/home.js @@ -7,7 +7,7 @@ const meta = require('../meta'); const user = require('../user'); function adminHomePageRoute() { - return (meta.config.homePageRoute || meta.config.homePageCustom || '').replace(/^\/+/, '') || 'categories'; + return ((meta.config.homePageRoute === 'custom' ? meta.config.homePageCustom : meta.config.homePageRoute) || 'categories').replace(/^\//, ''); } async function getUserHomeRoute(uid) { diff --git a/src/controllers/write/index.js b/src/controllers/write/index.js index 66d6598780..e781467c18 100644 --- a/src/controllers/write/index.js +++ b/src/controllers/write/index.js @@ -9,3 +9,4 @@ Write.topics = require('./topics'); Write.posts = require('./posts'); Write.admin = require('./admin'); Write.files = require('./files'); +Write.utilities = require('./utilities'); diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 7988439a1f..8cd094848c 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -38,6 +38,14 @@ Posts.delete = async (req, res) => { helpers.formatApiResponse(200, res); }; +Posts.move = async (req, res) => { + await api.posts.move(req, { + pid: req.params.pid, + tid: req.body.tid, + }); + helpers.formatApiResponse(200, res); +}; + async function mock(req) { const tid = await posts.getPostField(req.params.pid, 'tid'); return { pid: req.params.pid, room_id: `topic_${tid}` }; @@ -73,3 +81,16 @@ Posts.unbookmark = async (req, res) => { await api.posts.unbookmark(req, data); helpers.formatApiResponse(200, res); }; + +Posts.getDiffs = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.getDiffs(req, { ...req.params })); +}; + +Posts.loadDiff = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.loadDiff(req, { ...req.params })); +}; + +Posts.restoreDiff = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.restoreDiff(req, { ...req.params })); +}; + diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index c20cc6a269..5696f51a07 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -86,16 +86,34 @@ Topics.unfollow = async (req, res) => { }; Topics.addTags = async (req, res) => { + if (!await privileges.topics.canEdit(req.params.tid, req.user.uid)) { + return helpers.formatApiResponse(403, res); + } + await topics.createTags(req.body.tags, req.params.tid, Date.now()); helpers.formatApiResponse(200, res); }; Topics.deleteTags = async (req, res) => { + if (!await privileges.topics.canEdit(req.params.tid, req.user.uid)) { + return helpers.formatApiResponse(403, res); + } + await topics.deleteTopicTags(req.params.tid); helpers.formatApiResponse(200, res); }; Topics.getThumbs = async (req, res) => { + if (isFinite(req.params.tid)) { // post_uuids can be passed in occasionally, in that case no checks are necessary + const [exists, canRead] = await Promise.all([ + topics.exists(req.params.tid), + privileges.topics.can('topics:read', req.params.tid, req.uid), + ]); + if (!exists || !canRead) { + return helpers.formatApiResponse(403, res); + } + } + helpers.formatApiResponse(200, res, await topics.thumbs.get(req.params.tid)); }; diff --git a/src/controllers/write/utilities.js b/src/controllers/write/utilities.js new file mode 100644 index 0000000000..1d81fa0698 --- /dev/null +++ b/src/controllers/write/utilities.js @@ -0,0 +1,51 @@ +'use strict'; + +const user = require('../../user'); +const authenticationController = require('../authentication'); +const slugify = require('../../slugify'); +const helpers = require('../helpers'); + +const Utilities = module.exports; + +Utilities.ping = {}; +Utilities.ping.get = (req, res) => { + helpers.formatApiResponse(200, res, { + pong: true, + }); +}; + +Utilities.ping.post = (req, res) => { + helpers.formatApiResponse(200, res, { + uid: req.user.uid, + received: req.body, + }); +}; + +Utilities.login = (req, res) => { + res.locals.continueLogin = async (req, res) => { + const { username, password } = req.body; + + const userslug = slugify(username); + const uid = await user.getUidByUserslug(userslug); + let ok = false; + try { + ok = await user.isPasswordCorrect(uid, password, req.ip); + } catch (err) { + if (err.message === '[[error:account-locked]]') { + return helpers.formatApiResponse(429, res, err); + } + } + + if (ok) { + const userData = await user.getUsers([uid], uid); + helpers.formatApiResponse(200, res, userData.pop()); + } else { + helpers.formatApiResponse(403, res); + } + }; + res.locals.noScriptErrors = (req, res, err, statusCode) => { + helpers.formatApiResponse(statusCode, res, new Error(err)); + }; + + authenticationController.login(req, res); +}; diff --git a/src/emailer.js b/src/emailer.js index 7a2fcdc46d..f1717ccb26 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -230,6 +230,16 @@ Emailer.send = async (template, uid, params) => { params.uid = uid; params.username = userData.username; params.rtl = await translator.translate('[[language:dir]]', userSettings.userLang) === 'rtl'; + + const result = await Plugins.hooks.fire('filter:email.cancel', { + cancel: false, // set to true in plugin to cancel sending email + template: template, + params: params, + }); + + if (result.cancel) { + return; + } await Emailer.sendToEmail(template, userData.email, userSettings.userLang, params); }; diff --git a/src/flags.js b/src/flags.js index 8bf57257fd..c6a2a90edd 100644 --- a/src/flags.js +++ b/src/flags.js @@ -36,7 +36,11 @@ Flags.init = async function () { if (!Array.isArray(value)) { sets.push(prefix + value); } else if (value.length) { - value.forEach(x => orSets.push(prefix + x)); + if (value.length === 1) { + sets.push(prefix + value[0]); + } else { + value.forEach(x => orSets.push(prefix + x)); + } } } @@ -67,6 +71,10 @@ Flags.init = async function () { case 'mine': sets.push('flags:byAssignee:' + uid); break; + + case 'unresolved': + prepareSets(sets, orSets, 'flags:byState:', ['open', 'wip']); + break; } }, }, @@ -113,9 +121,13 @@ Flags.get = async function (flagId) { return data.flag; }; -Flags.list = async function (data) { - const filters = data.filters || {}; +Flags.getCount = async function ({ uid, filters }) { + filters = filters || {}; + const flagIds = await Flags.getFlagIdsWithFilters({ filters, uid }); + return flagIds.length; +}; +Flags.getFlagIdsWithFilters = async function ({ filters, uid }) { let sets = []; const orSets = []; @@ -126,7 +138,7 @@ Flags.list = async function (data) { for (var type in filters) { if (filters.hasOwnProperty(type)) { if (Flags._filters.hasOwnProperty(type)) { - Flags._filters[type](sets, orSets, filters[type], data.uid); + Flags._filters[type](sets, orSets, filters[type], uid); } else { winston.warn('[flags/list] No flag filter type found: ' + type); } @@ -152,6 +164,15 @@ Flags.list = async function (data) { } } + return flagIds; +}; + +Flags.list = async function (data) { + const filters = data.filters || {}; + let flagIds = await Flags.getFlagIdsWithFilters({ + filters, + uid: data.uid, + }); flagIds = await Flags.sort(flagIds, data.sort); // Create subset for parsing based on page number (n=20) diff --git a/src/groups/update.js b/src/groups/update.js index 0e34169660..bced8783df 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -24,10 +24,11 @@ module.exports = function (Groups) { values: values, })); - // Case some values as bool (if not boolean already) + // Cast some values as bool (if not boolean already) + // 'true' and '1' = true, everything else false ['userTitleEnabled', 'private', 'hidden', 'disableJoinRequests', 'disableLeave'].forEach((prop) => { if (values.hasOwnProperty(prop) && typeof values[prop] !== 'boolean') { - values[prop] = !!parseInt(values[prop], 10); + values[prop] = values[prop] === 'true' || parseInt(values[prop], 10) === 1; } }); diff --git a/src/meta/aliases.js b/src/meta/aliases.js new file mode 100644 index 0000000000..d35df972f1 --- /dev/null +++ b/src/meta/aliases.js @@ -0,0 +1,44 @@ +'use strict'; + +const _ = require('lodash'); + +const 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'], +}; + +exports.aliases = aliases; + +function buildTargets() { + var length = 0; + var output = Object.keys(aliases).map(function (name) { + var arr = aliases[name]; + if (name.length > length) { + length = name.length; + } + + return [name, arr.join(', ')]; + }).map(function (tuple) { + return ' ' + _.padEnd('"' + tuple[0] + '"', length + 2).magenta + ' | ' + tuple[1]; + }).join('\n'); + console.log( + '\n\n Build targets:\n' + + ('\n ' + _.padEnd('Target', length + 2) + ' | Aliases').green + + '\n ------------------------------------------------------\n'.blue + + output + '\n' + ); +} + +exports.buildTargets = buildTargets; diff --git a/src/meta/build.js b/src/meta/build.js index 9c63902d3a..dbf69296e1 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -8,6 +8,7 @@ const path = require('path'); const mkdirp = require('mkdirp'); const cacheBuster = require('./cacheBuster'); +const { aliases } = require('./aliases'); let meta; const targetHandlers = { @@ -47,26 +48,7 @@ const targetHandlers = { }, }; -let 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'], -}; - -exports.aliases = aliases; - -aliases = Object.keys(aliases).reduce(function (prev, key) { +const aliasMap = Object.keys(aliases).reduce(function (prev, key) { var arr = aliases[key]; arr.forEach(function (alias) { prev[alias] = key; @@ -151,7 +133,7 @@ exports.build = async function (targets, options) { // get full target name .map(function (target) { target = target.toLowerCase().replace(/-/g, ''); - if (!aliases[target]) { + if (!aliasMap[target]) { winston.warn('[build] Unknown target: ' + target); if (target.includes(',')) { winston.warn('[build] Are you specifying multiple targets? Separate them with spaces:'); @@ -161,7 +143,7 @@ exports.build = async function (targets, options) { return false; } - return aliases[target]; + return aliasMap[target]; }) // filter nonexistent targets .filter(Boolean); diff --git a/src/middleware/assert.js b/src/middleware/assert.js index 80fd009e7a..6f2617fdcb 100644 --- a/src/middleware/assert.js +++ b/src/middleware/assert.js @@ -46,7 +46,7 @@ Assert.topic = helpers.try(async (req, res, next) => { Assert.post = helpers.try(async (req, res, next) => { if (!await posts.exists(req.params.pid)) { - return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-topic]]')); + return controllerHelpers.formatApiResponse(404, res, new Error('[[error:no-post]]')); } next(); diff --git a/src/middleware/header.js b/src/middleware/header.js index 2174f7c461..c2f6594e80 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -1,25 +1,26 @@ 'use strict'; -var nconf = require('nconf'); -var jsesc = require('jsesc'); -var _ = require('lodash'); +const nconf = require('nconf'); +const jsesc = require('jsesc'); +const _ = require('lodash'); const validator = require('validator'); -var util = require('util'); +const util = require('util'); -var db = require('../database'); -var user = require('../user'); -var topics = require('../topics'); -var messaging = require('../messaging'); -var meta = require('../meta'); -var plugins = require('../plugins'); -var navigation = require('../navigation'); -var translator = require('../translator'); -var privileges = require('../privileges'); -var languages = require('../languages'); -var utils = require('../utils'); -var helpers = require('./helpers'); +const db = require('../database'); +const user = require('../user'); +const topics = require('../topics'); +const messaging = require('../messaging'); +const flags = require('../flags'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const navigation = require('../navigation'); +const translator = require('../translator'); +const privileges = require('../privileges'); +const languages = require('../languages'); +const utils = require('../utils'); +const helpers = require('./helpers'); -var controllers = { +const controllers = { api: require('../controllers/api'), helpers: require('../controllers/helpers'), }; @@ -48,9 +49,9 @@ middleware.buildHeader = helpers.try(async function buildHeader(req, res, next) middleware.buildHeaderAsync = util.promisify(middleware.buildHeader); middleware.renderHeader = async function renderHeader(req, res, data) { - var registrationType = meta.config.registrationType || 'normal'; + const registrationType = meta.config.registrationType || 'normal'; res.locals.config = res.locals.config || {}; - var templateValues = { + const templateValues = { title: meta.config.title || '', 'title:url': meta.config['title:url'] || '', description: meta.config.description || '', @@ -79,9 +80,6 @@ middleware.renderHeader = async function renderHeader(req, res, data) { timeagoCode: languages.userTimeagoCode(res.locals.config.userLang), browserTitle: translator.translate(controllers.helpers.buildTitle(translator.unescape(data.title))), navigation: navigation.get(req.uid), - unreadData: topics.getUnreadData({ uid: req.uid }), - unreadChatCount: messaging.getUnreadCount(req.uid), - unreadNotificationCount: user.notifications.getUnreadCount(req.uid), }); const unreadData = { @@ -105,44 +103,15 @@ middleware.renderHeader = async function renderHeader(req, res, data) { templateValues.bootswatchSkin = (parseInt(meta.config.disableCustomUserSkins, 10) !== 1 ? res.locals.config.bootswatchSkin : '') || meta.config.bootswatchSkin || ''; templateValues.config.bootswatchSkin = templateValues.bootswatchSkin || 'noskin'; // TODO remove in v1.12.0+ - - const unreadCounts = results.unreadData.counts; - const unreadCount = { - topic: unreadCounts[''] || 0, - newTopic: unreadCounts.new || 0, - watchedTopic: unreadCounts.watched || 0, - unrepliedTopic: unreadCounts.unreplied || 0, - chat: results.unreadChatCount || 0, - notification: results.unreadNotificationCount || 0, - }; - - Object.keys(unreadCount).forEach(function (key) { - if (unreadCount[key] > 99) { - unreadCount[key] = '99+'; - } - }); - - const tidsByFilter = results.unreadData.tidsByFilter; - results.navigation = results.navigation.map(function (item) { - function modifyNavItem(item, route, filter, content) { - if (item && validator.unescape(item.originalRoute) === route) { - unreadData[filter] = _.zipObject(tidsByFilter[filter], tidsByFilter[filter].map(() => true)); - item.content = content; - if (unreadCounts[filter] > 0) { - item.iconClass += ' unread-count'; - } - } - } - modifyNavItem(item, '/unread', '', unreadCount.topic); - modifyNavItem(item, '/unread?filter=new', 'new', unreadCount.newTopic); - modifyNavItem(item, '/unread?filter=watched', 'watched', unreadCount.watchedTopic); - modifyNavItem(item, '/unread?filter=unreplied', 'unreplied', unreadCount.unrepliedTopic); - return item; - }); - templateValues.browserTitle = results.browserTitle; - templateValues.navigation = results.navigation; - templateValues.unreadCount = unreadCount; + ({ + navigation: templateValues.navigation, + unreadCount: templateValues.unreadCount, + } = await appendUnreadCounts({ + uid: req.uid, + navigation: results.navigation, + unreadData, + })); templateValues.isAdmin = results.user.isAdmin; templateValues.isGlobalMod = results.user.isGlobalMod; templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod; @@ -183,6 +152,73 @@ middleware.renderHeader = async function renderHeader(req, res, data) { return await req.app.renderAsync('header', hookReturn.templateValues); }; +async function appendUnreadCounts({ uid, navigation, unreadData }) { + const originalRoutes = navigation.map(nav => nav.originalRoute); + const calls = { + unreadData: topics.getUnreadData({ uid: uid }), + unreadChatCount: messaging.getUnreadCount(uid), + unreadNotificationCount: user.notifications.getUnreadCount(uid), + unreadFlagCount: (async function () { + if (originalRoutes.includes('/flags') && await user.isPrivileged(uid)) { + return flags.getCount({ + uid, + filters: { + quick: 'unresolved', + cid: (await user.isAdminOrGlobalMod(uid)) ? [] : (await user.getModeratedCids(uid)), + }, + }); + } + return 0; + }()), + }; + const results = await utils.promiseParallel(calls); + + const unreadCounts = results.unreadData.counts; + const unreadCount = { + topic: unreadCounts[''] || 0, + newTopic: unreadCounts.new || 0, + watchedTopic: unreadCounts.watched || 0, + unrepliedTopic: unreadCounts.unreplied || 0, + chat: results.unreadChatCount || 0, + notification: results.unreadNotificationCount || 0, + flags: results.unreadFlagCount || 0, + }; + + Object.keys(unreadCount).forEach(function (key) { + if (unreadCount[key] > 99) { + unreadCount[key] = '99+'; + } + }); + + const tidsByFilter = results.unreadData.tidsByFilter; + navigation = navigation.map(function (item) { + function modifyNavItem(item, route, filter, content) { + if (item && item.originalRoute === route) { + unreadData[filter] = _.zipObject(tidsByFilter[filter], tidsByFilter[filter].map(() => true)); + item.content = content; + if (unreadCounts[filter] > 0) { + item.iconClass += ' unread-count'; + } + } + } + modifyNavItem(item, '/unread', '', unreadCount.topic); + modifyNavItem(item, '/unread?filter=new', 'new', unreadCount.newTopic); + modifyNavItem(item, '/unread?filter=watched', 'watched', unreadCount.watchedTopic); + modifyNavItem(item, '/unread?filter=unreplied', 'unreplied', unreadCount.unrepliedTopic); + + ['flags'].forEach((prop) => { + if (item && item.originalRoute === `/${prop}` && unreadCount[prop] > 0) { + item.iconClass += ' unread-count'; + item.content = unreadCount.flags; + } + }); + + return item; + }); + + return { navigation, unreadCount }; +} + middleware.renderFooter = async function renderFooter(req, res, templateValues) { const data = await plugins.hooks.fire('filter:middleware.renderFooter', { req: req, @@ -204,7 +240,7 @@ middleware.renderFooter = async function renderFooter(req, res, templateValues) }; function modifyTitle(obj) { - var title = controllers.helpers.buildTitle(meta.config.homePageTitle || '[[pages:home]]'); + const title = controllers.helpers.buildTitle(meta.config.homePageTitle || '[[pages:home]]'); obj.browserTitle = title; if (obj.metaTags) { diff --git a/src/navigation/index.js b/src/navigation/index.js index ed639afbfa..a581508367 100644 --- a/src/navigation/index.js +++ b/src/navigation/index.js @@ -1,6 +1,7 @@ 'use strict'; const nconf = require('nconf'); +const validator = require('validator'); const admin = require('./admin'); const groups = require('../groups'); @@ -12,7 +13,7 @@ navigation.get = async function (uid) { let data = await admin.get(); data = data.filter(item => item && item.enabled).map(function (item) { - item.originalRoute = item.route; + item.originalRoute = validator.unescape(item.route); if (!item.route.startsWith('http')) { item.route = relative_path + item.route; diff --git a/src/notifications.js b/src/notifications.js index b98a987cb8..1b12d27e39 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -327,10 +327,9 @@ Notifications.prune = async function () { ]); await batch.processSortedSet('users:joindate', async function (uids) { - await Promise.all([ - db.sortedSetsRemoveRangeByScore(uids.map(uid => 'uid:' + uid + ':notifications:unread'), '-inf', cutoffTime), - db.sortedSetsRemoveRangeByScore(uids.map(uid => 'uid:' + uid + ':notifications:read'), '-inf', cutoffTime), - ]); + const unread = uids.map(uid => 'uid:' + uid + ':notifications:unread'); + const read = uids.map(uid => 'uid:' + uid + ':notifications:read'); + await db.sortedSetsRemoveRangeByScore(unread.concat(read), '-inf', cutoffTime); }, { batch: 500, interval: 100 }); } catch (err) { if (err) { diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js index 0ed353e946..e603bc73eb 100644 --- a/src/plugins/hooks.js +++ b/src/plugins/hooks.js @@ -92,7 +92,7 @@ Hooks.unregister = function (id, hook, method) { Hooks.fire = async function (hook, params) { const hookList = plugins.loadedHooks[hook]; const hookType = hook.split(':')[0]; - if (global.env === 'development' && hook !== 'action:plugins.fireHook') { + if (global.env === 'development' && hook !== 'action:plugins.firehook') { winston.verbose('[plugins/fireHook] ' + hook); } @@ -102,8 +102,8 @@ Hooks.fire = async function (hook, params) { } const result = await hookTypeToMethod[hookType](hook, hookList, params); - if (hook !== 'action:plugins.fireHook') { - Hooks.fire('action:plugins.fireHook', { hook: hook, params: params }); + if (hook !== 'action:plugins.firehook') { + Hooks.fire('action:plugins.firehook', { hook: hook, params: params }); } if (result !== undefined) { return result; diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 10d5c7c785..188fb10ac6 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -106,8 +106,14 @@ module.exports = function (privileges) { } cids = _.uniq(cids); - const results = await privileges.categories.getBase(privilege, cids, uid); - return cids.filter((cid, index) => !!cid && !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); + const [categoryData, allowedTo, isAdmin] = await Promise.all([ + categories.getCategoriesFields(cids, ['disabled']), + helpers.isAllowedTo(privilege, uid, cids), + user.isAdministrator(uid), + ]); + return cids.filter( + (cid, index) => !!cid && !categoryData[index].disabled && (allowedTo[index] || isAdmin) + ); }; privileges.categories.getBase = async function (privilege, cids, uid) { diff --git a/src/routes/authentication.js b/src/routes/authentication.js index 8de1915c21..9831d72677 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -98,13 +98,21 @@ Auth.reloadRoutes = async function (params) { loginStrategies = loginStrategies || []; loginStrategies.forEach(function (strategy) { if (strategy.url) { - router.get(strategy.url, Auth.middleware.applyCSRF, function (req, res, next) { - req.session.ssoState = req.csrfToken && req.csrfToken(); - passport.authenticate(strategy.name, { + router.get(strategy.url, Auth.middleware.applyCSRF, async function (req, res, next) { + let opts = { scope: strategy.scope, prompt: strategy.prompt || undefined, - state: req.session.ssoState, - })(req, res, next); + }; + + if (strategy.checkState) { + req.session.ssoState = req.csrfToken && req.csrfToken(); + opts.state = req.session.ssoState; + } + + // Allow SSO plugins to override/append options (for use in passport prototype authorizationParams) + ({ opts } = await plugins.hooks.fire('filter:auth.options', { req, res, opts })); + + passport.authenticate(strategy.name, opts)(req, res, next); }); } diff --git a/src/routes/write/index.js b/src/routes/write/index.js index 6cf266acdb..b2689cc586 100644 --- a/src/routes/write/index.js +++ b/src/routes/write/index.js @@ -4,6 +4,7 @@ const winston = require('winston'); const meta = require('../../meta'); const plugins = require('../../plugins'); const middleware = require('../../middleware'); +const writeControllers = require('../../controllers/write'); const helpers = require('../../controllers/helpers'); const Write = module.exports; @@ -38,19 +39,10 @@ Write.reload = async (params) => { router.use('/api/v3/posts', require('./posts')()); router.use('/api/v3/admin', require('./admin')()); router.use('/api/v3/files', require('./files')()); + router.use('/api/v3/utilities', require('./utilities')()); - router.get('/api/v3/ping', function (req, res) { - helpers.formatApiResponse(200, res, { - pong: true, - }); - }); - - router.post('/api/v3/ping', middleware.authenticate, function (req, res) { - helpers.formatApiResponse(200, res, { - uid: req.user.uid, - received: req.body, - }); - }); + router.get('/api/v3/ping', writeControllers.utilities.ping.get); + router.post('/api/v3/ping', middleware.authenticate, writeControllers.utilities.ping.post); /** * Plugins can add routes to the Write API by attaching a listener to the diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index 837f8def8c..d842270e82 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -18,11 +18,17 @@ module.exports = function () { setupApiRoute(router, 'put', '/:pid/state', [...middlewares, middleware.assert.post], controllers.write.posts.restore); setupApiRoute(router, 'delete', '/:pid/state', [...middlewares, middleware.assert.post], controllers.write.posts.delete); + setupApiRoute(router, 'put', '/:pid/move', [...middlewares, middleware.assert.post, middleware.checkRequired.bind(null, ['tid'])], controllers.write.posts.move); + setupApiRoute(router, 'put', '/:pid/vote', [...middlewares, middleware.checkRequired.bind(null, ['delta']), middleware.assert.post], controllers.write.posts.vote); setupApiRoute(router, 'delete', '/:pid/vote', [...middlewares, middleware.assert.post], controllers.write.posts.unvote); setupApiRoute(router, 'put', '/:pid/bookmark', [...middlewares, middleware.assert.post], controllers.write.posts.bookmark); setupApiRoute(router, 'delete', '/:pid/bookmark', [...middlewares, middleware.assert.post], controllers.write.posts.unbookmark); + setupApiRoute(router, 'get', '/:pid/diffs', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.getDiffs); + setupApiRoute(router, 'get', '/:pid/diffs/:since', [middleware.authenticateOrGuest, middleware.assert.post], controllers.write.posts.loadDiff); + setupApiRoute(router, 'put', '/:pid/diffs/:since', [...middlewares, middleware.assert.post], controllers.write.posts.restoreDiff); + return router; }; diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index d2c05a38bd..465ecdea45 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -35,7 +35,7 @@ module.exports = function () { setupApiRoute(router, 'put', '/:tid/tags', [...middlewares, middleware.checkRequired.bind(null, ['tags']), middleware.assert.topic], controllers.write.topics.addTags); setupApiRoute(router, 'delete', '/:tid/tags', [...middlewares, middleware.assert.topic], controllers.write.topics.deleteTags); - setupApiRoute(router, 'get', '/:tid/thumbs', [], controllers.write.topics.getThumbs); + setupApiRoute(router, 'get', '/:tid/thumbs', middleware.authenticateOrGuest, controllers.write.topics.getThumbs); setupApiRoute(router, 'post', '/:tid/thumbs', [multipartMiddleware, middleware.validateFiles, ...middlewares], controllers.write.topics.addThumb); setupApiRoute(router, 'put', '/:tid/thumbs', [], controllers.write.topics.migrateThumbs); setupApiRoute(router, 'delete', '/:tid/thumbs', [...middlewares, middleware.checkRequired.bind(null, ['path'])], controllers.write.topics.deleteThumb); diff --git a/src/routes/write/utilities.js b/src/routes/write/utilities.js new file mode 100644 index 0000000000..085536766c --- /dev/null +++ b/src/routes/write/utilities.js @@ -0,0 +1,16 @@ +'use strict'; + +const router = require('express').Router(); +const middleware = require('../../middleware'); +const controllers = require('../../controllers'); +const routeHelpers = require('../helpers'); + +const setupApiRoute = routeHelpers.setupApiRoute; + +module.exports = function () { + // The "ping" routes are mounted at root level, but for organizational purposes, the controllers are in `utilities.js` + + setupApiRoute(router, 'post', '/login', [middleware.checkRequired.bind(null, ['username', 'password'])], controllers.write.utilities.login); + + return router; +}; diff --git a/src/socket.io/posts/diffs.js b/src/socket.io/posts/diffs.js index fb74f6a9ef..a1e2043243 100644 --- a/src/socket.io/posts/diffs.js +++ b/src/socket.io/posts/diffs.js @@ -1,67 +1,21 @@ 'use strict'; -const posts = require('../../posts'); -const user = require('../../user'); -const privileges = require('../../privileges'); -const apiHelpers = require('../../api/helpers'); +const api = require('../../api'); const websockets = require('..'); module.exports = function (SocketPosts) { SocketPosts.getDiffs = async function (socket, data) { - await privilegeCheck(data.pid, socket.uid); - const timestamps = await posts.diffs.list(data.pid); - const post = await posts.getPostFields(data.pid, ['timestamp', 'uid']); - - const diffs = await posts.diffs.get(data.pid); - const uids = diffs.map(diff => diff.uid || null); - uids.push(post.uid); - let usernames = await user.getUsersFields(uids, ['username']); - usernames = usernames.map(userObj => (userObj.uid ? userObj.username : null)); - - let canEdit = true; - try { - await user.isPrivilegedOrSelf(socket.uid, post.uid); - } catch (e) { - canEdit = false; - } - - timestamps.push(post.timestamp); - - return { - timestamps: timestamps, - revisions: timestamps.map((timestamp, idx) => ({ - timestamp: timestamp, - username: usernames[idx], - })), - editable: canEdit, - }; + websockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/diffs'); + return await api.posts.getDiffs(socket, data); }; SocketPosts.showPostAt = async function (socket, data) { - await privilegeCheck(data.pid, socket.uid); - return await posts.diffs.load(data.pid, data.since, socket.uid); + websockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/diffs/:since'); + return await api.posts.loadDiff(socket, data); }; - async function privilegeCheck(pid, uid) { - const [deleted, privilegesData] = await Promise.all([ - posts.getPostField(pid, 'deleted'), - privileges.posts.get([pid], uid), - ]); - - const allowed = privilegesData[0]['posts:history'] && (deleted ? privilegesData[0]['posts:view_deleted'] : true); - if (!allowed) { - throw new Error('[[error:no-privileges]]'); - } - } - SocketPosts.restoreDiff = async function (socket, data) { - const cid = await posts.getCidByPid(data.pid); - const canEdit = await privileges.categories.can('edit', cid, socket.uid); - if (!canEdit) { - throw new Error('[[error:no-privileges]]'); - } - - const edit = await posts.diffs.restore(data.pid, data.since, socket.uid, apiHelpers.buildReqObject(socket)); - websockets.in('topic_' + edit.topic.tid).emit('event:post_edited', edit); + websockets.warnDeprecated(socket, 'PUT /api/v3/posts/:pid/diffs/:since'); + return await api.posts.restoreDiff(socket, data); }; }; diff --git a/src/socket.io/posts/move.js b/src/socket.io/posts/move.js index c1637fad51..9424d73a06 100644 --- a/src/socket.io/posts/move.js +++ b/src/socket.io/posts/move.js @@ -1,45 +1,33 @@ 'use strict'; -const privileges = require('../../privileges'); -const topics = require('../../topics'); -const posts = require('../../posts'); -const socketHelpers = require('../helpers'); +const api = require('../../api'); +const sockets = require('..'); module.exports = function (SocketPosts) { - SocketPosts.movePost = async function (socket, data) { - await SocketPosts.movePosts(socket, { pids: [data.pid], tid: data.tid }); - }; - - SocketPosts.movePosts = async function (socket, data) { + function moveChecks(socket, typeCheck, data) { if (!socket.uid) { throw new Error('[[error:not-logged-in]]'); } - if (!data || !Array.isArray(data.pids) || !data.tid) { + if (!data || !typeCheck || !data.tid) { throw new Error('[[error:invalid-data]]'); } + } - const canMove = await privileges.topics.isAdminOrMod(data.tid, socket.uid); - if (!canMove) { - throw new Error('[[error:no-privileges]]'); - } + SocketPosts.movePost = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v3/posts/:pid/move'); - for (const pid of data.pids) { - /* eslint-disable no-await-in-loop */ - const canMove = await privileges.posts.canMove(pid, socket.uid); - if (!canMove) { - throw new Error('[[error:no-privileges]]'); - } - await topics.movePostToTopic(socket.uid, pid, data.tid); + moveChecks(socket, isFinite(data.pid), data); + await api.posts.move(socket, data); + }; - const [postDeleted, topicDeleted] = await Promise.all([ - posts.getPostField(pid, 'deleted'), - topics.getTopicField(data.tid, 'deleted'), - ]); + SocketPosts.movePosts = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v3/posts/:pid/move'); - if (!postDeleted && !topicDeleted) { - socketHelpers.sendNotificationToPostOwner(pid, socket.uid, 'move', 'notifications:moved_your_post'); - } - } + moveChecks(socket, !Array.isArray(data.pids), data); + await Promise.all(data.pids.map(async pid => api.posts.move(socket, { + tid: data.tid, + pid, + }))); }; }; diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index d65b99b3c4..169bc8d60e 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -32,7 +32,7 @@ Thumbs.get = async function (tids) { } const hasTimestampPrefix = /^\d+-/; - const upload_url = nconf.get('upload_url'); + const upload_url = nconf.get('relative_path') + nconf.get('upload_url'); const sets = tids.map(tid => `${validator.isUUID(String(tid)) ? 'draft' : 'topic'}:${tid}:thumbs`); const thumbs = await Promise.all(sets.map(set => getThumbs(set))); let response = thumbs.map((thumbSet, idx) => thumbSet.map(thumb => ({ diff --git a/src/topics/tools.js b/src/topics/tools.js index bc23e55ccb..8d04f4603e 100644 --- a/src/topics/tools.js +++ b/src/topics/tools.js @@ -146,7 +146,7 @@ module.exports = function (Topics) { throw new Error('[[error:no-topic]]'); } - if (uid !== 'system' && !await privileges.topics.can('moderate', tid, uid)) { + if (uid !== 'system' && !await privileges.topics.isAdminOrMod(tid, uid)) { throw new Error('[[error:no-privileges]]'); } diff --git a/src/upgrades/1.15.0/consolidate_flags.js b/src/upgrades/1.15.0/consolidate_flags.js index 49da790840..481895e9d3 100644 --- a/src/upgrades/1.15.0/consolidate_flags.js +++ b/src/upgrades/1.15.0/consolidate_flags.js @@ -32,7 +32,7 @@ module.exports = { } methods.push( - db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reports`, flagObj.datetime, flagObj.description), + db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reports`, flagObj.datetime, String(flagObj.description).substr(0, 250)), db.sortedSetAdd.bind(db, `flag:${flagObj.flagId}:reporters`, flagObj.datetime, flagObj.uid) ); diff --git a/src/upgrades/1.15.0/fullname_search_set.js b/src/upgrades/1.15.0/fullname_search_set.js index 9f479af145..c3955226bd 100644 --- a/src/upgrades/1.15.0/fullname_search_set.js +++ b/src/upgrades/1.15.0/fullname_search_set.js @@ -16,7 +16,7 @@ module.exports = { const userData = await user.getUsersFields(uids, ['uid', 'fullname']); const bulkAdd = userData .filter(u => u.uid && u.fullname) - .map(u => ['fullname:sorted', 0, u.fullname.substr(0, 255).toLowerCase() + ':' + u.uid]); + .map(u => ['fullname:sorted', 0, String(u.fullname).substr(0, 255).toLowerCase() + ':' + u.uid]); await db.sortedSetAddBulk(bulkAdd); }, { batch: 500, diff --git a/src/upgrades/1.15.0/verified_users_group.js b/src/upgrades/1.15.0/verified_users_group.js index d09b5fa9e5..9d940f1f84 100644 --- a/src/upgrades/1.15.0/verified_users_group.js +++ b/src/upgrades/1.15.0/verified_users_group.js @@ -13,6 +13,9 @@ module.exports = { timestamp: Date.UTC(2020, 9, 13), method: async function () { const progress = this.progress; + + const maxGroupLength = meta.config.maximumGroupNameLength; + meta.config.maximumGroupNameLength = 30; const timestamp = await db.getObjectField('group:administrators', 'timestamp'); const verifiedExists = await groups.exists('verified-users'); if (!verifiedExists) { @@ -38,7 +41,8 @@ module.exports = { timestamp: timestamp + 1, }); } - + // restore setting + meta.config.maximumGroupNameLength = maxGroupLength; await batch.processSortedSet('users:joindate', async function (uids) { progress.incr(uids.length); const userData = await user.getUsersFields(uids, ['uid', 'email:confirmed']); diff --git a/src/user/data.js b/src/user/data.js index 1fd101a8a9..6bc1f9055b 100644 --- a/src/user/data.js +++ b/src/user/data.js @@ -37,12 +37,14 @@ module.exports = function (User) { User.guestData = { uid: 0, username: '[[global:guest]]', + displayname: '[[global:guest]]', userslug: '', fullname: '[[global:guest]]', email: '', 'icon:text': '?', 'icon:bgColor': '#aaa', groupTitle: '', + groupTitleArray: [], status: 'offline', reputation: 0, 'email:confirmed': 0, @@ -68,14 +70,19 @@ module.exports = function (User) { fields = fields.filter(value => value !== 'password'); } - let users = await db.getObjectsFields(uniqueUids.map(uid => 'user:' + uid), fields); + const users = await db.getObjectsFields(uniqueUids.map(uid => 'user:' + uid), fields); const result = await plugins.hooks.fire('filter:user.getFields', { uids: uniqueUids, users: users, fields: fields, }); - users = uidsToUsers(uids, uniqueUids, result.users); - return await modifyUserData(users, fields, fieldsToRemove); + result.users.forEach((user, index) => { + if (uniqueUids[index] > 0 && !user.uid) { + user.oldUid = uniqueUids[index]; + } + }); + await modifyUserData(result.users, fields, fieldsToRemove); + return uidsToUsers(uids, uniqueUids, result.users); }; function ensureRequiredFields(fields, fieldsToRemove) { @@ -110,12 +117,13 @@ module.exports = function (User) { function uidsToUsers(uids, uniqueUids, usersData) { const uidToUser = _.zipObject(uniqueUids, usersData); const users = uids.map(function (uid) { - const returnPayload = uidToUser[uid] || { ...User.guestData }; - if (uid > 0 && !returnPayload.uid) { - returnPayload.oldUid = parseInt(uid, 10); + const user = uidToUser[uid] || { ...User.guestData }; + if (!parseInt(user.uid, 10)) { + user.username = (user.hasOwnProperty('oldUid') && parseInt(user.oldUid, 10)) ? '[[global:former_user]]' : '[[global:guest]]'; + user.displayname = user.username; } - return returnPayload; + return user; }); return users; } @@ -142,19 +150,14 @@ module.exports = function (User) { async function modifyUserData(users, requestedFields, fieldsToRemove) { let uidToSettings = {}; if (meta.config.showFullnameAsDisplayName) { - const uids = _.uniq(users.map(user => user.uid)); + const uids = users.map(user => user.uid); uidToSettings = _.zipObject(uids, await db.getObjectsFields( uids.map(uid => 'user:' + uid + ':settings'), ['showfullname'] )); } - const uidToUser = {}; - users.forEach(function (user) { - uidToUser[user.uid] = user; - }); - await Promise.all(Object.keys(uidToUser).map(async function (uid) { - const user = uidToUser[uid]; + await Promise.all(users.map(async function (user) { if (!user) { return; } @@ -171,14 +174,10 @@ module.exports = function (User) { } if (!parseInt(user.uid, 10)) { - user.uid = 0; - user.username = (user.hasOwnProperty('oldUid') && parseInt(user.oldUid, 10)) ? '[[global:former_user]]' : '[[global:guest]]'; - user.displayname = user.username; - user.userslug = ''; + for (const [key, value] of Object.entries(User.guestData)) { + user[key] = value; + } user.picture = User.getDefaultAvatar(); - user['icon:text'] = '?'; - user['icon:bgColor'] = '#aaa'; - user.groupTitle = ''; } if (user.hasOwnProperty('groupTitle')) { diff --git a/src/user/invite.js b/src/user/invite.js index 8085ae4ece..b939f6cd68 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -38,6 +38,10 @@ module.exports = function (User) { }; User.sendInvitationEmail = async function (uid, email, groupsToJoin) { + if (!uid) { + throw new Error('[[error:invalid-uid]]'); + } + const email_exists = await User.getUidByEmail(email); if (email_exists) { throw new Error('[[error:email-taken]]'); @@ -104,6 +108,11 @@ module.exports = function (User) { } async function prepareInvitation(uid, email, groupsToJoin) { + const inviterExists = await User.exists(uid); + if (!inviterExists) { + throw new Error('[[error:invalid-uid]]'); + } + const token = utils.generateUUID(); const registerLink = nconf.get('url') + '/register?token=' + token + '&email=' + encodeURIComponent(email); diff --git a/src/views/modals/topic-thumbs.tpl b/src/views/modals/topic-thumbs.tpl index 814ea86718..7ee9773836 100644 --- a/src/views/modals/topic-thumbs.tpl +++ b/src/views/modals/topic-thumbs.tpl @@ -5,7 +5,7 @@ {{{ each thumbs }}}
- +

diff --git a/test/api.js b/test/api.js index e4c28cfcef..7401b152ea 100644 --- a/test/api.js +++ b/test/api.js @@ -135,6 +135,12 @@ describe('API', async () => { title: 'Test Topic 2', content: 'Test topic 2 content', }); + await topics.post({ + uid: unprivUid, + cid: testCategory.cid, + title: 'Test Topic 3', + content: 'Test topic 3 content', + }); // Create a sample flag await flags.create('post', 1, unprivUid, 'sample reasons', Date.now()); @@ -332,7 +338,7 @@ describe('API', async () => { } }); - it('should resolve with a 200 when called', async () => { + it('should not error out when called', async () => { await setupData(); if (csrfToken) { @@ -372,7 +378,7 @@ describe('API', async () => { }); } } catch (e) { - assert(!e, `${method.toUpperCase()} ${path} resolved with ${e.message}`); + assert(!e, `${method.toUpperCase()} ${path} errored with: ${e.message}`); } }); diff --git a/test/authentication.js b/test/authentication.js index 6a1d255bac..35be6ad0c2 100644 --- a/test/authentication.js +++ b/test/authentication.js @@ -422,7 +422,7 @@ describe('authentication', function () { loginUser('ginger@nodebb.org', '123456', function (err, response, body) { meta.config.allowLoginWith = 'username-email'; assert.ifError(err); - assert.equal(response.statusCode, 500); + assert.equal(response.statusCode, 400); assert.equal(body, '[[error:wrong-login-type-username]]'); done(); }); diff --git a/test/controllers.js b/test/controllers.js index 670ab3be34..382e1ccd62 100644 --- a/test/controllers.js +++ b/test/controllers.js @@ -95,17 +95,17 @@ describe('Controllers', function () { assert(hookData.res); assert(hookData.next); - hookData.res.render('custom', { + hookData.res.render('mycustompage', { works: true, }); } var message = utils.generateUUID(); - var name = 'custom.tpl'; + var name = 'mycustompage.tpl'; var tplPath = path.join(nconf.get('views_dir'), name); before(async () => { plugins.registerHook('myTestPlugin', { - hook: 'action:homepage.get:custom', + hook: 'action:homepage.get:mycustompage', method: hookMethod, }); @@ -224,14 +224,14 @@ describe('Controllers', function () { }); it('api should work with hook', function (done) { - meta.configs.set('homePageRoute', 'custom', function (err) { + meta.configs.set('homePageRoute', 'mycustompage', function (err) { assert.ifError(err); request(nconf.get('url') + '/api', { json: true }, function (err, res, body) { assert.ifError(err); assert.equal(res.statusCode, 200); assert.equal(body.works, true); - assert.equal(body.template.custom, true); + assert.equal(body.template.mycustompage, true); done(); }); @@ -239,7 +239,7 @@ describe('Controllers', function () { }); it('should render with hook', function (done) { - meta.configs.set('homePageRoute', 'custom', function (err) { + meta.configs.set('homePageRoute', 'mycustompage', function (err) { assert.ifError(err); request(nconf.get('url'), function (err, res, body) { diff --git a/test/topicThumbs.js b/test/topicThumbs.js index 7be9e56f89..bc952139d3 100644 --- a/test/topicThumbs.js +++ b/test/topicThumbs.js @@ -94,7 +94,7 @@ describe('Topic thumbs', () => { assert.deepStrictEqual(thumbs, [{ id: 1, name: 'test.png', - url: `${nconf.get('upload_url')}${relativeThumbPaths[0]}`, + url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, }]); }); @@ -104,7 +104,7 @@ describe('Topic thumbs', () => { [{ id: 1, name: 'test.png', - url: `${nconf.get('upload_url')}${relativeThumbPaths[0]}`, + url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, }], [], ]); @@ -153,7 +153,7 @@ describe('Topic thumbs', () => { { id: 2, name: 'test.png', - url: `${nconf.get('upload_url')}${relativeThumbPaths[0]}`, + url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`, }, { id: 2, @@ -163,7 +163,7 @@ describe('Topic thumbs', () => { { id: 2, name: 'test2.png', - url: `${nconf.get('upload_url')}${relativeThumbPaths[1]}`, + url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[1]}`, }, ]); }); diff --git a/test/translator.js b/test/translator.js index 1776c06b9b..bd6d0f0fbb 100644 --- a/test/translator.js +++ b/test/translator.js @@ -135,6 +135,16 @@ describe('new Translator(language)', function () { }); }); + it('should translate escaped translation arguments properly', function () { + // https://github.com/NodeBB/NodeBB/issues/9206 + var translator = Translator.create('en-GB'); + + var key = '[[notifications:upvoted_your_post_in, test1, error: Error: [[error:group-name-too-long]] on NodeBB Upgrade]]'; + return translator.translate(key).then(function (translated) { + assert.strictEqual(translated, 'test1 has upvoted your post in error: Error: [[error:group-name-too-long]] on NodeBB Upgrade.'); + }); + }); + it('should properly escape and ignore % and \\, in arguments', function () { var translator = Translator.create('en-GB');