mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-12-24 09:20:32 +01:00
Compare commits
116 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8228eeb468 | ||
|
|
fc09f6c4f9 | ||
|
|
7a26672872 | ||
|
|
de820ae198 | ||
|
|
961a193787 | ||
|
|
4ab31e3f79 | ||
|
|
338f89deb5 | ||
|
|
dc14d6a8d1 | ||
|
|
fb43f9ae10 | ||
|
|
638e098f30 | ||
|
|
2514aace4e | ||
|
|
9ec7ab4afc | ||
|
|
dd5ed9e507 | ||
|
|
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 | ||
|
|
e45a6de24b | ||
|
|
3f8248d673 | ||
|
|
f4282c091b | ||
|
|
af6ce44737 | ||
|
|
c6681a1725 | ||
|
|
bff5ce2d79 | ||
|
|
4821b21e81 | ||
|
|
a46b2bbc45 | ||
|
|
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}}
|
||||
|
||||
201
CHANGELOG.md
201
CHANGELOG.md
@@ -1,35 +1,90 @@
|
||||
#### 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)
|
||||
* incrementing version number - v2.8.5 (bff5ce2d)
|
||||
* incrementing version number - v2.8.4 (a46b2bbc)
|
||||
* incrementing version number - v2.8.3 (c20b20a7)
|
||||
* incrementing version number - v2.8.2 (050e43f8)
|
||||
* 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)
|
||||
|
||||
##### Documentation Changes
|
||||
|
||||
@@ -58,31 +113,6 @@
|
||||
* **i18n:** fallback strings for new resources: nodebb.error (8335f90a)
|
||||
* incrementing version number - v2.8.5 (bff5ce2d)
|
||||
* update changelog for v2.8.5 (24e58c28)
|
||||
* incrementing version number - v2.8.4 (a46b2bbc)
|
||||
* incrementing version number - v2.8.3 (c20b20a7)
|
||||
* incrementing version number - v2.8.2 (050e43f8)
|
||||
* 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)
|
||||
|
||||
##### New Features
|
||||
|
||||
@@ -108,30 +138,6 @@
|
||||
|
||||
* incrementing version number - v2.8.4 (a46b2bbc)
|
||||
* update changelog for v2.8.4 (c13f0e21)
|
||||
* incrementing version number - v2.8.3 (c20b20a7)
|
||||
* incrementing version number - v2.8.2 (050e43f8)
|
||||
* 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
|
||||
|
||||
@@ -143,29 +149,6 @@
|
||||
|
||||
* incrementing version number - v2.8.3 (c20b20a7)
|
||||
* update changelog for v2.8.3 (eb2841ee)
|
||||
* incrementing version number - v2.8.2 (050e43f8)
|
||||
* 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)
|
||||
|
||||
#### v2.8.3 (2023-01-25)
|
||||
|
||||
@@ -174,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.8",
|
||||
"version": "2.8.18",
|
||||
"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.1",
|
||||
"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.6",
|
||||
"nodebb-plugin-dbsearch": "5.1.5",
|
||||
"nodebb-plugin-emoji": "4.0.6",
|
||||
"nodebb-plugin-emoji-android": "3.0.0",
|
||||
@@ -99,7 +99,7 @@
|
||||
"nodebb-plugin-spam-be-gone": "1.0.2",
|
||||
"nodebb-rewards-essentials": "0.2.1",
|
||||
"nodebb-theme-lavender": "6.0.1",
|
||||
"nodebb-theme-persona": "12.1.12",
|
||||
"nodebb-theme-persona": "12.1.18",
|
||||
"nodebb-theme-slick": "2.0.2",
|
||||
"nodebb-theme-vanilla": "12.1.19",
|
||||
"nodebb-widget-essentials": "6.0.1",
|
||||
@@ -192,4 +192,4 @@
|
||||
"url": "https://github.com/barisusakli"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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...');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -374,6 +374,20 @@ get:
|
||||
type: string
|
||||
postIndex:
|
||||
type: number
|
||||
author:
|
||||
type: object
|
||||
required: [username, uid]
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
userslug:
|
||||
type: string
|
||||
uid:
|
||||
type: number
|
||||
fullname:
|
||||
type: string
|
||||
displayname:
|
||||
type: string
|
||||
loggedInUser:
|
||||
$ref: ../../components/schemas/UserObject.yaml#/UserObject
|
||||
- type: object
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -24,7 +24,7 @@ define('forum/topic/images', [], function () {
|
||||
|
||||
if (!$this.parent().is('a')) {
|
||||
$this.wrap('<a href="' + src + '" ' +
|
||||
(!srcExt && altExt ? ' download="' + altFilename + '" ' : '') +
|
||||
(!srcExt && altExt ? ' download="' + utils.escapeHTML(altFilename) + '" ' : '') +
|
||||
' target="_blank" rel="noopener">');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -237,23 +237,26 @@ Analytics.getDailyStatsForSet = async function (set, day, numDays) {
|
||||
set = `analytics:${set}`;
|
||||
}
|
||||
|
||||
const daysArr = [];
|
||||
day = new Date(day);
|
||||
// set the date to tomorrow, because getHourlyStatsForSet steps *backwards* 24 hours to sum up the values
|
||||
day.setDate(day.getDate() + 1);
|
||||
day.setHours(0, 0, 0, 0);
|
||||
|
||||
while (numDays > 0) {
|
||||
/* eslint-disable no-await-in-loop */
|
||||
async function getHourlyStats(hour) {
|
||||
const dayData = await Analytics.getHourlyStatsForSet(
|
||||
set,
|
||||
day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1)),
|
||||
hour,
|
||||
24
|
||||
);
|
||||
daysArr.push(dayData.reduce((cur, next) => cur + next));
|
||||
return dayData.reduce((cur, next) => cur + next);
|
||||
}
|
||||
const hours = [];
|
||||
while (numDays > 0) {
|
||||
hours.push(day.getTime() - (1000 * 60 * 60 * 24 * (numDays - 1)));
|
||||
numDays -= 1;
|
||||
}
|
||||
return daysArr;
|
||||
|
||||
return await Promise.all(hours.map(getHourlyStats));
|
||||
};
|
||||
|
||||
Analytics.getUnwrittenPageviews = function () {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -128,12 +128,13 @@ async function getStats() {
|
||||
}
|
||||
|
||||
let results = await Promise.all([
|
||||
getStatsForSet('ip:recent', 'uniqueIPCount'),
|
||||
getStatsFromAnalytics('uniquevisitors', 'uniqueIPCount'),
|
||||
getStatsFromAnalytics('logins', 'loginCount'),
|
||||
getStatsForSet('users:joindate', 'userCount'),
|
||||
getStatsForSet('posts:pid', 'postCount'),
|
||||
getStatsForSet('topics:tid', 'topicCount'),
|
||||
]);
|
||||
|
||||
results[0].name = '[[admin/dashboard:unique-visitors]]';
|
||||
|
||||
results[1].name = '[[admin/dashboard:logins]]';
|
||||
|
||||
@@ -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 || '',
|
||||
|
||||
@@ -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,7 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const validator = require('validator');
|
||||
|
||||
const user = require('../user');
|
||||
const groups = require('../groups');
|
||||
@@ -41,9 +42,9 @@ modsController.flags.list = async function (req, res) {
|
||||
filters = filters.reduce((memo, cur) => {
|
||||
if (req.query.hasOwnProperty(cur)) {
|
||||
if (typeof req.query[cur] === 'string' && req.query[cur].trim() !== '') {
|
||||
memo[cur] = req.query[cur].trim();
|
||||
memo[cur] = validator.escape(String(req.query[cur].trim()));
|
||||
} else if (Array.isArray(req.query[cur]) && req.query[cur].length) {
|
||||
memo[cur] = req.query[cur];
|
||||
memo[cur] = req.query[cur].map(item => validator.escape(String(item).trim()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -105,8 +105,8 @@ topicsController.get = async function getTopic(req, res, next) {
|
||||
|
||||
topicData.postIndex = postIndex;
|
||||
|
||||
await Promise.all([
|
||||
buildBreadcrumbs(topicData),
|
||||
const [author] = await Promise.all([
|
||||
user.getUserFields(topicData.uid, ['username', 'userslug']),
|
||||
addOldCategory(topicData, userPrivileges),
|
||||
addTags(topicData, req, res),
|
||||
incrementViewCount(req, tid),
|
||||
@@ -114,6 +114,7 @@ topicsController.get = async function getTopic(req, res, next) {
|
||||
analytics.increment([`pageviews:byCid:${topicData.category.cid}`]),
|
||||
]);
|
||||
|
||||
topicData.author = author;
|
||||
topicData.pagination = pagination.create(currentPage, pageCount, req.query);
|
||||
topicData.pagination.rel.forEach((rel) => {
|
||||
rel.href = `${url}/topic/${topicData.slug}${rel.href}`;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -629,9 +629,9 @@ SELECT z."value",
|
||||
ON o."_key" = z."_key"
|
||||
AND o."type" = z."type"
|
||||
WHERE o."_key" = $1::TEXT
|
||||
AND z."value" LIKE '${match}'
|
||||
AND z."value" LIKE $3
|
||||
LIMIT $2::INTEGER`,
|
||||
values: [params.key, params.limit],
|
||||
values: [params.key, params.limit, match],
|
||||
});
|
||||
if (!params.withScores) {
|
||||
return res.rows.map(r => r.value);
|
||||
|
||||
@@ -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 || [];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -40,6 +40,10 @@ 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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1 +1 @@
|
||||
data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-timestamp="{posts.timestamp}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemscope itemtype="http://schema.org/Comment"
|
||||
data-index="{posts.index}" data-pid="{posts.pid}" data-uid="{posts.uid}" data-timestamp="{posts.timestamp}" data-username="{posts.user.username}" data-userslug="{posts.user.userslug}" itemprop="comment" itemtype="http://schema.org/Comment" itemscope
|
||||
@@ -89,6 +89,21 @@ describe('Sorted Set methods', () => {
|
||||
assert(data.includes('ddb'));
|
||||
assert(data.includes('adb'));
|
||||
});
|
||||
|
||||
it('should not error with invalid input', async () => {
|
||||
const query = `-3217'
|
||||
OR 1251=CAST((CHR(113)||CHR(98)||CHR(118)||CHR(98)||CHR(113))||(SELECT
|
||||
(CASE WHEN (1251=1251) THEN 1 ELSE 0
|
||||
END))::text||(CHR(113)||CHR(113)||CHR(118)||CHR(98)||CHR(113)) AS
|
||||
NUMERIC)-- WsPn&query[cid]=-1&parentCid=0&selectedCids[]=-1&privilege=topics:read&states[]=watching&states[]=tracking&states[]=notwatching&showLinks=`;
|
||||
const match = `*${query.toLowerCase()}*`;
|
||||
const data = await db.getSortedSetScan({
|
||||
key: 'categories:name',
|
||||
match: match,
|
||||
limit: 500,
|
||||
});
|
||||
assert.strictEqual(data.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortedSetAdd()', () => {
|
||||
@@ -996,6 +1011,11 @@ describe('Sorted Set methods', () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array if sets is empty', async () => {
|
||||
const result = await db.getSortedSetRevUnion({ sets: [], start: 0, stop: -1 });
|
||||
assert.deepStrictEqual(result, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortedSetIncrBy()', () => {
|
||||
|
||||
@@ -869,6 +869,11 @@ describe('Flags', () => {
|
||||
assert.strictEqual(flagData.reports[0].value, '"<script>alert('ok');</script>');
|
||||
});
|
||||
|
||||
it('should escape filters', async () => {
|
||||
const { body } = await request.get(`${nconf.get('url')}/api/flags?quick="<script>alert('foo');</script>`, { jar });
|
||||
assert.strictEqual(body.filters.quick, '"<script>alert('foo');</script>');
|
||||
});
|
||||
|
||||
it('should not allow flagging post in private category', async () => {
|
||||
const category = await Categories.create({ name: 'private category' });
|
||||
|
||||
@@ -1149,5 +1154,7 @@ describe('Flags', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,7 +95,7 @@ helpers.logoutUser = function (jar, callback) {
|
||||
});
|
||||
};
|
||||
|
||||
helpers.connectSocketIO = function (res, callback) {
|
||||
helpers.connectSocketIO = function (res, csrf_token, callback) {
|
||||
const io = require('socket.io-client');
|
||||
let cookies = res.headers['set-cookie'];
|
||||
cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c));
|
||||
@@ -106,6 +106,9 @@ helpers.connectSocketIO = function (res, callback) {
|
||||
Origin: nconf.get('url'),
|
||||
Cookie: cookie,
|
||||
},
|
||||
query: {
|
||||
_csrf: csrf_token,
|
||||
},
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
@@ -121,7 +124,6 @@ helpers.uploadFile = function (uploadEndPoint, filePath, body, jar, csrf_token,
|
||||
let formData = {
|
||||
files: [
|
||||
fs.createReadStream(filePath),
|
||||
fs.createReadStream(filePath), // see https://github.com/request/request/issues/2445
|
||||
],
|
||||
};
|
||||
formData = utils.merge(formData, body);
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('socket.io', () => {
|
||||
}, (err, res) => {
|
||||
assert.ifError(err);
|
||||
|
||||
helpers.connectSocketIO(res, (err, _io) => {
|
||||
helpers.connectSocketIO(res, body.csrf_token, (err, _io) => {
|
||||
io = _io;
|
||||
assert.ifError(err);
|
||||
|
||||
@@ -107,6 +107,14 @@ describe('socket.io', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should return error for invalid eventName type', (done) => {
|
||||
const eventName = ['topics.loadMoreTags'];
|
||||
io.emit(eventName, (err) => {
|
||||
assert.strictEqual(err.message, `[[error:invalid-event, object]]`);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get installed themes', (done) => {
|
||||
const themes = ['nodebb-theme-lavender', 'nodebb-theme-persona', 'nodebb-theme-vanilla'];
|
||||
io.emit('admin.themes.getInstalled', (err, data) => {
|
||||
|
||||
@@ -1932,6 +1932,14 @@ describe('Topic\'s', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should only delete one tag from topic', async () => {
|
||||
const result1 = await topics.post({ uid: adminUid, tags: ['deleteme1', 'deleteme2', 'deleteme3'], title: 'topic tagged with plugins', content: 'topic 1 content', cid: topic.categoryId });
|
||||
await topics.deleteTag('deleteme2');
|
||||
const topicData = await topics.getTopicData(result1.topicData.tid);
|
||||
const tags = topicData.tags.map(t => t.value);
|
||||
assert.deepStrictEqual(tags, ['deleteme1', 'deleteme3']);
|
||||
});
|
||||
|
||||
it('should delete tag', (done) => {
|
||||
topics.deleteTag('javascript', (err) => {
|
||||
assert.ifError(err);
|
||||
|
||||
@@ -82,6 +82,7 @@ describe('Topic thumbs', () => {
|
||||
assert.deepStrictEqual(thumbs, [{
|
||||
id: topicObj.topicData.tid,
|
||||
name: 'test.png',
|
||||
path: `${relativeThumbPaths[0]}`,
|
||||
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`,
|
||||
}]);
|
||||
});
|
||||
@@ -92,6 +93,7 @@ describe('Topic thumbs', () => {
|
||||
[{
|
||||
id: topicObj.topicData.tid,
|
||||
name: 'test.png',
|
||||
path: `${relativeThumbPaths[0]}`,
|
||||
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`,
|
||||
}],
|
||||
[],
|
||||
@@ -200,16 +202,19 @@ describe('Topic thumbs', () => {
|
||||
{
|
||||
id: tid,
|
||||
name: 'test.png',
|
||||
path: relativeThumbPaths[0],
|
||||
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[0]}`,
|
||||
},
|
||||
{
|
||||
id: tid,
|
||||
name: 'example.org',
|
||||
path: 'https://example.org',
|
||||
url: 'https://example.org',
|
||||
},
|
||||
{
|
||||
id: tid,
|
||||
name: 'test2.png',
|
||||
path: relativeThumbPaths[1],
|
||||
url: `${nconf.get('relative_path')}${nconf.get('upload_url')}${relativeThumbPaths[1]}`,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -87,8 +87,7 @@ describe('Upload Controllers', () => {
|
||||
const oldValue = meta.config.allowedFileExtensions;
|
||||
meta.config.allowedFileExtensions = 'png,jpg,bmp,html';
|
||||
require('../src/middleware/uploads').clearCache();
|
||||
// why / 2? see: helpers.uploadFile for a weird quirk where we actually upload 2 files per upload in our tests.
|
||||
const times = (meta.config.uploadRateLimitThreshold / 2) + 1;
|
||||
const times = meta.config.uploadRateLimitThreshold + 1;
|
||||
async.timesSeries(times, (i, next) => {
|
||||
helpers.uploadFile(`${nconf.get('url')}/api/post/upload`, path.join(__dirname, '../test/files/503.html'), {}, jar, csrf_token, (err, res, body) => {
|
||||
if (i + 1 >= times) {
|
||||
@@ -522,7 +521,7 @@ describe('Upload Controllers', () => {
|
||||
it('should return files with no post associated with them', async () => {
|
||||
const orphans = await posts.uploads.getOrphans();
|
||||
|
||||
assert.strictEqual(orphans.length, 2);
|
||||
assert.strictEqual(orphans.length, 1);
|
||||
orphans.forEach((relPath) => {
|
||||
assert(relPath.startsWith('files/'));
|
||||
assert(relPath.endsWith('test.png'));
|
||||
@@ -553,7 +552,7 @@ describe('Upload Controllers', () => {
|
||||
await posts.uploads.cleanOrphans();
|
||||
const orphans = await posts.uploads.getOrphans();
|
||||
|
||||
assert.strictEqual(orphans.length, 2);
|
||||
assert.strictEqual(orphans.length, 1);
|
||||
});
|
||||
|
||||
it('should not touch orphans if they are newer than the configured expiry', async () => {
|
||||
@@ -561,7 +560,7 @@ describe('Upload Controllers', () => {
|
||||
await posts.uploads.cleanOrphans();
|
||||
const orphans = await posts.uploads.getOrphans();
|
||||
|
||||
assert.strictEqual(orphans.length, 2);
|
||||
assert.strictEqual(orphans.length, 1);
|
||||
});
|
||||
|
||||
it('should delete orphans older than the configured number of days', async () => {
|
||||
|
||||
Reference in New Issue
Block a user