mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-24 17:30:39 +01:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bc8cf1ba0 | ||
|
|
62e162cf1e | ||
|
|
a5d92da9dd | ||
|
|
2bd6eea2fa | ||
|
|
42b9fbc91c | ||
|
|
5c0bf7ccbe | ||
|
|
30b6bcfca1 | ||
|
|
de2669a2c6 | ||
|
|
21fb8590e5 | ||
|
|
c931183287 | ||
|
|
ae5afdbc66 | ||
|
|
5343d2a01b | ||
|
|
2ec81eff43 | ||
|
|
f2ca93f6c6 | ||
|
|
82f0efb14b | ||
|
|
df08b47163 | ||
|
|
c27567289f | ||
|
|
c33730530e | ||
|
|
67055006df | ||
|
|
e0b2065802 | ||
|
|
4d2d76897a | ||
|
|
7397873db3 | ||
|
|
5b7c3671c8 | ||
|
|
188ec62f9a | ||
|
|
48c1c7594d | ||
|
|
73ff25887c | ||
|
|
830f142b7a | ||
|
|
1aff9cad91 | ||
|
|
37b48b82a4 | ||
|
|
e9a8e19508 | ||
|
|
894f392bfc | ||
|
|
c2961ad4cd | ||
|
|
57f14e419f | ||
|
|
18b2150edd | ||
|
|
fb100ac731 | ||
|
|
bb725987b3 | ||
|
|
73a50d1718 | ||
|
|
93aa43f717 | ||
|
|
9ed6961af8 | ||
|
|
4b94c033c4 | ||
|
|
9e685e657a | ||
|
|
767c1d1faf | ||
|
|
a3a38e4ba3 | ||
|
|
cfd5027245 | ||
|
|
56427e4f9d | ||
|
|
b331b9423b | ||
|
|
c03d5db71e | ||
|
|
f5a59991fc | ||
|
|
e45a6de24b | ||
|
|
22fc8fe38f | ||
|
|
17d0b40efa | ||
|
|
1545223e7f | ||
|
|
f054a4f44d | ||
|
|
8c762d3228 | ||
|
|
3f8248d673 | ||
|
|
2ca38e7b95 | ||
|
|
6976925943 | ||
|
|
f4282c091b | ||
|
|
791551098c | ||
|
|
ec58700f6d | ||
|
|
8cf4a6f62e | ||
|
|
3bd9a87154 | ||
|
|
edd2fc38fc | ||
|
|
1b29dbb69d | ||
|
|
40e7b86da9 | ||
|
|
326b92687f | ||
|
|
e335d0f601 | ||
|
|
845c8013b6 | ||
|
|
7a5bcc2171 | ||
|
|
af6ce44737 | ||
|
|
f3306d038a | ||
|
|
76732140f3 | ||
|
|
c6681a1725 | ||
|
|
bf92ee0e5f | ||
|
|
8335f90ae0 | ||
|
|
202378b939 | ||
|
|
705cd13ad3 | ||
|
|
b5598a6e5d | ||
|
|
c241baf641 | ||
|
|
d68352cce5 | ||
|
|
0713482bd4 | ||
|
|
1d3c0e5a2b | ||
|
|
6d819b056e | ||
|
|
bff5ce2d79 | ||
|
|
24e58c2895 | ||
|
|
93ccf604db | ||
|
|
4821b21e81 | ||
|
|
f6c96948fe | ||
|
|
a46b2bbc45 | ||
|
|
c13f0e2128 | ||
|
|
ce924eca0d | ||
|
|
c20b20a7aa | ||
|
|
82eb55d77d | ||
|
|
050e43f8b4 | ||
|
|
9b6dad367d | ||
|
|
727f879e5b | ||
|
|
fe662f3a46 | ||
|
|
8e77673d39 | ||
|
|
3f950d5162 | ||
|
|
96cc0617c5 | ||
|
|
ccf8739344 | ||
|
|
7e52a7a574 | ||
|
|
21d9806ca9 | ||
|
|
e7fcf482f3 | ||
|
|
d80c80b618 | ||
|
|
dec0e7deac | ||
|
|
c7ff98a12d | ||
|
|
5836bf4a05 | ||
|
|
a5357812c6 | ||
|
|
c7bd7dbfe6 | ||
|
|
ec4dadabd4 | ||
|
|
3509ed9461 | ||
|
|
cb8d94563a | ||
|
|
e83260ca28 | ||
|
|
4bf1ce42e6 | ||
|
|
7e922936d0 | ||
|
|
3c8ce70c74 | ||
|
|
babcd17e6c | ||
|
|
ec6ffaad4e | ||
|
|
ce3aa95053 | ||
|
|
7aab01d87a | ||
|
|
01d276cbee | ||
|
|
9758b7af2c | ||
|
|
dd3e1a2861 | ||
|
|
2a97342035 | ||
|
|
d5525c873b | ||
|
|
e7c3634f9a | ||
|
|
9c647c6ce2 | ||
|
|
52fc05edfe | ||
|
|
3aa7b8552a | ||
|
|
36523c67b8 | ||
|
|
60cbd1480d | ||
|
|
f3e59508ae | ||
|
|
4834cde335 | ||
|
|
01da76e1dc | ||
|
|
d2425942a6 | ||
|
|
8d7475be7b | ||
|
|
046ea12022 |
10
.github/workflows/docker.yml
vendored
10
.github/workflows/docker.yml
vendored
@@ -13,13 +13,14 @@ on:
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -32,14 +33,15 @@ jobs:
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: nodebb/docker
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
174
CHANGELOG.md
174
CHANGELOG.md
@@ -1,3 +1,155 @@
|
||||
#### v2.8.11 (2023-04-11)
|
||||
|
||||
##### Chores
|
||||
|
||||
* incrementing version number - v2.8.10 (5b7c3671)
|
||||
* update changelog for v2.8.10 (188ec62f)
|
||||
|
||||
##### Continuous Integration
|
||||
|
||||
* publish to ghcr instead of docker hub (c2756728)
|
||||
|
||||
##### Documentation Changes
|
||||
|
||||
* update readme with new screenshot and updated copy for Harmony (67055006)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* don't crash on objects with toString property (4d2d7689)
|
||||
* fire action:user.online on user login (7397873d)
|
||||
|
||||
##### Tests
|
||||
|
||||
* update socket.io test (e0b20658)
|
||||
|
||||
#### v2.8.10 (2023-03-27)
|
||||
|
||||
##### Chores
|
||||
|
||||
* up composer-default (e9a8e195)
|
||||
* incrementing version number - v2.8.9 (57f14e41)
|
||||
* update changelog for v2.8.9 (18b2150e)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* #11403, remove loader.js crash counter logic (830f142b)
|
||||
* don't crash if event name is not a string (37b48b82)
|
||||
* closes #11173, move cache clear code (c2961ad4)
|
||||
|
||||
##### Other Changes
|
||||
|
||||
* fix arrow (1aff9cad)
|
||||
* whitespace (894f392b)
|
||||
|
||||
#### v2.8.9 (2023-03-19)
|
||||
|
||||
##### Chores
|
||||
|
||||
* up cron (73a50d17)
|
||||
* incrementing version number - v2.8.8 (b331b942)
|
||||
* update changelog for v2.8.8 (c03d5db7)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* thumb remove on windows, closes #11357 (767c1d1f)
|
||||
* #11357 clear cache on thumb remove (a3a38e4b)
|
||||
* closes #11352, try/catch rss feeds (cfd50272)
|
||||
* closes #11343, don't crash if tags array is empty (56427e4f)
|
||||
|
||||
##### Code Style Changes
|
||||
|
||||
* more fixes (93aa43f7)
|
||||
|
||||
##### Tests
|
||||
|
||||
* openapi for thumbs (9e685e65)
|
||||
|
||||
#### v2.8.8 (2023-03-09)
|
||||
|
||||
##### Chores
|
||||
|
||||
* incrementing version number - v2.8.7 (3f8248d6)
|
||||
* update changelog for v2.8.7 (2ca38e7b)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* stop topic navigation hotkeys from firing if in a mousetrap-enabled form element (22fc8fe3)
|
||||
* stop topic navigation hotkeys from firing if in a mousetrap-enabled form element (17d0b40e)
|
||||
* tag filtering when changing filter to watched topics (1545223e)
|
||||
* get cid from pid instead of passing in (f054a4f4)
|
||||
* closes #11331, allow 0 length content if set to 0 in acp (8c762d32)
|
||||
|
||||
#### v2.8.7 (2023-03-01)
|
||||
|
||||
##### Chores
|
||||
|
||||
* incrementing version number - v2.8.6 (af6ce447)
|
||||
* update changelog for v2.8.6 (f3306d03)
|
||||
|
||||
##### Documentation Changes
|
||||
|
||||
* update openapi spec to include info about passing in timestamps for topic creation, removing timestamp as valid request param for topic replying (40e7b86d)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* display 25 topics on category feed (79155109)
|
||||
* object destructuring overwriting type parameter (ec58700f)
|
||||
* alert on page load (8cf4a6f6)
|
||||
* show error alert if password change fails (3bd9a871)
|
||||
* update main post timestamp when rescheduling (edd2fc38)
|
||||
* show admins/globalmods if content is purged (326b9268)
|
||||
* email expiry timestamps (e335d0f6)
|
||||
* #11259, clean old emails when updating via admin (#11260) (845c8013)
|
||||
* #11257, onSuccessfulLogin called with improper uid (7a5bcc21)
|
||||
|
||||
##### Tests
|
||||
|
||||
* add dummy emailer hook in authentication test (1b29dbb6)
|
||||
|
||||
#### v2.8.6 (2023-02-03)
|
||||
|
||||
##### Chores
|
||||
|
||||
* **i18n:** fallback strings for new resources: nodebb.error (8335f90a)
|
||||
* incrementing version number - v2.8.5 (bff5ce2d)
|
||||
* update changelog for v2.8.5 (24e58c28)
|
||||
|
||||
##### New Features
|
||||
|
||||
* add sitemap filter hooks for categories/topic pages (bf92ee0e)
|
||||
* closes #11241, add missing error lang keys (c241baf6)
|
||||
* #11240, only show relevant users in flags assignee list (0713482b)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* #11254, return check for reroll property (202378b9)
|
||||
* closes #11249, notification uses displayname (705cd13a)
|
||||
* wrong link to topics in acp dashboard (b5598a6e)
|
||||
* https://github.com/NodeBB/NodeBB/issues/11239 (1d3c0e5a)
|
||||
* notif filter selecte field (6d819b05)
|
||||
|
||||
##### Other Changes
|
||||
|
||||
* remove unused (d68352cc)
|
||||
|
||||
#### v2.8.5 (2023-01-27)
|
||||
|
||||
##### Chores
|
||||
|
||||
* incrementing version number - v2.8.4 (a46b2bbc)
|
||||
* update changelog for v2.8.4 (c13f0e21)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* import resolution within plugin modules (#11219) (f6c96948)
|
||||
|
||||
#### v2.8.4 (2023-01-26)
|
||||
|
||||
##### Chores
|
||||
|
||||
* incrementing version number - v2.8.3 (c20b20a7)
|
||||
* update changelog for v2.8.3 (eb2841ee)
|
||||
|
||||
#### v2.8.3 (2023-01-25)
|
||||
|
||||
##### Chores
|
||||
@@ -5,28 +157,6 @@
|
||||
* remove extraneous lines from changelog (48c9f447)
|
||||
* incrementing version number - v2.8.2 (050e43f8)
|
||||
* update changelog for v2.8.2 (66aa3169)
|
||||
* incrementing version number - v2.8.1 (727f879e)
|
||||
* incrementing version number - v2.8.0 (8e77673d)
|
||||
* incrementing version number - v2.7.0 (96cc0617)
|
||||
* incrementing version number - v2.6.1 (7e52a7a5)
|
||||
* incrementing version number - v2.6.0 (e7fcf482)
|
||||
* incrementing version number - v2.5.8 (dec0e7de)
|
||||
* incrementing version number - v2.5.7 (5836bf4a)
|
||||
* incrementing version number - v2.5.6 (c7bd7dbf)
|
||||
* incrementing version number - v2.5.5 (3509ed94)
|
||||
* incrementing version number - v2.5.4 (e83260ca)
|
||||
* incrementing version number - v2.5.3 (7e922936)
|
||||
* incrementing version number - v2.5.2 (babcd17e)
|
||||
* incrementing version number - v2.5.1 (ce3aa950)
|
||||
* incrementing version number - v2.5.0 (01d276cb)
|
||||
* incrementing version number - v2.4.5 (dd3e1a28)
|
||||
* incrementing version number - v2.4.4 (d5525c87)
|
||||
* incrementing version number - v2.4.3 (9c647c6c)
|
||||
* incrementing version number - v2.4.2 (3aa7b855)
|
||||
* incrementing version number - v2.4.1 (60cbd148)
|
||||
* incrementing version number - v2.4.0 (4834cde3)
|
||||
* incrementing version number - v2.3.1 (d2425942)
|
||||
* incrementing version number - v2.3.0 (046ea120)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "nodebb",
|
||||
"license": "GPL-3.0",
|
||||
"description": "NodeBB Forum",
|
||||
"version": "2.8.4",
|
||||
"version": "2.8.12",
|
||||
"homepage": "http://www.nodebb.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -53,9 +53,9 @@
|
||||
"connect-pg-simple": "8.0.0",
|
||||
"connect-redis": "6.1.3",
|
||||
"cookie-parser": "1.4.6",
|
||||
"cron": "2.1.0",
|
||||
"cron": "2.3.0",
|
||||
"cropperjs": "1.5.13",
|
||||
"csurf": "1.11.0",
|
||||
"csrf-sync": "4.0.0",
|
||||
"daemon": "1.1.0",
|
||||
"diff": "5.1.0",
|
||||
"esbuild": "0.16.10",
|
||||
@@ -90,7 +90,7 @@
|
||||
"@nodebb/bootswatch": "3.4.2",
|
||||
"nconf": "0.12.0",
|
||||
"nodebb-plugin-2factor": "5.1.2",
|
||||
"nodebb-plugin-composer-default": "9.2.4",
|
||||
"nodebb-plugin-composer-default": "9.2.5",
|
||||
"nodebb-plugin-dbsearch": "5.1.5",
|
||||
"nodebb-plugin-emoji": "4.0.6",
|
||||
"nodebb-plugin-emoji-android": "3.0.0",
|
||||
|
||||
19
loader.js
19
loader.js
@@ -30,9 +30,7 @@ const output = logrotate({ file: outputLogFilePath, size: '1m', keep: 3, compres
|
||||
const silent = nconf.get('silent') === 'false' ? false : nconf.get('silent') !== false;
|
||||
let numProcs;
|
||||
const workers = [];
|
||||
const Loader = {
|
||||
timesStarted: 0,
|
||||
};
|
||||
const Loader = {};
|
||||
const appPath = path.join(__dirname, 'app.js');
|
||||
|
||||
Loader.init = function () {
|
||||
@@ -57,21 +55,6 @@ Loader.displayStartupMessages = function () {
|
||||
|
||||
Loader.addWorkerEvents = function (worker) {
|
||||
worker.on('exit', (code, signal) => {
|
||||
if (code !== 0) {
|
||||
if (Loader.timesStarted < numProcs * 3) {
|
||||
Loader.timesStarted += 1;
|
||||
if (Loader.crashTimer) {
|
||||
clearTimeout(Loader.crashTimer);
|
||||
}
|
||||
Loader.crashTimer = setTimeout(() => {
|
||||
Loader.timesStarted = 0;
|
||||
}, 10000);
|
||||
} else {
|
||||
console.log(`${numProcs * 3} restarts in 10 seconds, most likely an error on startup. Halting.`);
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[cluster] Child Process (${worker.pid}) has exited (code: ${code}, signal: ${signal})`);
|
||||
if (!(worker.suicide || code === 0)) {
|
||||
console.log('[cluster] Spinning up another process...');
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "اسم مستخدم غير موجود",
|
||||
"no-teaser": "مقتطف غير موجود",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "لاتملك الصلاحيات اللازمة للقيام بهذه العملية",
|
||||
"category-disabled": "قائمة معطلة",
|
||||
"topic-locked": "الموضوع مقفول",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "لقد شاركت بالتصويت ، ألا تذكر؟",
|
||||
"reputation-system-disabled": "نظام السمعة معطل",
|
||||
"downvoting-disabled": "التصويتات السلبية معطلة",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Потребителят не съществува",
|
||||
"no-teaser": "Резюмето не съществува",
|
||||
"no-flag": "Докладът не съществува",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Нямате достатъчно права за това действие.",
|
||||
"category-disabled": "Категорията е изключена",
|
||||
"topic-locked": "Темата е заключена",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Това съобщение вече е изтрито.",
|
||||
"chat-restored-already": "Това съобщение вече е възстановено.",
|
||||
"chat-room-does-not-exist": "Стаята за разговори не съществува.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Вече сте дали глас за тази публикация.",
|
||||
"reputation-system-disabled": "Системата за репутация е изключена.",
|
||||
"downvoting-disabled": "Отрицателното гласуване е изключено",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "এই নামে কোন সদস্য নেই",
|
||||
"no-teaser": "টিজারটি খুজে পাওয়া যায় নি",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "এই কাজটির জন্য আপনার পর্যাপ্ত অধিকার নেই",
|
||||
"category-disabled": "বিভাগটি নিষ্ক্রিয়",
|
||||
"topic-locked": "টপিক বন্ধ",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "সম্মাননা ব্যাবস্থা নিস্ক্রীয় রাখা হয়েছে",
|
||||
"downvoting-disabled": "ঋণাত্মক ভোট নিস্ক্রীয় রাখা হয়েছে।",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Uživatel neexistuje",
|
||||
"no-teaser": "Chyták neexistuje",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Na tuto akci nemáte dostatečné oprávnění.",
|
||||
"category-disabled": "Kategorie zakázána",
|
||||
"topic-locked": "Téma uzamknuto",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Brugeren eksisterer ikke",
|
||||
"no-teaser": "Teaser eksisterer ikke",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Du har ikke nok rettigheder til at udføre denne handling",
|
||||
"category-disabled": "Kategorien er deaktiveret",
|
||||
"topic-locked": "Tråden er låst",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Der Benutzer existiert nicht",
|
||||
"no-teaser": "Zusammenfassung existiert nicht",
|
||||
"no-flag": "Markierung existiert nicht",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Du verfügst nicht über ausreichende Berechtigungen, um die Aktion durchzuführen.",
|
||||
"category-disabled": "Kategorie ist deaktiviert",
|
||||
"topic-locked": "Thema ist gesperrt",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Diese Chatnachricht wurde bereits gelöscht.",
|
||||
"chat-restored-already": "Diese Chatnachricht wurde bereits wiederhergestellt.",
|
||||
"chat-room-does-not-exist": "Der Chatraum existiert nicht.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Du hast diesen Beitrag bereits bewertet.",
|
||||
"reputation-system-disabled": "Das Reputationssystem ist deaktiviert.",
|
||||
"downvoting-disabled": "Downvotes sind deaktiviert.",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "User does not exist",
|
||||
"no-teaser": "Teaser does not exist",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "You do not have enough privileges for this action.",
|
||||
"category-disabled": "Η κατηγορία έχει απενεργοποιηθεί",
|
||||
"topic-locked": "Το θέμα έχει κλειδωθεί",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Το σύστημα φήμης έχει απενεργοποιηθεί.",
|
||||
"downvoting-disabled": "Η καταψήφιση έχει απενεργοποιηθεί",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"no-user": "User does not exist",
|
||||
"no-teaser": "Teaser does not exist",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "You do not have enough privileges for this action.",
|
||||
|
||||
"category-disabled": "Category disabled",
|
||||
@@ -182,6 +183,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Reputation system is disabled.",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "User does not exist",
|
||||
"no-teaser": "Teaser does not exist",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "You do not have enough privileges for this action.",
|
||||
"category-disabled": "Category disabled",
|
||||
"topic-locked": "Topic Locked",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Reputation system is disabled.",
|
||||
"downvoting-disabled": "Downvoting is disabled",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "User does not exist",
|
||||
"no-teaser": "Teaser does not exist",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "You do not have enough privileges for this action.",
|
||||
"category-disabled": "Category disabled",
|
||||
"topic-locked": "Topic Locked",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Reputation system is disabled.",
|
||||
"downvoting-disabled": "Downvoting is disabled",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "El usuario no existe",
|
||||
"no-teaser": "El resumen no existe",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "No tienes suficientes privilegios para realizar esta acción.",
|
||||
"category-disabled": "Categoría deshabilitada",
|
||||
"topic-locked": "Tema bloqueado",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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.",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Kasutajat ei eksisteeri",
|
||||
"no-teaser": "Eelvaadet ei eksisteeri",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Sul pole piisavalt õigusi.",
|
||||
"category-disabled": "Kategooria keelatud",
|
||||
"topic-locked": "Teema lukustatud",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "کاربر وجود ندارد",
|
||||
"no-teaser": "تیزر وجود ندارد",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "شما دسترسی کافی برای این کار را ندارید",
|
||||
"category-disabled": "دسته غیرفعال شد.",
|
||||
"topic-locked": "موضوع بسته شد.",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "این پیام قبلا حذف شده است",
|
||||
"chat-restored-already": "This chat message has already been restored.",
|
||||
"chat-room-does-not-exist": "Chat room does not exist.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "شما قبلا به این پست رای داده اید.",
|
||||
"reputation-system-disabled": "سیستم اعتبار غیر فعال شده است",
|
||||
"downvoting-disabled": "رأی منفی غیر فعال شده است",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Käyttäjää ei ole olemassa",
|
||||
"no-teaser": "Teaser does not exist",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Oikeutesi eivät riitä toiminnon suorittamiseen.",
|
||||
"category-disabled": "Kategoria ei ole käytössä",
|
||||
"topic-locked": "Aihe lukittu",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Reputation system is disabled.",
|
||||
"downvoting-disabled": "Downvoting is disabled",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Cet utilisateur n'existe pas",
|
||||
"no-teaser": "L’aperçu n'existe pas",
|
||||
"no-flag": "Le signalement n'existe pas",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Vous n'avez pas les privilèges nécessaires pour effectuer cette action.",
|
||||
"category-disabled": "Catégorie désactivée",
|
||||
"topic-locked": "Sujet verrouillé",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "O usuario non existe",
|
||||
"no-teaser": "A vista previa do tema non existe",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Non tes privilexios dabondo para ver este tema.",
|
||||
"category-disabled": "Categoría deshabilitada",
|
||||
"topic-locked": "Tema Pechado",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "משתמש אינו קיים",
|
||||
"no-teaser": "תקציר אינו קיים",
|
||||
"no-flag": "דיווח לא קיים",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "ההרשאות שלכם אינן מספיקות לביצוע פעולה זו.",
|
||||
"category-disabled": "קטגוריה לא פעילה",
|
||||
"topic-locked": "נושא נעול",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "הודעת צ'אט זו כבר נמחקה.",
|
||||
"chat-restored-already": "הודעת צ'אט זו כבר שוחזרה.",
|
||||
"chat-room-does-not-exist": "חדר צ'אט אינו קיים.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "הצבעתם כבר בנושא זה.",
|
||||
"reputation-system-disabled": "מערכת המוניטין לא פעילה.",
|
||||
"downvoting-disabled": "היכולת להצביע נגד מושבתת",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Korisnik ne postoji",
|
||||
"no-teaser": "Zadirkivač ne postoji",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Nemate privilegije za ovu radnju.",
|
||||
"category-disabled": "Kategorija onemogućena",
|
||||
"topic-locked": "Tema zaključana",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Nem létező felhasználó",
|
||||
"no-teaser": "A bevezető nem létezik",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Nincs elég jogod ehhez a művelethez.",
|
||||
"category-disabled": "Kategória kikapcsolva",
|
||||
"topic-locked": "Téma lezárva",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Ez az üzenet már törölve lett.",
|
||||
"chat-restored-already": "Ez az üzenet már vissza van állítva.",
|
||||
"chat-room-does-not-exist": "Csevegő szoba nem létezik.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Már szavaztál erre a hozzászólásra.",
|
||||
"reputation-system-disabled": "Hírnév funkció kikapcsolva.",
|
||||
"downvoting-disabled": "Leszavazás funkció kikapcsolva",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Օգտվողը գոյություն չունի",
|
||||
"no-teaser": "Թիզերը գոյություն չունի",
|
||||
"no-flag": "Դրոշ գոյություն չունի",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Դուք չունեք բավարար արտոնություններ այս գործողության համար:",
|
||||
"category-disabled": "Կատեգորիան անջատված է",
|
||||
"topic-locked": "Թեման փակված է",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Այս զրույցի հաղորդագրությունն արդեն ջնջված է",
|
||||
"chat-restored-already": "This chat message has already been restored.",
|
||||
"chat-room-does-not-exist": "Այս զրուցարանը գոյություն չունի:",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Դուք արդեն քվեարկել եք այս գրառման օգտին:",
|
||||
"reputation-system-disabled": "Վարկանիշի համակարգը անջատված է:",
|
||||
"downvoting-disabled": "Դեմ քվեարկությունն անջատված է",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Pengguna tidak ditemukan",
|
||||
"no-teaser": "Teaser tidak ditemukan",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Kamu tidak punya cukup izin untuk melakukan ini",
|
||||
"category-disabled": "Kategori ditiadakan",
|
||||
"topic-locked": "Topik dikunci",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Sistem reputasi ditiadakan.",
|
||||
"downvoting-disabled": "Downvoting ditiadakan",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "L'Utente non esiste",
|
||||
"no-teaser": "Teaser non esiste",
|
||||
"no-flag": "Segnalazione non esiste",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Non hai abbastanza privilegi per questa azione.",
|
||||
"category-disabled": "Categoria disabilitata",
|
||||
"topic-locked": "Discussione Bloccata",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Hai già votato per questo post",
|
||||
"reputation-system-disabled": "Il sistema di reputazione è disabilitato.",
|
||||
"downvoting-disabled": "Votata negativamente è disabilitato",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "ユーザーは存在しません",
|
||||
"no-teaser": "ティーザーが存在しません",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "あなたがこの行為する権利がありません。",
|
||||
"category-disabled": "この板は無効された",
|
||||
"topic-locked": "スレッドがロックされた",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "このチャットメッセージは既に削除されています",
|
||||
"chat-restored-already": "このチャットメッセージは既に削除されています",
|
||||
"chat-room-does-not-exist": "Chat room does not exist.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "あなたはすでにこの投稿を評価しました。",
|
||||
"reputation-system-disabled": "Reputation system is disabled.",
|
||||
"downvoting-disabled": "Downvoting is disabled",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "존재하지 않는 사용자입니다.",
|
||||
"no-teaser": "존재하지 않는 미리보기입니다.",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "이 작업을 할 수 있는 권한이 없습니다.",
|
||||
"category-disabled": "카테고리가 비활성화 되었습니다.",
|
||||
"topic-locked": "게시물이 잠금 상태입니다.",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "이미 삭제된 채팅 메시지입니다.",
|
||||
"chat-restored-already": "이 채팅 메시지는 이미 복원되었습니다.",
|
||||
"chat-room-does-not-exist": "채팅이 존재하지 않습니다.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "이미 이 포스트에 투표하셨습니다.",
|
||||
"reputation-system-disabled": "인지도 시스템이 비활성화되어있습니다.",
|
||||
"downvoting-disabled": "비추천 기능이 비활성 상태입니다.",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Tokio vartotojo nėra",
|
||||
"no-teaser": "Anonsas neegzistuoja",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Šiam veiksmui jūs neturite pakankamų privilegijų.",
|
||||
"category-disabled": "Kategorija išjungta",
|
||||
"topic-locked": "Tema užrakinta",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Lietotājs nav atrasts",
|
||||
"no-teaser": "Ievadapraksts nav atrasts",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Tev nepietiek tiesības šai darbībai.",
|
||||
"category-disabled": "Kategorija ir atspējota",
|
||||
"topic-locked": "Temats ir slēgts",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Pengguna tidak wujud",
|
||||
"no-teaser": "Pengusik tidak wujud",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Anda tidak mempunyai cukup keistimewaan untuk perbuatan ini.",
|
||||
"category-disabled": "Kategori dilumpuhkan",
|
||||
"topic-locked": "Topik Dikunci",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Sistem reputasi dilumpuhkan.",
|
||||
"downvoting-disabled": "Undi turun dilumpuhkan",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Bruker eksisterer ikke",
|
||||
"no-teaser": "Teaseren eksisterer ikke",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Du har ikke nok rettigheter til å utføre denne handlingen.",
|
||||
"category-disabled": "Kategori deaktivert",
|
||||
"topic-locked": "Emne låst",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Denne meldingen har allerede blitt slettet.",
|
||||
"chat-restored-already": "Denne meldingen har allerede blitt gjenopprettet.",
|
||||
"chat-room-does-not-exist": "Dette chatterommet finnes ikke.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Du har allerede stemt på dette innlegget",
|
||||
"reputation-system-disabled": "Omdømmesystemet er deaktivert.",
|
||||
"downvoting-disabled": "Nedstemming er deaktivert",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Gebruiker bestaat niet",
|
||||
"no-teaser": "Dit voorproefje bestaat niet",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Onvoldoende rechten om deze actie uit te voeren",
|
||||
"category-disabled": "Categorie uitgeschakeld",
|
||||
"topic-locked": "Onderwerp gesloten",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Użytkownik nie istnieje",
|
||||
"no-teaser": "Zwiastun nie istnieje",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Nie masz przywileju wykonywania tej akcji",
|
||||
"category-disabled": "Kategoria wyłączona.",
|
||||
"topic-locked": "Temat zablokowany",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "O usuário não existe",
|
||||
"no-teaser": "O teaser não existe",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Você não possui privilégios suficientes para esta ação.",
|
||||
"category-disabled": "Categoria desativada",
|
||||
"topic-locked": "Tópico Trancado",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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": "A sala de chat não existe.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Utilizador não existente",
|
||||
"no-teaser": "Não existe pré-visualização",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Não possuis privilégios suficientes para esta ação.",
|
||||
"category-disabled": "Categoria desativada",
|
||||
"topic-locked": "Tópico bloqueado",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Utilizatorul nu exista.",
|
||||
"no-teaser": "Teaser does not exist",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "You do not have enough privileges for this action.",
|
||||
"category-disabled": "Categorie dezactivată",
|
||||
"topic-locked": "Subiect Închis",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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ă",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Такого пользователя не существует",
|
||||
"no-teaser": "Такого тизера не существует",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "У вас недостаточно прав для этого действия.",
|
||||
"category-disabled": "Категория отключена",
|
||||
"topic-locked": "Тема закрыта",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Это сообщение чата уже удалено.",
|
||||
"chat-restored-already": "Это сообщение чата уже было восстановлено.",
|
||||
"chat-room-does-not-exist": "Комната чата не существует.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Вы уже проголосовали за это сообщение.",
|
||||
"reputation-system-disabled": "Система репутации отключена.",
|
||||
"downvoting-disabled": "Понижение рейтинга отключено",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Umuntu utabaho",
|
||||
"no-teaser": "Inshamake itabaho",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Ntabwo uragira uburenganzira buhagije ngo wemererwe iki gikorwa",
|
||||
"category-disabled": "Icyiciro cyabujijwe",
|
||||
"topic-locked": "Ikiganiro Cyafungiranywe",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "User does not exist",
|
||||
"no-teaser": "Teaser does not exist",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "You do not have enough privileges for this action.",
|
||||
"category-disabled": "Category disabled",
|
||||
"topic-locked": "Topic Locked",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "You have already voted for this post.",
|
||||
"reputation-system-disabled": "Reputation system is disabled.",
|
||||
"downvoting-disabled": "Downvoting is disabled",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Užívateľ neexistuje",
|
||||
"no-teaser": "Ukážka neexistuje",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Na túto akciu nemáte dostatočné oprávnenia.",
|
||||
"category-disabled": "Kategória je zablokovaná",
|
||||
"topic-locked": "Téma je uzamknutá",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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é",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Uporabnik ne obstaja.",
|
||||
"no-teaser": "Predogled ne obstaja.",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Nimate dovolj pravic za to dejanje.",
|
||||
"category-disabled": "Kategorija je onemogočena.",
|
||||
"topic-locked": "Tema je zaklenjena.",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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.",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Përdoruesi nuk ekziston",
|
||||
"no-teaser": "Përmbledhja nuk ekziston",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Nuk keni akses të mjaftueshem për këtë veprim.",
|
||||
"category-disabled": "Kategori e çaktivizuar",
|
||||
"topic-locked": "Temë e kyçur",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Ky mesazh është fshirë tashmë.",
|
||||
"chat-restored-already": "Ky mesazh është rikthyer tashmë.",
|
||||
"chat-room-does-not-exist": "Kjo dhomë bisede nuk ekziston.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Ju keni votuar tashmë për këtë postim.",
|
||||
"reputation-system-disabled": "Sistemi i reputacionit është i çaktivizuar.",
|
||||
"downvoting-disabled": "Votimi kundër është i çaktivizuar",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Корисник не постоји",
|
||||
"no-teaser": "Исечак не постоји",
|
||||
"no-flag": "Заставица не постоји",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Немате довољне привилегије за обављање ове радње.",
|
||||
"category-disabled": "Категорија је онемогућена",
|
||||
"topic-locked": "Тема је закључана",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Ова порука ћаскања је већ избрисана.",
|
||||
"chat-restored-already": "Ова порука ћаскања је већ обновљена.",
|
||||
"chat-room-does-not-exist": "Соба за ћаскање не постоји.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Већ сте гласали за ову поруку.",
|
||||
"reputation-system-disabled": "Угледи су онемогућени.",
|
||||
"downvoting-disabled": "Негативно гласање је онемогућено",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Användaren finns inte",
|
||||
"no-teaser": "Förhandsvisningen finns inte",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Du har inte tillräckliga rättigheter för den här åtgärden.",
|
||||
"category-disabled": "Kategorin inaktiverad",
|
||||
"topic-locked": "Ämnet låst",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"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",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "ยังไม่มีผู้ใช้งานนี้",
|
||||
"no-teaser": "ยังไม่มีทีเซอร์นี้",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "คุณมีสิทธิ์ไม่เพียงพอที่จะทำรายการนี้",
|
||||
"category-disabled": "Category นี้ถูกปิดการใช้งานแล้ว",
|
||||
"topic-locked": "กระทู้ถูกล็อก",
|
||||
@@ -156,6 +157,9 @@
|
||||
"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.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "คุณได้โหวตโพสต์นี้แล้ว",
|
||||
"reputation-system-disabled": "ระบบชื่อเสียงถูกปิดใช้งาน",
|
||||
"downvoting-disabled": "\"การโหวตลง\" ถูกปิดใช้งาน",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Kullanıcı Yok",
|
||||
"no-teaser": "İleti Yok",
|
||||
"no-flag": "Şikayet Yok",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Bu işlemi yapmak için yeterli yetkiniz yok.",
|
||||
"category-disabled": "Kategori aktif değil",
|
||||
"topic-locked": "Başlık Kilitli",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Bu sohbet mesajı zaten silinmiş.",
|
||||
"chat-restored-already": "Bu sohbet mesajı zaten geri yüklendi.",
|
||||
"chat-room-does-not-exist": "Sohbet Odası Mevcut Değil",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Bu gönderi için zaten oy verdin.",
|
||||
"reputation-system-disabled": "İtibar sistemi devre dışı.",
|
||||
"downvoting-disabled": "Eksi oylama devre dışı bırakılmış. ",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Користувач не існує",
|
||||
"no-teaser": "Тизер не існує",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "У вас недостатньо повноважень для цієї дії. ",
|
||||
"category-disabled": "Категорію відключено",
|
||||
"topic-locked": "Тему заблоковано",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Це повідомлення чату вже було видалено.",
|
||||
"chat-restored-already": "Це чат повідомлення вже було відновлене",
|
||||
"chat-room-does-not-exist": "Chat room does not exist.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Ви вже проголосували за цей пост.",
|
||||
"reputation-system-disabled": "Система репутацій вимкнена.",
|
||||
"downvoting-disabled": "Голосування проти вимкнено",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "Người dùng không tồn tại",
|
||||
"no-teaser": "Đoạn giới thiệu không tồn tại",
|
||||
"no-flag": "Cờ không tồn tại",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "Bạn không đủ quyền để thực thi hành động này",
|
||||
"category-disabled": "Chuyên mục bị khóa",
|
||||
"topic-locked": "Chủ đề bị khóa",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "Cuộc trò chuyện này đã được xóa.",
|
||||
"chat-restored-already": "Tin nhắn trò chuyện này đã được khôi phục.",
|
||||
"chat-room-does-not-exist": "Phòng trò chuyện không tồn tại.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "Bạn đã bỏ phiếu cho bài viết này",
|
||||
"reputation-system-disabled": "Hệ thống đánh giá uy tính đã bị vô hiệu hóa.",
|
||||
"downvoting-disabled": "Phản đối đã bị tắt",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "用户不存在",
|
||||
"no-teaser": "主题预览不存在",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "您没有权限执行此操作。",
|
||||
"category-disabled": "版块已禁用",
|
||||
"topic-locked": "主题已锁定",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "聊天消息已经被删除",
|
||||
"chat-restored-already": "此聊天消息已经恢复。\n",
|
||||
"chat-room-does-not-exist": "聊天室不存在。",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "您已为此帖回复投过票了。",
|
||||
"reputation-system-disabled": "声望系统已禁用。",
|
||||
"downvoting-disabled": "踩已被禁用",
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
"no-user": "使用者不存在",
|
||||
"no-teaser": "主題預覽不存在",
|
||||
"no-flag": "Flag does not exist",
|
||||
"no-chat-room": "Chat room does not exist",
|
||||
"no-privileges": "您的權限不足以執行此操作。",
|
||||
"category-disabled": "版面已停用",
|
||||
"topic-locked": "主題已鎖定",
|
||||
@@ -156,6 +157,9 @@
|
||||
"chat-deleted-already": "聊天訊息已經被刪除",
|
||||
"chat-restored-already": "此聊天訊息已經恢復。",
|
||||
"chat-room-does-not-exist": "Chat room does not exist.",
|
||||
"cant-add-users-to-chat-room": "Can't add users to chat room.",
|
||||
"cant-remove-users-from-chat-room": "Can't remove users from chat room.",
|
||||
"chat-room-name-too-long": "Chat room name too long.",
|
||||
"already-voting-for-this-post": "您已讚過此貼文回覆了。",
|
||||
"reputation-system-disabled": "聲望系統已停用。",
|
||||
"downvoting-disabled": "倒讚已被停用",
|
||||
|
||||
@@ -265,6 +265,9 @@ TopicObjectSlim:
|
||||
name:
|
||||
type: string
|
||||
description: The topic thumbnail filename
|
||||
path:
|
||||
type: string
|
||||
description: Path to topic thumbnail without upload_url prefix
|
||||
url:
|
||||
type: string
|
||||
description: Relative path to the topic thumbnail
|
||||
|
||||
@@ -19,6 +19,15 @@ post:
|
||||
content:
|
||||
type: string
|
||||
example: This is the test topic's content
|
||||
timestamp:
|
||||
type: number
|
||||
description: |
|
||||
A UNIX timestamp of the topic's creation date (i.e. when it will be posted).
|
||||
Specifically, this value can only be set to a value in the future if the calling user has the `topics:schedule` privilege for the passed-in category.
|
||||
Otherwise, the current date and time are always assumed.
|
||||
In some scenarios (e.g. forum migrations), you may want to backdate topics and posts.
|
||||
Please see [this Developer FAQ topic](https://community.nodebb.org/topic/16983/how-can-i-backdate-topics-and-posts-for-migration-purposes) for more information.
|
||||
example: 556084800000
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -46,8 +46,6 @@ post:
|
||||
content:
|
||||
type: string
|
||||
example: This is a test reply
|
||||
timestamp:
|
||||
type: number
|
||||
toPid:
|
||||
type: number
|
||||
required:
|
||||
|
||||
@@ -31,6 +31,8 @@ get:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
description: Path to a topic thumbnail
|
||||
@@ -155,6 +157,8 @@ delete:
|
||||
type: string
|
||||
name:
|
||||
type: string
|
||||
path:
|
||||
type: string
|
||||
url:
|
||||
type: string
|
||||
description: Path to a topic thumbnail
|
||||
@@ -38,4 +38,4 @@ put:
|
||||
$ref: ../../../../components/schemas/Status.yaml#/Status
|
||||
response:
|
||||
type: object
|
||||
properties: {}
|
||||
properties: {}
|
||||
|
||||
@@ -9,15 +9,23 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
|
||||
configureEmailTester();
|
||||
configureEmailEditor();
|
||||
handleDigestHourChange();
|
||||
handleSmtpServiceChange();
|
||||
|
||||
$(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange);
|
||||
$(window).on('action:admin.settingsSaved', function () {
|
||||
socket.emit('admin.user.restartJobs');
|
||||
});
|
||||
$('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange);
|
||||
$(window).off('action:admin.settingsLoaded', onSettingsLoaded)
|
||||
.on('action:admin.settingsLoaded', onSettingsLoaded);
|
||||
$(window).off('action:admin.settingsSaved', onSettingsSaved)
|
||||
.on('action:admin.settingsSaved', onSettingsSaved);
|
||||
};
|
||||
|
||||
function onSettingsLoaded() {
|
||||
handleDigestHourChange();
|
||||
handleSmtpServiceChange();
|
||||
}
|
||||
|
||||
function onSettingsSaved() {
|
||||
handleDigestHourChange();
|
||||
socket.emit('admin.user.restartJobs');
|
||||
}
|
||||
|
||||
function configureEmailTester() {
|
||||
$('button[data-action="email.test"]').off('click').on('click', function () {
|
||||
socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) {
|
||||
@@ -106,20 +114,26 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
|
||||
}
|
||||
|
||||
function handleSmtpServiceChange() {
|
||||
const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
|
||||
$('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
|
||||
|
||||
const enabledEl = document.getElementById('email:smtpTransport:enabled');
|
||||
if (enabledEl) {
|
||||
if (!enabledEl.checked) {
|
||||
enabledEl.closest('label').classList.toggle('is-checked', true);
|
||||
enabledEl.checked = true;
|
||||
alerts.alert({
|
||||
message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
function toggleCustomService() {
|
||||
const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
|
||||
$('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
|
||||
}
|
||||
toggleCustomService();
|
||||
$('[id="email:smtpTransport:service"]').change(function () {
|
||||
toggleCustomService();
|
||||
|
||||
const enabledEl = document.getElementById('email:smtpTransport:enabled');
|
||||
if (enabledEl) {
|
||||
if (!enabledEl.checked) {
|
||||
$('label[for="email:smtpTransport:enabled"]').toggleClass('is-checked', true);
|
||||
enabledEl.checked = true;
|
||||
alerts.alert({
|
||||
message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
|
||||
timeout: 5000,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return module;
|
||||
|
||||
@@ -77,6 +77,7 @@ define('forum/account/edit/password', [
|
||||
ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
|
||||
}
|
||||
})
|
||||
.catch(alerts.error)
|
||||
.finally(() => {
|
||||
btn.removeClass('disabled').find('i').addClass('hide');
|
||||
currentPassword.val('');
|
||||
|
||||
@@ -88,7 +88,11 @@ define('forum/topic', [
|
||||
});
|
||||
}
|
||||
|
||||
mousetrap.bind('j', () => {
|
||||
mousetrap.bind('j', (e) => {
|
||||
if (e.target.classList.contains('mousetrap')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = navigator.getIndex();
|
||||
const count = navigator.getCount();
|
||||
if (index === count) {
|
||||
@@ -98,7 +102,11 @@ define('forum/topic', [
|
||||
navigator.scrollToIndex(index, true, 0);
|
||||
});
|
||||
|
||||
mousetrap.bind('k', () => {
|
||||
mousetrap.bind('k', (e) => {
|
||||
if (e.target.classList.contains('mousetrap')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = navigator.getIndex();
|
||||
if (index === 1) {
|
||||
return;
|
||||
|
||||
@@ -41,7 +41,7 @@ define('forum/topic/postTools', [
|
||||
const pid = postEl.attr('data-pid');
|
||||
const index = parseInt(postEl.attr('data-index'), 10);
|
||||
|
||||
socket.emit('posts.loadPostTools', { pid: pid, cid: ajaxify.data.cid }, async (err, data) => {
|
||||
socket.emit('posts.loadPostTools', { pid: pid }, async (err, data) => {
|
||||
if (err) {
|
||||
return alerts.error(err);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ app = window.app || {};
|
||||
reconnectionDelay: config.reconnectionDelay,
|
||||
transports: config.socketioTransports,
|
||||
path: config.relative_path + '/socket.io',
|
||||
query: {
|
||||
_csrf: config.csrf_token,
|
||||
},
|
||||
};
|
||||
|
||||
window.socket = io(config.websocketAddress, ioParams);
|
||||
|
||||
@@ -443,6 +443,10 @@ usersAPI.changePicture = async (caller, data) => {
|
||||
};
|
||||
|
||||
usersAPI.generateExport = async (caller, { uid, type }) => {
|
||||
const validTypes = ['profile', 'posts', 'uploads'];
|
||||
if (!validTypes.includes(type)) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
const count = await db.incrObjectField('locks', `export:${uid}${type}`);
|
||||
if (count > 1) {
|
||||
throw new Error('[[error:already-exporting]]');
|
||||
|
||||
@@ -95,11 +95,9 @@ module.exports = function (Categories) {
|
||||
await privileges.categories.give(result.modPrivileges, category.cid, ['administrators', 'Global Moderators']);
|
||||
await privileges.categories.give(result.guestPrivileges, category.cid, ['guests', 'spiders']);
|
||||
|
||||
cache.del([
|
||||
'categories:cid',
|
||||
`cid:${parentCid}:children`,
|
||||
`cid:${parentCid}:children:all`,
|
||||
]);
|
||||
cache.del('categories:cid');
|
||||
await clearParentCategoryCache(parentCid);
|
||||
|
||||
if (data.cloneFromCid && parseInt(data.cloneFromCid, 10)) {
|
||||
category = await Categories.copySettingsFrom(data.cloneFromCid, category.cid, !data.parentCid);
|
||||
}
|
||||
@@ -112,6 +110,22 @@ module.exports = function (Categories) {
|
||||
return category;
|
||||
};
|
||||
|
||||
async function clearParentCategoryCache(parentCid) {
|
||||
while (parseInt(parentCid, 10) >= 0) {
|
||||
cache.del([
|
||||
`cid:${parentCid}:children`,
|
||||
`cid:${parentCid}:children:all`,
|
||||
]);
|
||||
|
||||
if (parseInt(parentCid, 10) === 0) {
|
||||
return;
|
||||
}
|
||||
// clear all the way to root
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
parentCid = await Categories.getCategoryField(parentCid, 'parentCid');
|
||||
}
|
||||
}
|
||||
|
||||
async function duplicateCategoriesChildren(parentCid, cid, uid) {
|
||||
let children = await Categories.getChildren([cid], uid);
|
||||
if (!children.length) {
|
||||
|
||||
@@ -5,7 +5,6 @@ const _ = require('lodash');
|
||||
|
||||
const db = require('../database');
|
||||
const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
const plugins = require('../plugins');
|
||||
const privileges = require('../privileges');
|
||||
const cache = require('../cache');
|
||||
@@ -99,39 +98,7 @@ Categories.getModerators = async function (cid) {
|
||||
};
|
||||
|
||||
Categories.getModeratorUids = async function (cids) {
|
||||
// Only check active categories
|
||||
const disabled = (await Categories.getCategoriesFields(cids, ['disabled'])).map(obj => obj.disabled);
|
||||
// cids = cids.filter((_, idx) => !disabled[idx]);
|
||||
|
||||
const groupNames = cids.reduce((memo, cid) => {
|
||||
memo.push(`cid:${cid}:privileges:moderate`);
|
||||
memo.push(`cid:${cid}:privileges:groups:moderate`);
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
const memberSets = await groups.getMembersOfGroups(groupNames);
|
||||
// Every other set is actually a list of user groups, not uids, so convert those to members
|
||||
const sets = memberSets.reduce((memo, set, idx) => {
|
||||
if (idx % 2) {
|
||||
memo.groupNames.push(set);
|
||||
} else {
|
||||
memo.uids.push(set);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, { groupNames: [], uids: [] });
|
||||
|
||||
const uniqGroups = _.uniq(_.flatten(sets.groupNames));
|
||||
const groupUids = await groups.getMembersOfGroups(uniqGroups);
|
||||
const map = _.zipObject(uniqGroups, groupUids);
|
||||
const moderatorUids = cids.map((cid, index) => {
|
||||
if (disabled[index]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g]))));
|
||||
});
|
||||
return moderatorUids;
|
||||
return await privileges.categories.getUidsWithPrivilege(cids, 'moderate');
|
||||
};
|
||||
|
||||
Categories.getCategories = async function (cids, uid) {
|
||||
|
||||
@@ -32,12 +32,6 @@ try {
|
||||
if (!semver.satisfies(version, defaultPackage.dependencies[packageName])) {
|
||||
const e = new TypeError(`Incorrect dependency version: ${packageName}`);
|
||||
e.code = 'DEP_WRONG_VERSION';
|
||||
// delete the module from require cache so it doesn't break rest of the upgrade
|
||||
// https://github.com/NodeBB/NodeBB/issues/11173
|
||||
const resolvedModule = require.resolve(packageName);
|
||||
if (require.cache[resolvedModule]) {
|
||||
delete require.cache[resolvedModule];
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -57,6 +51,16 @@ try {
|
||||
packageInstall.preserveExtraneousPlugins();
|
||||
packageInstall.installAll();
|
||||
|
||||
// delete the module from require cache so it doesn't break rest of the upgrade
|
||||
// https://github.com/NodeBB/NodeBB/issues/11173
|
||||
const packages = ['nconf', 'async', 'commander', 'chalk', 'lodash', 'lru-cache'];
|
||||
packages.forEach((packageName) => {
|
||||
const resolvedModule = require.resolve(packageName);
|
||||
if (require.cache[resolvedModule]) {
|
||||
delete require.cache[resolvedModule];
|
||||
}
|
||||
});
|
||||
|
||||
const chalk = require('chalk');
|
||||
console.log(`${chalk.green('OK')}\n`);
|
||||
} else {
|
||||
|
||||
@@ -45,10 +45,11 @@ notificationsController.get = async function (req, res, next) {
|
||||
{ separator: true },
|
||||
]).concat(filters.moderatorFilters);
|
||||
}
|
||||
const selectedFilter = allFilters.find((filterData) => {
|
||||
|
||||
allFilters.forEach((filterData) => {
|
||||
filterData.selected = filterData.filter === filter;
|
||||
return filterData.selected;
|
||||
});
|
||||
const selectedFilter = allFilters.find(filterData => filterData.selected);
|
||||
if (!selectedFilter) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const categories = require('../categories');
|
||||
const plugins = require('../plugins');
|
||||
const translator = require('../translator');
|
||||
const languages = require('../languages');
|
||||
const { generateToken } = require('../middleware/csrf');
|
||||
|
||||
const apiController = module.exports;
|
||||
|
||||
@@ -64,7 +65,7 @@ apiController.loadConfig = async function (req) {
|
||||
'cache-buster': meta.config['cache-buster'] || '',
|
||||
topicPostSort: meta.config.topicPostSort || 'oldest_to_newest',
|
||||
categoryTopicSort: meta.config.categoryTopicSort || 'newest_to_oldest',
|
||||
csrf_token: req.uid >= 0 && req.csrfToken && req.csrfToken(),
|
||||
csrf_token: req.uid >= 0 ? generateToken(req) : undefined,
|
||||
searchEnabled: plugins.hooks.hasListeners('filter:search.query'),
|
||||
searchDefaultInQuick: meta.config.searchDefaultInQuick || 'titles',
|
||||
bootswatchSkin: meta.config.bootswatchSkin || '',
|
||||
|
||||
@@ -339,7 +339,7 @@ authenticationController.doLogin = async function (req, uid) {
|
||||
return;
|
||||
}
|
||||
const loginAsync = util.promisify(req.login).bind(req);
|
||||
await loginAsync({ uid: uid }, { keepSessionInfo: req.res.locals !== false });
|
||||
await loginAsync({ uid: uid }, { keepSessionInfo: req.res.locals.reroll !== false });
|
||||
await authenticationController.onSuccessfulLogin(req, uid);
|
||||
};
|
||||
|
||||
@@ -383,7 +383,7 @@ authenticationController.onSuccessfulLogin = async function (req, uid) {
|
||||
}),
|
||||
user.auth.addSession(uid, req.sessionID),
|
||||
user.updateLastOnlineTime(uid),
|
||||
user.updateOnlineUsers(uid),
|
||||
user.onUserOnline(uid, Date.now()),
|
||||
analytics.increment('logins'),
|
||||
db.incrObjectFieldBy('global', 'loginCount', 1),
|
||||
]);
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
|
||||
const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
const posts = require('../posts');
|
||||
const flags = require('../flags');
|
||||
const analytics = require('../analytics');
|
||||
@@ -110,7 +113,6 @@ modsController.flags.detail = async function (req, res, next) {
|
||||
isAdminOrGlobalMod: user.isAdminOrGlobalMod(req.uid),
|
||||
moderatedCids: user.getModeratedCids(req.uid),
|
||||
flagData: flags.get(req.params.flagId),
|
||||
assignees: user.getAdminsandGlobalModsandModerators(),
|
||||
privileges: Promise.all(['global', 'admin'].map(async type => privileges[type].get(req.uid))),
|
||||
});
|
||||
results.privileges = { ...results.privileges[0], ...results.privileges[1] };
|
||||
@@ -119,6 +121,28 @@ modsController.flags.detail = async function (req, res, next) {
|
||||
return next(); // 404
|
||||
}
|
||||
|
||||
async function getAssignees(flagData) {
|
||||
let uids = [];
|
||||
const [admins, globalMods] = await Promise.all([
|
||||
groups.getMembers('administrators', 0, -1),
|
||||
groups.getMembers('Global Moderators', 0, -1),
|
||||
]);
|
||||
if (flagData.type === 'user') {
|
||||
uids = await privileges.admin.getUidsWithPrivilege('admin:users');
|
||||
uids = _.uniq(admins.concat(uids));
|
||||
} else if (flagData.type === 'post') {
|
||||
const cid = await posts.getCidByPid(flagData.targetId);
|
||||
uids = _.uniq(admins.concat(globalMods));
|
||||
if (cid) {
|
||||
const modUids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0];
|
||||
uids = _.uniq(uids.concat(modUids));
|
||||
}
|
||||
}
|
||||
const userData = await user.getUsersData(uids);
|
||||
return userData.filter(u => u && u.userslug);
|
||||
}
|
||||
|
||||
const assignees = await getAssignees(results.flagData);
|
||||
results.flagData.history = results.isAdminOrGlobalMod ? (await flags.getHistory(req.params.flagId)) : null;
|
||||
|
||||
if (results.flagData.type === 'user') {
|
||||
@@ -128,7 +152,7 @@ modsController.flags.detail = async function (req, res, next) {
|
||||
}
|
||||
|
||||
res.render('flags/detail', Object.assign(results.flagData, {
|
||||
assignees: results.assignees,
|
||||
assignees: assignees,
|
||||
type_bool: ['post', 'user', 'empty'].reduce((memo, cur) => {
|
||||
if (cur !== 'empty') {
|
||||
memo[cur] = results.flagData.type === cur && (
|
||||
|
||||
@@ -26,7 +26,7 @@ module.exports = function (module) {
|
||||
|
||||
async function getSortedSetUnion(params) {
|
||||
if (!Array.isArray(params.sets) || !params.sets.length) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
let limit = params.stop - params.start + 1;
|
||||
if (limit <= 0) {
|
||||
|
||||
@@ -32,6 +32,9 @@ SELECT COUNT(DISTINCT z."value") c
|
||||
|
||||
async function getSortedSetUnion(params) {
|
||||
const { sets } = params;
|
||||
if (!sets || !sets.length) {
|
||||
return [];
|
||||
}
|
||||
const start = params.hasOwnProperty('start') ? params.start : 0;
|
||||
const stop = params.hasOwnProperty('stop') ? params.stop : -1;
|
||||
let weights = params.weights || [];
|
||||
|
||||
@@ -40,14 +40,24 @@ async function linkModules() {
|
||||
await Promise.all(Object.keys(modules).map(async (relPath) => {
|
||||
const srcPath = path.join(__dirname, '../../', modules[relPath]);
|
||||
const destPath = path.join(__dirname, '../../build/public/src/modules', relPath);
|
||||
const destDir = path.dirname(destPath);
|
||||
|
||||
const [stats] = await Promise.all([
|
||||
fs.promises.stat(srcPath),
|
||||
mkdirp(path.dirname(destPath)),
|
||||
mkdirp(destDir),
|
||||
]);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
await file.linkDirs(srcPath, destPath, true);
|
||||
} else {
|
||||
await fs.promises.copyFile(srcPath, destPath);
|
||||
// Get the relative path to the destination directory
|
||||
const relPath = path.relative(destDir, srcPath)
|
||||
// and convert to a posix path
|
||||
.split(path.sep).join(path.posix.sep);
|
||||
|
||||
// Instead of copying file, create a new file re-exporting it
|
||||
// This way, imports in modules are resolved correctly
|
||||
await fs.promises.writeFile(destPath, `module.exports = require('${relPath}');`);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -108,7 +108,7 @@ Themes.set = async (data) => {
|
||||
await db.sortedSetAdd('plugins:active', numPlugins, data.id);
|
||||
} else if (!activePluginsConfig.includes(data.id)) {
|
||||
// This prevents changing theme when configuration doesn't include it, but allows it otherwise
|
||||
winston.error('When defining active plugins in configuration, changing themes requires adding the new theme to the list of active plugins before updating it in the ACP');
|
||||
winston.error(`When defining active plugins in configuration, changing themes requires adding the theme '${data.id}' to the list of active plugins before updating it in the ACP`);
|
||||
throw new Error('[[error:theme-not-set-in-configuration]]');
|
||||
}
|
||||
|
||||
|
||||
26
src/middleware/csrf.js
Normal file
26
src/middleware/csrf.js
Normal file
@@ -0,0 +1,26 @@
|
||||
'use strict';
|
||||
|
||||
const { csrfSync } = require('csrf-sync');
|
||||
|
||||
const {
|
||||
generateToken,
|
||||
csrfSynchronisedProtection,
|
||||
isRequestValid,
|
||||
} = csrfSync({
|
||||
getTokenFromRequest: (req) => {
|
||||
if (req.headers['x-csrf-token']) {
|
||||
return req.headers['x-csrf-token'];
|
||||
} else if (req.body && req.body.csrf_token) {
|
||||
return req.body.csrf_token;
|
||||
} else if (req.query) {
|
||||
return req.query._csrf;
|
||||
}
|
||||
},
|
||||
size: 64,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
generateToken,
|
||||
csrfSynchronisedProtection,
|
||||
isRequestValid,
|
||||
};
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
const async = require('async');
|
||||
const path = require('path');
|
||||
const csrf = require('csurf');
|
||||
const validator = require('validator');
|
||||
const nconf = require('nconf');
|
||||
const toobusy = require('toobusy-js');
|
||||
const util = require('util');
|
||||
const { csrfSynchronisedProtection } = require('./csrf');
|
||||
|
||||
const plugins = require('../plugins');
|
||||
const meta = require('../meta');
|
||||
@@ -34,7 +34,7 @@ middleware.regexes = {
|
||||
timestampedUpload: /^\d+-.+$/,
|
||||
};
|
||||
|
||||
const csrfMiddleware = csrf();
|
||||
const csrfMiddleware = csrfSynchronisedProtection;
|
||||
|
||||
middleware.applyCSRF = function (req, res, next) {
|
||||
if (req.uid >= 0) {
|
||||
@@ -102,11 +102,20 @@ middleware.pluginHooks = helpers.try(async (req, res, next) => {
|
||||
});
|
||||
|
||||
middleware.validateFiles = function validateFiles(req, res, next) {
|
||||
if (!Array.isArray(req.files.files) || !req.files.files.length) {
|
||||
if (!req.files.files) {
|
||||
return next(new Error(['[[error:invalid-files]]']));
|
||||
}
|
||||
|
||||
next();
|
||||
if (Array.isArray(req.files.files) && req.files.files.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (typeof req.files.files === 'object') {
|
||||
req.files.files = [req.files.files];
|
||||
return next();
|
||||
}
|
||||
|
||||
return next(new Error(['[[error:invalid-files]]']));
|
||||
};
|
||||
|
||||
middleware.prepareAPI = function prepareAPI(req, res, next) {
|
||||
|
||||
@@ -37,7 +37,7 @@ module.exports = function (middleware) {
|
||||
const loginAsync = util.promisify(req.login).bind(req);
|
||||
await loginAsync(user, { keepSessionInfo: true });
|
||||
await controllers.authentication.onSuccessfulLogin(req, user.uid);
|
||||
req.uid = user.uid;
|
||||
req.uid = parseInt(user.uid, 10);
|
||||
req.loggedIn = req.uid > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -211,3 +211,8 @@ privsAdmin.groupPrivileges = async function (groupName) {
|
||||
const groupPrivilegeList = await privsAdmin.getGroupPrivilegeList();
|
||||
return await helpers.userOrGroupPrivileges(0, groupName, groupPrivilegeList);
|
||||
};
|
||||
|
||||
privsAdmin.getUidsWithPrivilege = async function (privilege) {
|
||||
const uidsByCid = await helpers.getUidsWithPrivilege([0], privilege);
|
||||
return uidsByCid[0];
|
||||
};
|
||||
|
||||
@@ -218,3 +218,7 @@ privsCategories.groupPrivileges = async function (cid, groupName) {
|
||||
const groupPrivilegeList = await privsCategories.getGroupPrivilegeList();
|
||||
return await helpers.userOrGroupPrivileges(cid, groupName, groupPrivilegeList);
|
||||
};
|
||||
|
||||
privsCategories.getUidsWithPrivilege = async function (cids, privilege) {
|
||||
return await helpers.getUidsWithPrivilege(cids, privilege);
|
||||
};
|
||||
|
||||
@@ -134,3 +134,8 @@ privsGlobal.groupPrivileges = async function (groupName) {
|
||||
const groupPrivilegeList = await privsGlobal.getGroupPrivilegeList();
|
||||
return await helpers.userOrGroupPrivileges(0, groupName, groupPrivilegeList);
|
||||
};
|
||||
|
||||
privsGlobal.getUidsWithPrivilege = async function (privilege) {
|
||||
const uidsByCid = await helpers.getUidsWithPrivilege([0], privilege);
|
||||
return uidsByCid[0];
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ const validator = require('validator');
|
||||
|
||||
const groups = require('../groups');
|
||||
const user = require('../user');
|
||||
const categories = require('../categories');
|
||||
const plugins = require('../plugins');
|
||||
const translator = require('../translator');
|
||||
|
||||
@@ -189,4 +190,38 @@ helpers.userOrGroupPrivileges = async function (cid, uidOrGroup, privilegeList)
|
||||
return _.zipObject(privilegeList, isMembers);
|
||||
};
|
||||
|
||||
helpers.getUidsWithPrivilege = async (cids, privilege) => {
|
||||
const disabled = (await categories.getCategoriesFields(cids, ['disabled'])).map(obj => obj.disabled);
|
||||
|
||||
const groupNames = cids.reduce((memo, cid) => {
|
||||
memo.push(`cid:${cid}:privileges:${privilege}`);
|
||||
memo.push(`cid:${cid}:privileges:groups:${privilege}`);
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
const memberSets = await groups.getMembersOfGroups(groupNames);
|
||||
// Every other set is actually a list of user groups, not uids, so convert those to members
|
||||
const sets = memberSets.reduce((memo, set, idx) => {
|
||||
if (idx % 2) {
|
||||
memo.groupNames.push(set);
|
||||
} else {
|
||||
memo.uids.push(set);
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, { groupNames: [], uids: [] });
|
||||
|
||||
const uniqGroups = _.uniq(_.flatten(sets.groupNames));
|
||||
const groupUids = await groups.getMembersOfGroups(uniqGroups);
|
||||
const map = _.zipObject(uniqGroups, groupUids);
|
||||
const uidsByCid = cids.map((cid, index) => {
|
||||
if (disabled[index]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return _.uniq(sets.uids[index].concat(_.flatten(sets.groupNames[index].map(g => map[g]))));
|
||||
});
|
||||
return uidsByCid;
|
||||
};
|
||||
|
||||
require('../promisify')(helpers);
|
||||
|
||||
@@ -10,6 +10,7 @@ const meta = require('../meta');
|
||||
const controllers = require('../controllers');
|
||||
const helpers = require('../controllers/helpers');
|
||||
const plugins = require('../plugins');
|
||||
const { generateToken } = require('../middleware/csrf');
|
||||
|
||||
let loginStrategies = [];
|
||||
|
||||
@@ -108,7 +109,7 @@ Auth.reloadRoutes = async function (params) {
|
||||
};
|
||||
|
||||
if (strategy.checkState !== false) {
|
||||
req.session.ssoState = req.csrfToken && req.csrfToken();
|
||||
req.session.ssoState = generateToken(req, true);
|
||||
opts.state = req.session.ssoState;
|
||||
}
|
||||
|
||||
@@ -154,7 +155,7 @@ Auth.reloadRoutes = async function (params) {
|
||||
}, Auth.middleware.validateAuth, (req, res, next) => {
|
||||
async.waterfall([
|
||||
async.apply(req.login.bind(req), res.locals.user, { keepSessionInfo: true }),
|
||||
async.apply(controllers.authentication.onSuccessfulLogin, req, req.uid),
|
||||
async.apply(controllers.authentication.onSuccessfulLogin, req, res.locals.user.uid),
|
||||
], (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -9,11 +9,12 @@ const topics = require('../topics');
|
||||
const user = require('../user');
|
||||
const categories = require('../categories');
|
||||
const meta = require('../meta');
|
||||
const helpers = require('../controllers/helpers');
|
||||
const controllerHelpers = require('../controllers/helpers');
|
||||
const privileges = require('../privileges');
|
||||
const db = require('../database');
|
||||
const utils = require('../utils');
|
||||
const controllers404 = require('../controllers/404');
|
||||
const routeHelpers = require('./helpers');
|
||||
|
||||
const terms = {
|
||||
daily: 'day',
|
||||
@@ -23,18 +24,18 @@ const terms = {
|
||||
};
|
||||
|
||||
module.exports = function (app, middleware) {
|
||||
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic);
|
||||
app.get('/category/:category_id.rss', middleware.maintenanceMode, generateForCategory);
|
||||
app.get('/topics.rss', middleware.maintenanceMode, generateForTopics);
|
||||
app.get('/recent.rss', middleware.maintenanceMode, generateForRecent);
|
||||
app.get('/top.rss', middleware.maintenanceMode, generateForTop);
|
||||
app.get('/top/:term.rss', middleware.maintenanceMode, generateForTop);
|
||||
app.get('/popular.rss', middleware.maintenanceMode, generateForPopular);
|
||||
app.get('/popular/:term.rss', middleware.maintenanceMode, generateForPopular);
|
||||
app.get('/recentposts.rss', middleware.maintenanceMode, generateForRecentPosts);
|
||||
app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, generateForCategoryRecentPosts);
|
||||
app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, generateForUserTopics);
|
||||
app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag);
|
||||
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopic));
|
||||
app.get('/category/:category_id.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategory));
|
||||
app.get('/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTopics));
|
||||
app.get('/recent.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecent));
|
||||
app.get('/top.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop));
|
||||
app.get('/top/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTop));
|
||||
app.get('/popular.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular));
|
||||
app.get('/popular/:term.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForPopular));
|
||||
app.get('/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForRecentPosts));
|
||||
app.get('/category/:category_id/recentposts.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForCategoryRecentPosts));
|
||||
app.get('/user/:userslug/topics.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForUserTopics));
|
||||
app.get('/tags/:tag.rss', middleware.maintenanceMode, routeHelpers.tryRoute(generateForTag));
|
||||
};
|
||||
|
||||
async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) {
|
||||
@@ -46,16 +47,16 @@ async function validateTokenIfRequiresLogin(requiresLogin, cid, req, res) {
|
||||
}
|
||||
|
||||
if (uid <= 0 || !token) {
|
||||
return helpers.notAllowed(req, res);
|
||||
return controllerHelpers.notAllowed(req, res);
|
||||
}
|
||||
const userToken = await db.getObjectField(`user:${uid}`, 'rss_token');
|
||||
if (userToken !== token) {
|
||||
await user.auth.logAttempt(uid, req.ip);
|
||||
return helpers.notAllowed(req, res);
|
||||
return controllerHelpers.notAllowed(req, res);
|
||||
}
|
||||
const userPrivileges = await privileges.categories.get(cid, uid);
|
||||
if (!userPrivileges.read) {
|
||||
return helpers.notAllowed(req, res);
|
||||
return controllerHelpers.notAllowed(req, res);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -127,7 +128,7 @@ async function generateForCategory(req, res, next) {
|
||||
db.getSortedSetRevIntersect({
|
||||
sets: ['topics:tid', `cid:${cid}:tids:lastposttime`],
|
||||
start: 0,
|
||||
stop: 25,
|
||||
stop: 24,
|
||||
weights: [1, 0],
|
||||
}),
|
||||
]);
|
||||
@@ -230,7 +231,7 @@ async function generateSorted(options, req, res, next) {
|
||||
const { cid } = req.query;
|
||||
if (cid) {
|
||||
if (!await privileges.categories.can('topics:read', cid, uid)) {
|
||||
return helpers.notAllowed(req, res);
|
||||
return controllerHelpers.notAllowed(req, res);
|
||||
}
|
||||
params.cids = [cid];
|
||||
}
|
||||
|
||||
@@ -79,7 +79,11 @@ sitemap.getPages = async function () {
|
||||
|
||||
async function getSitemapCategories() {
|
||||
const cids = await categories.getCidsByPrivilege('categories:cid', 0, 'find');
|
||||
return await categories.getCategoriesFields(cids, ['slug']);
|
||||
const categoryData = await categories.getCategoriesFields(cids, ['slug']);
|
||||
const data = await plugins.hooks.fire('filter:sitemap.getCategories', {
|
||||
categories: categoryData,
|
||||
});
|
||||
return data.categories;
|
||||
}
|
||||
|
||||
sitemap.getCategories = async function () {
|
||||
@@ -128,7 +132,12 @@ sitemap.getTopicPage = async function (page) {
|
||||
tids = await privileges.topics.filterTids('topics:read', tids, 0);
|
||||
const topicData = await topics.getTopicsFields(tids, ['tid', 'title', 'slug', 'lastposttime']);
|
||||
|
||||
if (!topicData.length) {
|
||||
const data = await plugins.hooks.fire('filter:sitemap.getCategories', {
|
||||
page: page,
|
||||
topics: topicData,
|
||||
});
|
||||
|
||||
if (!data.topics.length) {
|
||||
sitemap.maps.topics[page - 1] = {
|
||||
sm: '',
|
||||
cacheExpireTimestamp: Date.now() + (1000 * 60 * 60 * 24),
|
||||
@@ -136,7 +145,7 @@ sitemap.getTopicPage = async function (page) {
|
||||
return sitemap.maps.topics[page - 1].sm;
|
||||
}
|
||||
|
||||
topicData.forEach((topic) => {
|
||||
data.topics.forEach((topic) => {
|
||||
if (topic) {
|
||||
topicUrls.push({
|
||||
url: `${nconf.get('relative_path')}/topic/${topic.slug}`,
|
||||
|
||||
@@ -34,13 +34,25 @@ Sockets.init = async function (server) {
|
||||
}
|
||||
}
|
||||
|
||||
io.use(authorize);
|
||||
|
||||
io.on('connection', onConnection);
|
||||
|
||||
const opts = {
|
||||
transports: nconf.get('socket.io:transports') || ['polling', 'websocket'],
|
||||
cookie: false,
|
||||
allowRequest: (req, callback) => {
|
||||
authorize(req, (err) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const csrf = require('../middleware/csrf');
|
||||
const isValid = csrf.isRequestValid({
|
||||
session: req.session || {},
|
||||
query: req._query,
|
||||
headers: req.headers,
|
||||
});
|
||||
callback(null, isValid);
|
||||
});
|
||||
},
|
||||
};
|
||||
/*
|
||||
* Restrict socket.io listener to cookie domain. If none is set, infer based on url.
|
||||
@@ -62,7 +74,11 @@ Sockets.init = async function (server) {
|
||||
};
|
||||
|
||||
function onConnection(socket) {
|
||||
socket.ip = (socket.request.headers['x-forwarded-for'] || socket.request.connection.remoteAddress || '').split(',')[0];
|
||||
socket.uid = socket.request.uid;
|
||||
socket.ip = (
|
||||
socket.request.headers['x-forwarded-for'] ||
|
||||
socket.request.connection.remoteAddress || ''
|
||||
).split(',')[0];
|
||||
socket.request.ip = socket.ip;
|
||||
logger.io_one(socket, socket.uid);
|
||||
|
||||
@@ -112,43 +128,49 @@ async function onMessage(socket, payload) {
|
||||
return winston.warn('[socket.io] Empty payload');
|
||||
}
|
||||
|
||||
const eventName = payload.data[0];
|
||||
let eventName = payload.data[0];
|
||||
const params = typeof payload.data[1] === 'function' ? {} : payload.data[1];
|
||||
const callback = typeof payload.data[payload.data.length - 1] === 'function' ? payload.data[payload.data.length - 1] : function () {};
|
||||
|
||||
if (!eventName) {
|
||||
return winston.warn('[socket.io] Empty method name');
|
||||
}
|
||||
|
||||
const parts = eventName.toString().split('.');
|
||||
const namespace = parts[0];
|
||||
const methodToCall = parts.reduce((prev, cur) => {
|
||||
if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {
|
||||
return prev[cur];
|
||||
}
|
||||
return null;
|
||||
}, Namespaces);
|
||||
|
||||
if (!methodToCall || typeof methodToCall !== 'function') {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
winston.warn(`[socket.io] Unrecognized message: ${eventName}`);
|
||||
}
|
||||
const escapedName = validator.escape(String(eventName));
|
||||
return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
|
||||
}
|
||||
|
||||
socket.previousEvents = socket.previousEvents || [];
|
||||
socket.previousEvents.push(eventName);
|
||||
if (socket.previousEvents.length > 20) {
|
||||
socket.previousEvents.shift();
|
||||
}
|
||||
|
||||
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
|
||||
winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
|
||||
return socket.disconnect();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!eventName) {
|
||||
return winston.warn('[socket.io] Empty method name');
|
||||
}
|
||||
|
||||
if (typeof eventName !== 'string') {
|
||||
eventName = typeof eventName;
|
||||
const escapedName = validator.escape(eventName);
|
||||
return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
|
||||
}
|
||||
|
||||
const parts = eventName.split('.');
|
||||
const namespace = parts[0];
|
||||
const methodToCall = parts.reduce((prev, cur) => {
|
||||
if (prev !== null && prev[cur] && (!prev.hasOwnProperty || prev.hasOwnProperty(cur))) {
|
||||
return prev[cur];
|
||||
}
|
||||
return null;
|
||||
}, Namespaces);
|
||||
|
||||
if (!methodToCall || typeof methodToCall !== 'function') {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
winston.warn(`[socket.io] Unrecognized message: ${eventName}`);
|
||||
}
|
||||
const escapedName = validator.escape(String(eventName));
|
||||
return callback({ message: `[[error:invalid-event, ${escapedName}]]` });
|
||||
}
|
||||
|
||||
socket.previousEvents = socket.previousEvents || [];
|
||||
socket.previousEvents.push(eventName);
|
||||
if (socket.previousEvents.length > 20) {
|
||||
socket.previousEvents.shift();
|
||||
}
|
||||
|
||||
if (!eventName.startsWith('admin.') && ratelimit.isFlooding(socket)) {
|
||||
winston.warn(`[socket.io] Too many emits! Disconnecting uid : ${socket.uid}. Events : ${socket.previousEvents}`);
|
||||
return socket.disconnect();
|
||||
}
|
||||
|
||||
await checkMaintenance(socket);
|
||||
await validateSession(socket, '[[error:revalidate-failure]]');
|
||||
|
||||
@@ -225,9 +247,7 @@ async function validateSession(socket, errorMsg) {
|
||||
|
||||
const cookieParserAsync = util.promisify((req, callback) => cookieParser(req, {}, err => callback(err)));
|
||||
|
||||
async function authorize(socket, callback) {
|
||||
const { request } = socket;
|
||||
|
||||
async function authorize(request, callback) {
|
||||
if (!request) {
|
||||
return callback(new Error('[[error:not-authorized]]'));
|
||||
}
|
||||
@@ -240,15 +260,13 @@ async function authorize(socket, callback) {
|
||||
});
|
||||
|
||||
const sessionData = await getSessionAsync(sessionId);
|
||||
|
||||
request.session = sessionData;
|
||||
let uid = 0;
|
||||
if (sessionData && sessionData.passport && sessionData.passport.user) {
|
||||
request.session = sessionData;
|
||||
socket.uid = parseInt(sessionData.passport.user, 10);
|
||||
} else {
|
||||
socket.uid = 0;
|
||||
uid = parseInt(sessionData.passport.user, 10);
|
||||
}
|
||||
request.uid = socket.uid;
|
||||
callback();
|
||||
request.uid = uid;
|
||||
callback(null, uid);
|
||||
}
|
||||
|
||||
Sockets.in = function (room) {
|
||||
|
||||
@@ -14,15 +14,15 @@ const utils = require('../../utils');
|
||||
|
||||
module.exports = function (SocketPosts) {
|
||||
SocketPosts.loadPostTools = async function (socket, data) {
|
||||
if (!data || !data.pid || !data.cid) {
|
||||
if (!data || !data.pid) {
|
||||
throw new Error('[[error:invalid-data]]');
|
||||
}
|
||||
|
||||
const cid = await posts.getCidByPid(data.pid);
|
||||
const results = await utils.promiseParallel({
|
||||
posts: posts.getPostFields(data.pid, ['deleted', 'bookmarks', 'uid', 'ip', 'flagId']),
|
||||
isAdmin: user.isAdministrator(socket.uid),
|
||||
isGlobalMod: user.isGlobalModerator(socket.uid),
|
||||
isModerator: user.isModerator(socket.uid, data.cid),
|
||||
isModerator: user.isModerator(socket.uid, cid),
|
||||
canEdit: privileges.posts.canEdit(data.pid, socket.uid),
|
||||
canDelete: privileges.posts.canDelete(data.pid, socket.uid),
|
||||
canPurge: privileges.posts.canPurge(data.pid, socket.uid),
|
||||
|
||||
@@ -74,6 +74,6 @@ module.exports = function (SocketUser) {
|
||||
|
||||
await user.isAdminOrSelf(socket.uid, data.uid);
|
||||
|
||||
api.users.generateExport(socket, { type, ...data });
|
||||
api.users.generateExport(socket, { type, uid: data.uid });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -82,9 +82,8 @@ module.exports = function (Topics) {
|
||||
|
||||
data.title = String(data.title).trim();
|
||||
data.tags = data.tags || [];
|
||||
if (data.content) {
|
||||
data.content = utils.rtrim(data.content);
|
||||
}
|
||||
data.content = String(data.content || '').trimEnd();
|
||||
|
||||
Topics.checkTitle(data.title);
|
||||
await Topics.validateTags(data.tags, data.cid, uid);
|
||||
data.tags = await Topics.filterTags(data.tags, data.cid);
|
||||
@@ -167,9 +166,8 @@ module.exports = function (Topics) {
|
||||
data.cid = topicData.cid;
|
||||
|
||||
await guestHandleValid(data);
|
||||
if (data.content) {
|
||||
data.content = utils.rtrim(data.content);
|
||||
}
|
||||
data.content = String(data.content || '').trimEnd();
|
||||
|
||||
if (!data.fromQueue) {
|
||||
await user.isReadyToPost(uid, data.cid);
|
||||
Topics.checkContent(data.content);
|
||||
|
||||
@@ -60,6 +60,7 @@ Scheduled.pin = async function (tid, topicData) {
|
||||
};
|
||||
|
||||
Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
|
||||
const mainPid = await topics.getTopicField(tid, 'mainPid');
|
||||
await Promise.all([
|
||||
db.sortedSetsAdd([
|
||||
'topics:scheduled',
|
||||
@@ -67,6 +68,7 @@ Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
|
||||
'topics:tid',
|
||||
`cid:${cid}:uid:${uid}:tids`,
|
||||
], timestamp, tid),
|
||||
posts.setPostField(mainPid, 'timestamp', timestamp),
|
||||
shiftPostTimes(tid, timestamp),
|
||||
]);
|
||||
return topics.updateLastPostTimeFromLastPid(tid);
|
||||
@@ -87,14 +89,15 @@ function unpin(tid, topicData) {
|
||||
}
|
||||
|
||||
async function sendNotifications(uids, topicsData) {
|
||||
const usernames = await Promise.all(uids.map(uid => user.getUserField(uid, 'username')));
|
||||
const uidToUsername = Object.fromEntries(uids.map((uid, idx) => [uid, usernames[idx]]));
|
||||
const userData = await user.getUsersData(uids);
|
||||
const uidToUserData = Object.fromEntries(uids.map((uid, idx) => [uid, userData[idx]]));
|
||||
|
||||
const postsData = await posts.getPostsData(topicsData.map(({ mainPid }) => mainPid));
|
||||
const postsData = await posts.getPostsData(topicsData.map(t => t && t.mainPid));
|
||||
postsData.forEach((postData, idx) => {
|
||||
postData.user = {};
|
||||
postData.user.username = uidToUsername[postData.uid];
|
||||
postData.topic = topicsData[idx];
|
||||
if (postData) {
|
||||
postData.user = uidToUserData[topicsData[idx].uid];
|
||||
postData.topic = topicsData[idx];
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all(topicsData.map(
|
||||
|
||||
@@ -174,7 +174,7 @@ module.exports = function (Topics) {
|
||||
}
|
||||
|
||||
tids = await privileges.topics.filterTids('topics:read', tids, uid);
|
||||
let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid']);
|
||||
let topicData = await Topics.getTopicsFields(tids, ['uid', 'tid', 'cid', 'tags']);
|
||||
const topicCids = _.uniq(topicData.map(topic => topic.cid)).filter(Boolean);
|
||||
|
||||
async function getIgnoredCids() {
|
||||
@@ -192,11 +192,13 @@ module.exports = function (Topics) {
|
||||
topicData = filtered;
|
||||
|
||||
const cids = params.cids && params.cids.map(String);
|
||||
const { tags } = params;
|
||||
tids = topicData.filter(t => (
|
||||
t &&
|
||||
t.cid &&
|
||||
!isCidIgnored[t.cid] &&
|
||||
(!cids || cids.includes(String(t.cid)))
|
||||
(!cids || cids.includes(String(t.cid))) &&
|
||||
(!tags.length || tags.every(tag => t.tags.find(topicTag => topicTag.value === tag)))
|
||||
)).map(t => t.tid);
|
||||
|
||||
const result = await plugins.hooks.fire('filter:topics.filterSortedTids', { tids: tids, params: params });
|
||||
|
||||
@@ -232,10 +232,15 @@ module.exports = function (Topics) {
|
||||
if (!tids.length) {
|
||||
return;
|
||||
}
|
||||
let topicsTags = await Topics.getTopicsTags(tids);
|
||||
topicsTags = topicsTags.map(
|
||||
topicTags => topicTags.filter(topicTag => topicTag && topicTag !== tag)
|
||||
);
|
||||
|
||||
await db.deleteObjectFields(
|
||||
tids.map(tid => `topic:${tid}`),
|
||||
['tags'],
|
||||
await db.setObjectBulk(
|
||||
tids.map((tid, index) => ([
|
||||
`topic:${tid}`, { tags: topicsTags[index].join(',') },
|
||||
]))
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -287,7 +292,7 @@ module.exports = function (Topics) {
|
||||
}
|
||||
|
||||
Topics.getTagData = async function (tags) {
|
||||
if (!tags.length) {
|
||||
if (!tags || !tags.length) {
|
||||
return [];
|
||||
}
|
||||
tags.forEach((tag) => {
|
||||
|
||||
@@ -52,6 +52,7 @@ Thumbs.get = async function (tids) {
|
||||
const name = path.basename(thumb);
|
||||
return hasTimestampPrefix.test(name) ? name.slice(14) : name;
|
||||
})(),
|
||||
path: thumb,
|
||||
url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, thumb),
|
||||
})));
|
||||
|
||||
@@ -151,6 +152,9 @@ Thumbs.delete = async function (id, relativePaths) {
|
||||
Promise.all(toRemove.map(async relativePath => posts.uploads.dissociate(mainPid, relativePath.slice(1)))),
|
||||
]);
|
||||
}
|
||||
if (toRemove.length) {
|
||||
cache.del(set);
|
||||
}
|
||||
};
|
||||
|
||||
Thumbs.deleteAll = async (id) => {
|
||||
|
||||
46
src/upgrades/2.8.7/fix-email-sorted-sets.js
Normal file
46
src/upgrades/2.8.7/fix-email-sorted-sets.js
Normal file
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
const db = require('../../database');
|
||||
const batch = require('../../batch');
|
||||
|
||||
|
||||
module.exports = {
|
||||
name: 'Fix user email sorted sets',
|
||||
timestamp: Date.UTC(2023, 1, 4),
|
||||
method: async function () {
|
||||
const { progress } = this;
|
||||
const bulkRemove = [];
|
||||
await batch.processSortedSet('email:uid', async (data) => {
|
||||
progress.incr(data.length);
|
||||
const usersData = await db.getObjects(data.map(d => `user:${d.score}`));
|
||||
data.forEach((emailData, index) => {
|
||||
const { score: uid, value: email } = emailData;
|
||||
const userData = usersData[index];
|
||||
// user no longer exists or doesn't have email set in user hash
|
||||
// remove the email/uid pair from email:uid, email:sorted
|
||||
if (!userData || !userData.email) {
|
||||
bulkRemove.push(['email:uid', email]);
|
||||
bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
|
||||
return;
|
||||
}
|
||||
|
||||
// user has email but doesn't match whats stored in user hash, gh#11259
|
||||
if (userData.email && userData.email.toLowerCase() !== email.toLowerCase()) {
|
||||
bulkRemove.push(['email:uid', email]);
|
||||
bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
|
||||
}
|
||||
});
|
||||
}, {
|
||||
batch: 500,
|
||||
withScores: true,
|
||||
progress: progress,
|
||||
});
|
||||
|
||||
await batch.processArray(bulkRemove, async (bulk) => {
|
||||
await db.sortedSetRemoveBulk(bulk);
|
||||
}, {
|
||||
batch: 500,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -70,7 +70,9 @@ module.exports = function (User) {
|
||||
|
||||
let line = '';
|
||||
usersData.forEach((user, index) => {
|
||||
line += `${fields.map(field => user[field]).join(',')}`;
|
||||
line += `${fields
|
||||
.map(field => (isFinite(user[field]) ? `'${user[field]}'` : user[field]))
|
||||
.join(',')}`;
|
||||
if (showIps) {
|
||||
userIPs = ips[index] ? ips[index].join(',') : '';
|
||||
line += `,"${userIPs}"\n`;
|
||||
|
||||
@@ -39,7 +39,7 @@ UserEmail.remove = async function (uid, sessionId) {
|
||||
db.sortedSetRemove('email:uid', email.toLowerCase()),
|
||||
db.sortedSetRemove('email:sorted', `${email.toLowerCase()}:${uid}`),
|
||||
user.email.expireValidation(uid),
|
||||
user.auth.revokeAllSessions(uid, sessionId),
|
||||
sessionId ? user.auth.revokeAllSessions(uid, sessionId) : Promise.resolve(),
|
||||
events.log({ type: 'email-change', email, newEmail: '' }),
|
||||
]);
|
||||
};
|
||||
@@ -69,7 +69,7 @@ UserEmail.expireValidation = async (uid) => {
|
||||
};
|
||||
|
||||
UserEmail.canSendValidation = async (uid, email) => {
|
||||
const pending = UserEmail.isValidationPending(uid, email);
|
||||
const pending = await UserEmail.isValidationPending(uid, email);
|
||||
if (!pending) {
|
||||
return true;
|
||||
}
|
||||
@@ -134,13 +134,13 @@ UserEmail.sendValidationEmail = async function (uid, options) {
|
||||
|
||||
await UserEmail.expireValidation(uid);
|
||||
await db.set(`confirm:byUid:${uid}`, confirm_code);
|
||||
await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 24 * 60 * 60 * 1000);
|
||||
await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 60 * 60 * 1000);
|
||||
|
||||
await db.setObject(`confirm:${confirm_code}`, {
|
||||
email: options.email.toLowerCase(),
|
||||
uid: uid,
|
||||
});
|
||||
await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 24 * 60 * 60 * 1000);
|
||||
await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 60 * 60 * 1000);
|
||||
|
||||
winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`);
|
||||
events.log({
|
||||
@@ -196,6 +196,20 @@ UserEmail.confirmByUid = async function (uid) {
|
||||
throw new Error('[[error:invalid-email]]');
|
||||
}
|
||||
|
||||
// If another uid has the same email throw error
|
||||
const oldUid = await db.sortedSetScore('email:uid', currentEmail.toLowerCase());
|
||||
if (oldUid && oldUid !== parseInt(uid, 10)) {
|
||||
throw new Error('[[error:email-taken]]');
|
||||
}
|
||||
|
||||
const confirmedEmails = await db.getSortedSetRangeByScore(`email:uid`, 0, -1, uid, uid);
|
||||
if (confirmedEmails.length) {
|
||||
// remove old email of user by uid
|
||||
await db.sortedSetsRemoveRangeByScore([`email:uid`], uid, uid);
|
||||
await db.sortedSetRemoveBulk(
|
||||
confirmedEmails.map(email => [`email:sorted`, `${email.toLowerCase()}:${uid}`])
|
||||
);
|
||||
}
|
||||
await Promise.all([
|
||||
db.sortedSetAddBulk([
|
||||
['email:uid', uid, currentEmail.toLowerCase()],
|
||||
|
||||
@@ -40,8 +40,13 @@ Interstitials.email = async (data) => {
|
||||
issuePasswordChallenge: !!data.userData.uid && hasPassword,
|
||||
},
|
||||
callback: async (userData, formData) => {
|
||||
if (formData.email) {
|
||||
formData.email = String(formData.email).trim();
|
||||
}
|
||||
|
||||
// Validate and send email confirmation
|
||||
if (userData.uid) {
|
||||
const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10);
|
||||
const [isPasswordCorrect, canEdit, { email: current, 'email:confirmed': confirmed }, { allowed, error }] = await Promise.all([
|
||||
user.isPasswordCorrect(userData.uid, formData.password, data.req.ip),
|
||||
privileges.users.canEdit(data.req.uid, userData.uid),
|
||||
@@ -68,13 +73,17 @@ Interstitials.email = async (data) => {
|
||||
if (formData.email === current) {
|
||||
if (confirmed) {
|
||||
throw new Error('[[error:email-nochange]]');
|
||||
} else if (await user.email.canSendValidation(userData.uid, current)) {
|
||||
} else if (!await user.email.canSendValidation(userData.uid, current)) {
|
||||
throw new Error(`[[error:confirm-email-already-sent, ${meta.config.emailConfirmInterval}]]`);
|
||||
}
|
||||
}
|
||||
|
||||
// Admins editing will auto-confirm, unless editing their own email
|
||||
if (isAdminOrGlobalMod && userData.uid !== data.req.uid) {
|
||||
if (!await user.email.available(formData.email)) {
|
||||
throw new Error('[[error:email-taken]]');
|
||||
}
|
||||
await user.email.remove(userData.uid);
|
||||
await user.setUserField(userData.uid, 'email', formData.email);
|
||||
await user.email.confirmByUid(userData.uid);
|
||||
} else if (canEdit) {
|
||||
@@ -99,8 +108,8 @@ Interstitials.email = async (data) => {
|
||||
}
|
||||
|
||||
if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) {
|
||||
// User explicitly clearing their email
|
||||
await user.email.remove(userData.uid, data.req.session.id);
|
||||
// User or admin explicitly clearing their email
|
||||
await user.email.remove(userData.uid, isSelf ? data.req.session.id : null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -27,9 +27,13 @@ module.exports = function (User) {
|
||||
if (now - parseInt(userOnlineTime, 10) < 300000) {
|
||||
return;
|
||||
}
|
||||
await db.sortedSetAdd('users:online', now, uid);
|
||||
await User.onUserOnline(uid, now);
|
||||
topics.pushUnreadCount(uid);
|
||||
plugins.hooks.fire('action:user.online', { uid: uid, timestamp: now });
|
||||
};
|
||||
|
||||
User.onUserOnline = async (uid, timestamp) => {
|
||||
await db.sortedSetAdd('users:online', timestamp, uid);
|
||||
plugins.hooks.fire('action:user.online', { uid, timestamp });
|
||||
};
|
||||
|
||||
User.isOnline = async function (uid) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
{{{ end }}}
|
||||
{{{ each topics }}}
|
||||
<tr>
|
||||
<td><a href="{config.relative_path}/topics/{../slug}">{../title}</a></td>
|
||||
<td><a href="{config.relative_path}/topic/{../slug}">{../title}</a></td>
|
||||
<td>[[topic:posted_by, {../user.username}]]</td>
|
||||
<td><span class="timeago" data-title="{../timestampISO}"></span></td>
|
||||
</tr>
|
||||
|
||||
@@ -150,7 +150,7 @@
|
||||
[[admin/settings/email:smtp-transport.gmail-warning2]]
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group well" id="email:smtpTransport:custom-service" style="display: none">
|
||||
<div class="form-group well" id="email:smtpTransport:custom-service">
|
||||
<h5>Custom Service</h5>
|
||||
|
||||
<label for="email:smtpTransport:host">[[admin/settings/email:smtp-transport.host]]</label>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="alert alert-info">[[modules:thumbs.modal.no-thumbs]]</div>
|
||||
{{{ end }}}
|
||||
{{{ each thumbs }}}
|
||||
<div class="media" data-id="{./id}" data-path="{./url}">
|
||||
<div class="media" data-id="{./id}" data-path="{./path}">
|
||||
<div class="media-left">
|
||||
<img class="media-object" src="{./url}" alt="" />
|
||||
</div>
|
||||
|
||||
@@ -12,13 +12,22 @@ const db = require('./mocks/databasemock');
|
||||
const user = require('../src/user');
|
||||
const utils = require('../src/utils');
|
||||
const meta = require('../src/meta');
|
||||
const plugins = require('../src/plugins');
|
||||
const privileges = require('../src/privileges');
|
||||
const helpers = require('./helpers');
|
||||
|
||||
describe('authentication', () => {
|
||||
const jar = request.jar();
|
||||
let regularUid;
|
||||
const dummyEmailerHook = async (data) => {};
|
||||
|
||||
before((done) => {
|
||||
// Attach an emailer hook so related requests do not error
|
||||
plugins.hooks.register('authentication-test', {
|
||||
hook: 'filter:email.send',
|
||||
method: dummyEmailerHook,
|
||||
});
|
||||
|
||||
user.create({ username: 'regular', password: 'regularpwd', email: 'regular@nodebb.org' }, (err, uid) => {
|
||||
assert.ifError(err);
|
||||
regularUid = uid;
|
||||
@@ -27,6 +36,10 @@ describe('authentication', () => {
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
plugins.hooks.unregister('authentication-test', 'filter:email.send');
|
||||
});
|
||||
|
||||
it('should allow login with email for uid 1', async () => {
|
||||
const oldValue = meta.config.allowLoginWith;
|
||||
meta.config.allowLoginWith = 'username-email';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user