Compare commits

..

19 Commits

Author SHA1 Message Date
Barış Soner Uşaklı
129474f268 Merge branch 'develop' into axios 2023-12-17 19:08:58 -05:00
Barış Soner Uşaklı
40170f8133 cleaunup 2023-12-17 14:42:09 -05:00
Barış Soner Uşaklı
d4452ba25c socket.io 2023-12-17 14:37:53 -05:00
Barış Soner Uşaklı
5c6a631fc6 uploads.js 2023-12-17 14:20:45 -05:00
Barış Soner Uşaklı
dc1cd3feaa user/emails 2023-12-17 14:02:41 -05:00
Barış Soner Uşaklı
841b856e27 topics/thumbs 2023-12-17 13:32:59 -05:00
Barış Soner Uşaklı
abf81ec57d search 2023-12-17 13:09:53 -05:00
Barış Soner Uşaklı
b89502ce0b posts 2023-12-17 13:07:21 -05:00
Barış Soner Uşaklı
381e64e657 plugins 2023-12-17 12:54:22 -05:00
Barış Soner Uşaklı
110b867ed5 meta 2023-12-17 12:52:23 -05:00
Barış Soner Uşaklı
4b006b37cc remove log 2023-12-17 12:44:30 -05:00
Barış Soner Uşaklı
e51c8de9cb messaging/middleware 2023-12-17 12:42:07 -05:00
Barış Soner Uşaklı
918008fffd locale-detect 2023-12-17 12:08:48 -05:00
Barış Soner Uşaklı
2e2b1e9d6c flags 2023-12-17 12:03:49 -05:00
Barış Soner Uşaklı
ca42094f3f remove unused async 2023-12-17 11:40:31 -05:00
Barış Soner Uşaklı
3d07f14859 feeds 2023-12-17 11:40:13 -05:00
Barış Soner Uşaklı
32175666d5 add missing deps 2023-12-16 23:30:06 -05:00
Barış Soner Uşaklı
1501a9303c controller tests 2023-12-16 23:28:14 -05:00
Barış Soner Uşaklı
db75571923 axios migration 2023-12-16 19:40:36 -05:00
84 changed files with 1678 additions and 1059 deletions

View File

@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
"version": "3.6.0",
"version": "3.5.3",
"homepage": "https://www.nodebb.org",
"repository": {
"type": "git",
@@ -38,6 +38,8 @@
"archiver": "6.0.1",
"async": "3.2.5",
"autoprefixer": "10.4.16",
"axios": "1.6.2",
"axios-cookiejar-support": "4.0.7",
"bcryptjs": "2.4.3",
"benchpressjs": "2.5.1",
"body-parser": "1.20.2",
@@ -67,8 +69,8 @@
"express": "4.18.2",
"express-session": "1.17.3",
"express-useragent": "1.0.15",
"fetch-cookie": "2.1.0",
"file-loader": "6.2.0",
"form-data": "4.0.0",
"fs-extra": "11.2.0",
"graceful-fs": "4.2.11",
"helmet": "7.1.0",
@@ -103,7 +105,7 @@
"nodebb-plugin-ntfy": "1.7.3",
"nodebb-plugin-spam-be-gone": "2.2.0",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "1.1.105",
"nodebb-theme-harmony": "1.1.103",
"nodebb-theme-lavender": "7.1.5",
"nodebb-theme-peace": "2.1.25",
"nodebb-theme-persona": "13.2.49",
@@ -127,7 +129,7 @@
"sass": "1.69.5",
"semver": "7.5.4",
"serve-favicon": "2.5.0",
"sharp": "0.33.1",
"sharp": "0.33.0",
"sitemap": "7.1.1",
"socket.io": "4.7.2",
"socket.io-client": "4.7.2",
@@ -146,7 +148,7 @@
"webpack": "5.89.0",
"webpack-merge": "5.10.0",
"winston": "3.11.0",
"workerpool": "9.0.1",
"workerpool": "8.0.0",
"xml": "1.0.1",
"xregexp": "5.1.1",
"yargs": "17.7.2",
@@ -157,9 +159,9 @@
"@commitlint/cli": "18.4.3",
"@commitlint/config-angular": "18.4.3",
"coveralls": "3.1.1",
"eslint": "8.56.0",
"eslint": "8.55.0",
"eslint-config-nodebb": "0.2.1",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-import": "2.29.0",
"grunt": "1.6.1",
"grunt-contrib-watch": "1.1.0",
"husky": "8.0.3",
@@ -181,7 +183,7 @@
"url": "https://github.com/NodeBB/NodeBB/issues"
},
"engines": {
"node": ">=18"
"node": ">=16"
},
"maintainers": [
{
@@ -195,4 +197,4 @@
"url": "https://github.com/barisusakli"
}
]
}
}

View File

@@ -1,41 +0,0 @@
<html>
<head>
<title>Internal Server Error</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/assets/5xx.css" />
<script type="text/javascript">
window.onload = function() {
let count = 0;
const bounce = document.getElementById('click-me');
bounce.onclick = function() {
count++;
bounce.className = '';
setTimeout(function() {
bounce.className = 'animated bounce';
}, 50);
if (count > 5) {
document.getElementById('hide').className = '';
}
};
}
</script>
</head>
<body>
<div class="wrapper">
<div class="center">
<h1 id="click-me" class="animated bounce">500</h1>
<p>
<strong>Internal server error. </strong>
</p>
<p>
{message}
</p>
<p>
&nbsp;<small id="hide" class="hide">Alright. You can stop clicking... it's not going to make the site come back sooner!</small>
</p>
</div>
</div>
</body>
</html>

View File

@@ -2,12 +2,147 @@
<head>
<title>Excessive Load Warning</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="/assets/5xx.css" />
<style type="text/css">
body {
background: #00A9EA;
color: white;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
text-align: center;
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
}
h1 {
font-size: 250px;
color: #fff;
opacity: 0.5;
margin: 10px;
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
}
p {
font-size: 20px;
}
p strong {
font-size: 28px;
}
@media (max-width: 640px) {
h1 {
font-size: 125px;
}
p {
font-size: 16px;
}
p strong {
font-size: 20px;
}
}
.center {
position: relative;
top: 50%;
-webkit-transform: translateY(50%);
-ms-transform: translateY(50%);
transform: translateY(50%);
}
@-webkit-keyframes bounce {
0%, 20%, 53%, 80%, 100% {
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
.bounce {
-webkit-animation-name: bounce;
animation-name: bounce;
-webkit-transform-origin: center bottom;
-ms-transform-origin: center bottom;
transform-origin: center bottom;
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
.hide {
display: none;
}
</style>
<script type="text/javascript">
window.onload = function() {
let count = 0;
const bounce = document.getElementById('click-me');
var count = 0,
bounce = document.getElementById('click-me');
bounce.onclick = function() {
count++;
bounce.className = '';

View File

@@ -1,135 +0,0 @@
body {
background: #00A9EA;
color: white;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
text-align: center;
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
}
h1 {
font-size: 250px;
color: #fff;
opacity: 0.5;
margin: 10px;
cursor: pointer;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
}
p {
font-size: 20px;
}
p strong {
font-size: 28px;
}
@media (max-width: 640px) {
h1 {
font-size: 125px;
}
p {
font-size: 16px;
}
p strong {
font-size: 20px;
}
}
.center {
position: relative;
top: 50%;
-webkit-transform: translateY(50%);
-ms-transform: translateY(50%);
transform: translateY(50%);
}
@-webkit-keyframes bounce {
0%, 20%, 53%, 80%, 100% {
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
@keyframes bounce {
0%, 20%, 53%, 80%, 100% {
-webkit-transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
transition-timing-function: cubic-bezier(0.215, 0.610, 0.355, 1.000);
-webkit-transform: translate3d(0,0,0);
transform: translate3d(0,0,0);
}
40%, 43% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -30px, 0);
transform: translate3d(0, -30px, 0);
}
70% {
-webkit-transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
transition-timing-function: cubic-bezier(0.755, 0.050, 0.855, 0.060);
-webkit-transform: translate3d(0, -15px, 0);
transform: translate3d(0, -15px, 0);
}
90% {
-webkit-transform: translate3d(0,-4px,0);
transform: translate3d(0,-4px,0);
}
}
.bounce {
-webkit-animation-name: bounce;
animation-name: bounce;
-webkit-transform-origin: center bottom;
-ms-transform-origin: center bottom;
transform-origin: center bottom;
}
.animated {
-webkit-animation-duration: 1s;
animation-duration: 1s;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
}
.animated.infinite {
-webkit-animation-iteration-count: infinite;
animation-iteration-count: infinite;
}
.animated.hinge {
-webkit-animation-duration: 2s;
animation-duration: 2s;
}
.hide {
display: none;
}

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "رقم الصفحة غير صحيح ، يجب أن يكون بين %1 و %2 .",
"username-taken": "اسم المستخدم مأخوذ",
"email-taken": "Email address is already taken.",
"email-taken": "البريد الالكتروني مأخوذ",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Вече има папка с това име",
"invalid-pagination-value": "Грешен номер на странициране, трябва да бъде между %1 и %2",
"username-taken": "Потребителското име е заето",
"email-taken": "Email address is already taken.",
"email-taken": "Е-пощата е заета",
"email-nochange": "Въведената е-поща е същата като съществуващата.",
"email-invited": "На тази е-поща вече е била изпратена покана",
"email-not-confirmed": "Публикуването в някои категории и теми ще бъде възможно едва след като е-пощата Ви бъде потвърдена. Щръкнете тук, за да Ви изпратим е-писмо за потвърждение.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "ইউজারনেম আগেই ব্যবহৃত",
"email-taken": "Email address is already taken.",
"email-taken": "ইমেইল আগেই ব্যবহৃত",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Neplatná hodnota stránkování, musí být alespoň %1 a nejvýše %2",
"username-taken": "Uživatelské jméno je již použito",
"email-taken": "Email address is already taken.",
"email-taken": "Tento e-mail je již použit",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Ugyldig side værdi, skal mindst være %1 og maks. %2",
"username-taken": "Brugernavn optaget",
"email-taken": "Email address is already taken.",
"email-taken": "Emailadresse allerede i brug",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Ordner existiert",
"invalid-pagination-value": "Ungültige Seitennummerierung, muss mindestens %1 und maximal %2 sein",
"username-taken": "Der Benutzername ist bereits vergeben",
"email-taken": "Email address is already taken.",
"email-taken": "E-Mail-Adresse vergeben",
"email-nochange": "Die eingegebene E-Mail ist die gleiche wie die bereits hinterlegte E-Mail.",
"email-invited": "E-Mail wurde bereits eingeladen",
"email-not-confirmed": "Das Schreiben von Beiträgen in einigen Kategorien oder Themen ist erst möglich, wenn Ihre E-Mail bestätigt wurde. Bitte klicken Sie hier, um eine Bestätigungs-E-Mail zu senden.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Το όνομα χρήστη είναι πιασμένο",
"email-taken": "Email address is already taken.",
"email-taken": "Το email είναι πιασμένο",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -37,7 +37,7 @@
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Username taken",
"email-taken": "Email address is already taken.",
"email-taken": "Email taken",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Username taken",
"email-taken": "Email address is already taken.",
"email-taken": "Email taken",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Username taken",
"email-taken": "Email address is already taken.",
"email-taken": "Email taken",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Número de página inválido, debe estar entre %1 y %2",
"username-taken": "Nombre de usuario ocupado",
"email-taken": "Email address is already taken.",
"email-taken": "Correo electrónico ocupado",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Väär lehekülje numeratsioon, peab olema vähemalt %1 ja kõige rohkem %2",
"username-taken": "Kasutajanimi on juba võetud",
"email-taken": "Email address is already taken.",
"email-taken": "Email on võetud",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "ارزش گذاری صفحه نامعتبر است، کمترین مقدار <strong>%1</strong> و بیشترین مقدار <strong>%2</strong> باید باشد",
"username-taken": "این نام کاربری گرفته شده است.",
"email-taken": "Email address is already taken.",
"email-taken": "این ایمیل گرفته شده است.",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "ایمیل قبلا دعوت شده‌است",
"email-not-confirmed": "پس از تایید ایمیل شما، ارسال در برخی دسته ها یا موضوعات فعال می شود، لطفاً برای ارسال ایمیل تایید اینجا را کلیک کنید.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Käyttäjänimi varattu",
"email-taken": "Email address is already taken.",
"email-taken": "Sähköpostiosoite varattu",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Le dossier existe",
"invalid-pagination-value": "Valeur de pagination invalide. Celle-ci doit être comprise entre %1 et %2.",
"username-taken": "Ce nom d'utilisateur est déjà pris",
"email-taken": "Email address is already taken.",
"email-taken": "E-mail déjà utilisé",
"email-nochange": "Le mail saisi est déjà enregistré.",
"email-invited": "Cet utilisateur a déjà été invité.",
"email-not-confirmed": "La publication dans certaines catégories ou sujets sera activée après confirmation de l'e-mail, veuillez cliquer ici pour envoyer un e-mail de confirmation.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Valor de paxinación incorreto, ten que estar entre %1 e %2",
"username-taken": "Nome de usuario en uso",
"email-taken": "Email address is already taken.",
"email-taken": "Enderezo electrónico en uso",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "התיקיה קיימת",
"invalid-pagination-value": "ערך דף לא חוקי, חייב להיות לפחות %1 ולא מעל %2",
"username-taken": "שם משתמש תפוס",
"email-taken": "Email address is already taken.",
"email-taken": "כתובת דוא\"ל תפוסה",
"email-nochange": "כתובת דוא\"ל שהוזן זהה לדוא\"ל שנמצא כבר",
"email-invited": "נשלחה כבר הזמנה לדוא\"ל זה",
"email-not-confirmed": "פרסום בקטגוריות או בנושאים מסוימים מופעל רק לאחר אישור הדוא\"ל שלכם, אנא לחצו כאן כדי לשלוח אימות לדוא\"ל שלכם.",

View File

@@ -48,7 +48,7 @@
"user-flagged-user-multiple": "<strong>%1</strong>, <strong>%2</strong> ו-%3 אחרים דיווחו על פרופיל משתמש (%4)",
"user-posted-to": "<strong>%1</strong> פרסם תגובה ל: <strong>%2</strong>",
"user-posted-to-dual": "<strong>%1</strong> ו<strong>%2</strong> הגיבו ל: <strong>%3</strong>",
"user-posted-to-triple": "<strong>%1</strong>, <strong>%2</strong> ו<strong>%3</strong> הגיבו ל: <strong>%4</strong>",
"user-posted-to-triple": "<strong>%1</strong>, <strong>%2</strong> ו<strong>3%</strong> הגיבו ל: <strong>%4</strong>",
"user-posted-to-multiple": "<strong>%1</strong>, <strong>%2</strong> ו-%3 אחרים הגיבו ל: <strong>%4</strong>",
"user-posted-topic": "<strong>%1</strong> העלה נושא חדש: <strong>%2</strong>",
"user-edited-post": "<strong>%1</strong> ערך פוסט ב: <strong>%2</strong>",
@@ -59,7 +59,7 @@
"user-posted-topic-in-category": "<strong>%1</strong> פרסם נושא חדש ב<strong>%2</strong>",
"user-started-following-you": "<strong>%1</strong> התחיל לעקוב אחריך.",
"user-started-following-you-dual": "<strong>%1</strong> ו-<strong>%2</strong> התחילו לעקוב אחריך.",
"user-started-following-you-triple": "<strong>%1</strong>, <strong>%2</strong> ו<strong>%3</strong> התחילו לעקוב אחריך.",
"user-started-following-you-triple": "<strong>%1</strong>, <strong>%2</strong> ו<strong>3%</strong> התחילו לעקוב אחריך.",
"user-started-following-you-multiple": "<strong>%1</strong>, <strong>%2</strong> ו-%3 אחרים התחילו לעקוב אחריך.",
"new-register": "<strong>%1</strong> שלח בקשת הרשמה.",
"new-register-multiple": "ישנן <strong>%1</strong> בקשות הרשמה שמחכות לבדיקה.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Netočno numeriranje stranica, mora biti %1 ili %2",
"username-taken": "Korisničko ime je zauzeto",
"email-taken": "Email address is already taken.",
"email-taken": "Email je zauzet",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Mappa létezik",
"invalid-pagination-value": "Érvénytelen lapozási érték, legalább %1 kell lennie és legfeljebb %2 -nak/nek",
"username-taken": "Foglalt felhasználónév",
"email-taken": "Email address is already taken.",
"email-taken": "Foglalt e-mail",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Ez az email cím már meg lett hívva",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -8,7 +8,7 @@
"edit-privileges": "Խմբագրել արտոնությունները",
"select-clear-all": "Ընտրել/Մաքրել բոլորը",
"chat": "Զրույց",
"chat-with-privileged": "Խոսել առավելություն ունեցողի հետ",
"chat-with-privileged": "Chat with Privileged",
"upload-images": "Վերբեռնեք պատկերներ",
"upload-files": "Վերբեռնել Ֆայլեր",
"signature": "Ստորագրություն",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Թղթապանակ գոյություն ունի",
"invalid-pagination-value": "Էջավորման անվավեր արժեքը, պետք է լինի առնվազն %1 և առավելագույնը %2",
"username-taken": "Օգտագործողի անունը վերցված է",
"email-taken": "Email address is already taken.",
"email-taken": "Էլփոստը վերցված է",
"email-nochange": "Մուտքագրված էլփոստը նույնն է, ինչ ֆայլում արդեն առկա էլ.",
"email-invited": "Էլփոստն արդեն հրավիրված էր",
"email-not-confirmed": "Որոշ կատեգորիաներում կամ թեմաներում հրապարակելը միացված կլինի, երբ ձեր էլփոստը հաստատվի, խնդրում ենք սեղմել այստեղ՝ հաստատող էլփոստը ուղարկելու համար:",

View File

@@ -178,7 +178,7 @@
"sessions.description": "Այս էջը թույլ է տալիս դիտել ցանկացած ակտիվ սեանս այս ֆորումում և անհրաժեշտության դեպքում չեղարկել դրանք: Դուք կարող եք չեղարկել ձեր սեփական սեանսը՝ դուրս գալով ձեր հաշվից:",
"revoke-session": "Չեղյալ համարել նիստը",
"browser-version-on-platform": "%1 %2 %3-ում",
"consent.title": "Ձեր Իրավունքները և Համաձայնությունը",
"consent.title": "Your Rights &amp; Consent",
"consent.lead": "Այս համայնքի ֆորումը հավաքում և մշակում է ձեր անձնական տվյալները:",
"consent.intro": "Մենք օգտագործում ենք այս տեղեկատվությունը խստորեն այս համայնքում ձեր փորձառությունն անհատականացնելու, ինչպես նաև ձեր կատարած գրառումները ձեր օգտատիրոջ հաշվին կապելու համար: Գրանցման քայլի ընթացքում ձեզանից պահանջվել է տրամադրել օգտատիրոջ անուն և էլ.փոստի հասցե, դուք կարող եք նաև լրացուցիչ տեղեկություններ տրամադրել այս կայքում ձեր օգտատիրոջ պրոֆիլը լրացնելու համար: Մենք պահպանում ենք այս տեղեկատվությունը ձեր օգտատիրոջ հաշվի ողջ կյանքի ընթացքում, և դուք կարող եք հետ վերցնել համաձայնությունը: ցանկացած պահի ջնջելով ձեր հաշիվը: Ցանկացած ժամանակ դուք կարող եք պահանջել ձեր ներդրման պատճենը այս կայքում՝ ձեր իրավունքների և amp; Համաձայնության էջ: Եթե ունեք հարցեր կամ մտահոգություններ, խորհուրդ ենք տալիս դիմել այս ֆորումի ադմինիստրատիվ թիմին:",
"consent.email-intro": "Երբեմն, մենք կարող ենք նամակներ ուղարկել ձեր գրանցված էլ․ հասցեին՝ թարմացումներ տրամադրելու և/կամ ձեզ ծանուցելու նոր գործունեության մասին, որը վերաբերում է ձեզ: Դուք կարող եք հարմարեցնել համայնքի ամփոփման հաճախականությունը (ներառյալ այն ուղղակիորեն անջատելը), ինչպես նաև ընտրել, թե ինչ տեսակի ծանուցումներ պետք է ստանալ էլփոստի միջոցով՝ ձեր օգտվողի կարգավորումների էջի միջոցով:",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Nomor pagination tidak valid, minimal %1 dan maksimal %2",
"username-taken": "Username sudah terdaftar",
"email-taken": "Email address is already taken.",
"email-taken": "Email sudah terdaftar",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "La cartella esiste",
"invalid-pagination-value": "Valore di impaginazione non valido, deve essere almeno %1 ed al massimo %2",
"username-taken": "Nome utente già esistente",
"email-taken": "Email address is already taken.",
"email-taken": "Email già esistente",
"email-nochange": "L'email inserita è la stessa dell'email già presente in archivio.",
"email-invited": "L'email è già stata invitata",
"email-not-confirmed": "Sarai abilitato a postare in alcune categorie o discussioni una volta che la tua email sarà confermata, per favore clicca qui per inviare una email di conferma.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "無効なページネーション値です。%1 から%2の値でなければありません。",
"username-taken": "ユーザー名は既に使われています",
"email-taken": "Email address is already taken.",
"email-taken": "メールアドレスは既に使われています",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "폴더가 이미 존재합니다.",
"invalid-pagination-value": "올바르지 않은 페이지 값입니다. 최소 %1에서 최대 2% 사이로 설정해야 합니다.",
"username-taken": "이미 사용 중인 사용자명입니다.",
"email-taken": "Email address is already taken.",
"email-taken": "이미 사용 중인 이메일입니다.",
"email-nochange": "입력한 전자 메일이 이미 등록되어 있는 전자 메일과 동일합니다.",
"email-invited": "해당 이메일의 사용자는 이미 초대되었습니다.",
"email-not-confirmed": "이메일 인증이 완료된 후 카테고리나 화제에 새로운 포스트를 작성할 수 있습니다. 여기를 눌러 인증 메일을 다시 발송할 수 있습니다.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Bloga puslapių išdėstymo reikšmė. Ji turėtų būti ne mažesnė nei %1 ir ne didesnė nei %2",
"username-taken": "Vartotojo vardas jau užimtas",
"email-taken": "Email address is already taken.",
"email-taken": "El. pašto adresas jau užimtas",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Nederīgs vienību skaits, ir jābūt vismaz %1 un ne vairāk kā %2",
"username-taken": "Lietotājvārds jau izmantots",
"email-taken": "Email address is already taken.",
"email-taken": "E-pasta adrese jau izmantota",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Nombor halaman tidak sah, mesti tidak kurang dari %1 dan tidak lebih dari %2",
"username-taken": "Nama pengguna telah digunakan",
"email-taken": "Email address is already taken.",
"email-taken": "Emel telah digunakan",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Mappen eksisterer",
"invalid-pagination-value": "Ugyldig sidetall, må være minst %1 og maks %2",
"username-taken": "Brukernavn opptatt",
"email-taken": "Email address is already taken.",
"email-taken": "E-post opptatt",
"email-nochange": "E-posten som er angitt er den samme e-posten som allerede er lagret.",
"email-invited": "E-post har allerede fått invitasjon",
"email-not-confirmed": "Posting i enkelte kategorier eller emner blir aktivert når e-posten din er bekreftet. Klikk her for å sende en bekreftelses-e-post.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Ongeldig paginering waarde. De waarde moet op z'n minst %1 zijn en niet hoger dan %2 zijn.",
"username-taken": "Gebruikersnaam is al in gebruik",
"email-taken": "Email address is already taken.",
"email-taken": "E-mailadres is al in gebruik",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "E-mail was reeds uitgenodigd",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder istnieje",
"invalid-pagination-value": "Błędna wartość paginacji, zakres od %1 do %2",
"username-taken": "Login zajęty",
"email-taken": "Email address is already taken.",
"email-taken": "Email zajęty",
"email-nochange": "Podany email jest taki sam jak ten już zapisany.",
"email-invited": "Ten adres email otrzymał już zaproszenie",
"email-not-confirmed": "Pisanie w niektórych kategoriach albo tematach jest dozwolone wtedy gdy Twój adres email został zweryfikowany, proszę kliknij tutaj aby wysłać potwierdzający email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Valor de paginação inválido, precisa ser no mínimo %1 e no máximo %2",
"username-taken": "Nome de usuário já existe",
"email-taken": "Email address is already taken.",
"email-taken": "Email já cadastrado",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "O email já foi convidado",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Valor de paginação errado, deve ser no mínimo %1 e no máximo %2",
"username-taken": "Nome de utilizar já utilizado",
"email-taken": "Email address is already taken.",
"email-taken": "E-mail já utilizado",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Numele de utilizator este deja folosit",
"email-taken": "Email address is already taken.",
"email-taken": "Adresa de email este deja folostă",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Папка существует",
"invalid-pagination-value": "Неправильно указан номер страницы. Значение должно быть в диапазоне от %1 до %2",
"username-taken": "Это имя пользователя уже занято",
"email-taken": "Email address is already taken.",
"email-taken": "Пользователь с таким адресом электронной почты уже зарегистрирован",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Электронная почта уже была приглашена",
"email-not-confirmed": "Вы не сможете отправлять сообщения, пока ваш адрес электронной почты не подтверждён. Пожалуйста, нажмите здесь, чтобы подтвердить его.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Izina ryarafashwe mbere",
"email-taken": "Email address is already taken.",
"email-taken": "Email yarafashwe mbere",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"username-taken": "Username taken",
"email-taken": "Email address is already taken.",
"email-taken": "Email taken",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Neplatná stránkovania hodnota, musí byť najmenej %1 a najviac %2",
"username-taken": "Užívateľské meno je už obsadené",
"email-taken": "Email address is already taken.",
"email-taken": "Tento e-mail je už obsadený",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Napačna vrednost za številčenje strani. Vrednost mora biti najmanj %1 in največ %2.",
"username-taken": "Uporabniško ime je že zasedeno.",
"email-taken": "Email address is already taken.",
"email-taken": "E-poštni naslov je že zaseden.",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Ky dokument ekziston",
"invalid-pagination-value": "Vlera e pasaktë e faqes, duhet të jetë së paku %1 dhe maksimumi %2",
"username-taken": "Username është i zënë",
"email-taken": "Email address is already taken.",
"email-taken": "Email-i është i zënë",
"email-nochange": "Email-i i futur është i njëjtë me emailin ekzistues në sistem.",
"email-invited": "Email-i është ftuar më herët",
"email-not-confirmed": "Postimi në disa kategori ose tema aktivizohet pasi emaili juaj të konfirmohet, ju lutemi klikoni këtu për të dërguar një email konfirmimi.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Фасцикла постоји",
"invalid-pagination-value": "Неважећа вредност приликом нумерисања страница, мора бити најмање %1 а највише %2",
"username-taken": "Корисничко име је заузето",
"email-taken": "Email address is already taken.",
"email-taken": "Адреса е-поште је заузета",
"email-nochange": "Унета е-пошта је иста као е-пошта која је већ у евиденцији.",
"email-invited": "Е-пошта је већ позвана",
"email-not-confirmed": "Објављивање у неким категоријама или темама је омогућено када потврдите вашу е-пошту, кликните овде да бисте послали е-поруку за потврду.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Ogiltigt värde för siduppdelning. Värdet måste vara mellan %1 och %2",
"username-taken": "Användarnamn upptaget",
"email-taken": "Email address is already taken.",
"email-taken": "Epostadress upptagen",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "หมายเลขหน้าไม่ถูกต้อง จำเป็นต้องเป็นตัวเลขอย่างน้อย %1 และอย่างมาก %2",
"username-taken": "ชื่อผู้ใช้นี้มีการใช้แล้ว",
"email-taken": "Email address is already taken.",
"email-taken": "อีเมลนี้มีการใช้แล้ว",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Dosya mevcut",
"invalid-pagination-value": "Geçersiz sayfa numarası girdiniz, en az %1 ve en fazla %2 olabilir",
"username-taken": "Kullanıcı İsmi Alınmış",
"email-taken": "Email address is already taken.",
"email-taken": "E-posta Alınmış",
"email-nochange": "Girdiğiniz e-posta var olan e-posta ile aynı",
"email-invited": "E-posta halihazırda davet edilmiş",
"email-not-confirmed": "Ancak e-postanız onaylandıktan sonra bazı kategorilere veya konulara ileti gönderebilirsiniz; lütfen bir onay e-postası almak için buraya tıklayın.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "Невірне значення сторінки, має бути щонайменше %1 та щонайбільше %2",
"username-taken": "Це ім'я зайняте",
"email-taken": "Email address is already taken.",
"email-taken": "Ця електронна пошта зайнята",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Thư mục tồn tại",
"invalid-pagination-value": "Giá trị phân trang không hợp lệ, tối thiểu phải là %1 và tối đa là %2",
"username-taken": "Tên đăng nhập đã tồn tại",
"email-taken": "Email address is already taken.",
"email-taken": "Email đã được đăng kí",
"email-nochange": "Email đã nhập giống với email đã có trong tệp.",
"email-invited": "Email đã được mời",
"email-not-confirmed": "Đăng trong một số danh mục hoặc chủ đề được bật sau khi email của bạn được xác nhận, vui lòng nhấp vào đây để gửi email xác nhận.",

View File

@@ -32,7 +32,7 @@
"folder-exists": "文件夹已存在",
"invalid-pagination-value": "无效的分页数值,必须介于 %1 和 %2 之间",
"username-taken": "此用户名已被占用",
"email-taken": "Email address is already taken.",
"email-taken": "此电子邮箱已被占用",
"email-nochange": "输入的邮件地址和已存档的邮件地址相同。",
"email-invited": "已通过电子邮件进行邀请",
"email-not-confirmed": "您需要验证您的邮箱后才能在版块或主题中发布帖子,请点击此处以发送验证邮件。",

View File

@@ -32,7 +32,7 @@
"folder-exists": "Folder exists",
"invalid-pagination-value": "無效的分頁數,必須介於 %1 和 %2 之間",
"username-taken": "此使用者名已被使用",
"email-taken": "Email address is already taken.",
"email-taken": "此電子信箱已被使用",
"email-nochange": "The email entered is the same as the email already on file.",
"email-invited": "Email was already invited",
"email-not-confirmed": "Posting in some categories or topics is enabled once your email is confirmed, please click here to send a confirmation email.",

View File

@@ -150,7 +150,7 @@ ajaxify.widgets = { render: render };
if (data) {
let status = parseInt(data.status, 10);
if ([400, 403, 404, 500, 502, 503].includes(status)) {
if ([400, 403, 404, 500, 502, 504].includes(status)) {
if (status === 502 && retry) {
retry = false;
ajaxifyTimer = undefined;

View File

@@ -10,6 +10,7 @@ const isPrerelease = /^v?\d+\.\d+\.\d+-.+$/;
const latestReleaseUrl = 'https://api.github.com/repos/NodeBB/NodeBB/releases/latest';
async function getLatestVersion() {
return '';
const headers = {
Accept: 'application/vnd.github.v3+json',
'User-Agent': encodeURIComponent(`NodeBB Admin Control Panel/${meta.config.title}`),
@@ -18,24 +19,25 @@ async function getLatestVersion() {
if (versionCacheLastModified) {
headers['If-Modified-Since'] = versionCacheLastModified;
}
try {
const { body: latestRelease, response } = await request.get(latestReleaseUrl, {
headers: headers,
timeout: 2000,
});
const { body: latestRelease, response } = await request.get(latestReleaseUrl, {
headers: headers,
timeout: 2000,
});
if (response.statusCode === 304) {
if (!latestRelease || !latestRelease.tag_name) {
throw new Error('[[error:cant-get-latest-release]]');
}
const tagName = latestRelease.tag_name.replace(/^v/, '');
versionCache = tagName;
versionCacheLastModified = response.headers['last-modified'];
return versionCache;
} catch (err) {
if (err.response && err.response.status === 304) {
return versionCache;
}
throw err;
}
if (response.statusCode !== 200) {
throw new Error(response.statusText);
}
if (!latestRelease || !latestRelease.tag_name) {
throw new Error('[[error:cant-get-latest-release]]');
}
const tagName = latestRelease.tag_name.replace(/^v/, '');
versionCache = tagName;
versionCacheLastModified = response.headers['last-modified'];
return versionCache;
}
exports.getLatestVersion = getLatestVersion;

View File

@@ -7,7 +7,7 @@ const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const request = require('../request');
const { paths, pluginNamePattern } = require('../constants');
const pkgInstall = require('./package-install');
@@ -74,11 +74,7 @@ async function getCurrentVersion() {
}
async function getSuggestedModules(nbbVersion, toCheck) {
const request = require('../request');
let { response, body } = await request.get(`https://packages.nodebb.org/api/v1/suggest?version=${nbbVersion}&package[]=${toCheck.join('&package[]=')}`);
if (!response.ok) {
throw new Error(`Unable to get suggested module for NodeBB(${nbbVersion}) ${toCheck.join(',')}`);
}
let { body } = await request.get(`https://packages.nodebb.org/api/v1/suggest?version=${nbbVersion}&package[]=${toCheck.join('&package[]=')}`);
if (!Array.isArray(body) && toCheck.length === 1) {
body = [body];
}

View File

@@ -1,10 +1,8 @@
'use strict';
const fs = require('fs');
const nconf = require('nconf');
const winston = require('winston');
const validator = require('validator');
const path = require('path');
const translator = require('../translator');
const plugins = require('../plugins');
const middleware = require('../middleware');
@@ -56,12 +54,6 @@ exports.handleErrors = async function handleErrors(err, req, res, next) { // esl
controllers['404'].handle404(req, res);
};
const notBuiltHandler = async () => {
let file = await fs.promises.readFile(path.join(__dirname, '../../public/500.html'), { encoding: 'utf-8' });
file = file.replace('{message}', 'Failed to lookup view! Did you run `./nodebb build`?');
return res.type('text/html').send(file);
};
const defaultHandler = async function () {
if (res.headersSent) {
return;
@@ -103,8 +95,6 @@ exports.handleErrors = async function handleErrors(err, req, res, next) { // esl
data.cases[err.code](err, req, res, defaultHandler);
} else if (err.message.startsWith('[[error:no-') && err.message !== '[[error:no-privileges]]') {
notFoundHandler();
} else if (err.message.startsWith('Failed to lookup view')) {
notBuiltHandler();
} else {
await defaultHandler();
}

View File

@@ -39,7 +39,7 @@ module.exports = function (middleware) {
const data = {
site_title: meta.config.title || 'NodeBB',
message: meta.config.maintenanceModeMessage,
message: meta.config.maintenanceModeMessage || '',
};
if (res.locals.isAPI) {

View File

@@ -153,10 +153,8 @@ Plugins.reloadRoutes = async function (params) {
Plugins.get = async function (id) {
const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins/${id}`;
const { response, body } = await request.get(url);
if (!response.ok) {
throw new Error(`[[error:unable-to-load-plugin, ${id}]]`);
}
const { body } = await request.get(url);
let normalised = await Plugins.normalise([body ? body.payload : {}]);
normalised = normalised.filter(plugin => plugin.id === id);
return normalised.length ? normalised[0] : undefined;
@@ -169,10 +167,7 @@ Plugins.list = async function (matching) {
const { version } = require(paths.currentPackage);
const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugins${matching !== false ? `?version=${version}` : ''}`;
try {
const { response, body } = await request.get(url);
if (!response.ok) {
throw new Error(`[[error:unable-to-load-plugins-from-nbbpm]]`);
}
const { body } = await request.get(url);
return await Plugins.normalise(body);
} catch (err) {
winston.error(`Error loading ${url}`, err);
@@ -182,10 +177,7 @@ Plugins.list = async function (matching) {
Plugins.listTrending = async () => {
const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/analytics/top/week`;
const { response, body } = await request.get(url);
if (!response.ok) {
throw new Error(`[[error:unable-to-load-trending-plugins]]`);
}
const { body } = await request.get(url);
return body;
};

View File

@@ -74,10 +74,8 @@ module.exports = function (Plugins) {
};
Plugins.checkWhitelist = async function (id, version) {
const { response, body } = await request.get(`https://packages.nodebb.org/api/v1/plugins/${encodeURIComponent(id)}`);
if (!response.ok) {
throw new Error(`[[error:cant-connect-to-nbbpm]]`);
}
const { body } = await request.get(`https://packages.nodebb.org/api/v1/plugins/${encodeURIComponent(id)}`);
if (body && body.code === 'ok' && (version === 'latest' || body.payload.valid.includes(version))) {
return;
}
@@ -86,10 +84,7 @@ module.exports = function (Plugins) {
};
Plugins.suggest = async function (pluginId, nbbVersion) {
const { response, body } = await request.get(`https://packages.nodebb.org/api/v1/suggest?package=${encodeURIComponent(pluginId)}&version=${encodeURIComponent(nbbVersion)}`);
if (!response.ok) {
throw new Error(`[[error:cant-connect-to-nbbpm]]`);
}
const { body } = await request.get(`https://packages.nodebb.org/api/v1/suggest?package=${encodeURIComponent(pluginId)}&version=${encodeURIComponent(nbbVersion)}`);
return body;
};

View File

@@ -27,7 +27,7 @@ module.exports = function (Plugins) {
const url = `${nconf.get('registry') || 'https://packages.nodebb.org'}/api/v1/plugin/usage`;
try {
const { response, body } = await request.post(url, {
body: {
data: {
id: hash.digest('hex'),
version: pkg.version,
plugins: Plugins.loadedPlugins,
@@ -35,7 +35,7 @@ module.exports = function (Plugins) {
timeout: 5000,
});
if (!response.ok) {
if (response.status !== 200) {
winston.error(`[plugins.submitUsageData] received ${response.status} ${body}`);
}
} catch (err) {

View File

@@ -1,80 +1,50 @@
'use strict';
const axios = require('axios').default;
const { CookieJar } = require('tough-cookie');
const fetchCookie = require('fetch-cookie');
const { wrapper } = require('axios-cookiejar-support');
wrapper(axios);
exports.jar = function () {
return new CookieJar();
};
async function call(url, method, { body, timeout, jar, ...config } = {}) {
let fetchImpl = fetch;
if (jar) {
fetchImpl = fetchCookie(fetch, jar);
}
const opts = {
async function call(url, method, config = {}) {
const result = await axios({
...config,
method,
headers: {
'content-type': 'application/json',
...config.headers,
},
};
if (timeout > 0) {
opts.signal = AbortSignal.timeout(timeout);
}
if (body && ['POST', 'PUT', 'PATCH', 'DEL', 'DELETE'].includes(method)) {
if (opts.headers['content-type'] && opts.headers['content-type'].startsWith('application/json')) {
opts.body = JSON.stringify(body);
} else {
opts.body = body;
}
}
const response = await fetchImpl(url, opts);
const { headers } = response;
const contentType = headers.get('content-type');
const jsonTest = /application\/([a-z]+\+)?json/;
const isJSON = contentType && jsonTest.test(contentType);
let respBody = await response.text();
if (isJSON && respBody) {
try {
respBody = JSON.parse(respBody);
} catch (err) {
throw new Error('invalid json in response body', url);
}
}
url: url,
});
return {
body: respBody,
body: result.data,
response: {
ok: response.ok,
status: response.status,
statusCode: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
status: result.status,
statusCode: result.status,
statusText: result.statusText,
headers: result.headers,
},
};
}
/*
const { body, response } = await request.get('someurl?foo=1&baz=2')
or
const { body, response } = await request.get('someurl', { params: { foo:1, baz: 2 } })
*/
exports.get = async (url, config) => call(url, 'GET', config);
exports.get = async (url, config) => call(url, 'get', config);
exports.head = async (url, config) => call(url, 'HEAD', config);
exports.del = async (url, config) => call(url, 'DELETE', config);
exports.head = async (url, config) => call(url, 'head', config);
exports.del = async (url, config) => call(url, 'delete', config);
exports.delete = exports.del;
exports.options = async (url, config) => call(url, 'OPTIONS', config);
exports.options = async (url, config) => call(url, 'delete', config);
/*
const { body, response } = await request.post('someurl', { body: { foo: 1, baz: 2}})
const { body, response } = await request.post('someurl', { data: { foo: 1, baz: 2}})
*/
exports.post = async (url, config) => call(url, 'POST', config);
exports.put = async (url, config) => call(url, 'PUT', config);
exports.patch = async (url, config) => call(url, 'PATCH', config);
exports.post = async (url, config) => call(url, 'post', config);
exports.put = async (url, config) => call(url, 'put', config);
exports.patch = async (url, config) => call(url, 'patch', config);

View File

@@ -13,9 +13,6 @@ module.exports = function (app, middleware, controllers) {
app.get('/css/previews/:theme', controllers.admin.themes.get);
app.get('/osd.xml', controllers.osd.handle);
app.get('/service-worker.js', (req, res) => {
res.status(200)
.type('application/javascript')
.set('Service-Worker-Allowed', `${nconf.get('relative_path')}/`)
.sendFile(path.join(__dirname, '../../build/public/src/service-worker.js'));
res.status(200).type('application/javascript').set('Service-Worker-Allowed', `${nconf.get('relative_path')}/`).sendFile(path.join(__dirname, '../../public/src/service-worker.js'));
});
};

View File

@@ -27,7 +27,6 @@ module.exports = function (SocketPosts) {
canDelete: privileges.posts.canDelete(data.pid, socket.uid),
canPurge: privileges.posts.canPurge(data.pid, socket.uid),
canFlag: privileges.posts.canFlag(data.pid, socket.uid),
canViewHistory: privileges.posts.can('posts:history', data.pid, socket.uid),
flagged: flags.exists('post', data.pid, socket.uid), // specifically, whether THIS calling user flagged
bookmarked: posts.hasBookmarked(data.pid, socket.uid),
postSharing: social.getActivePostSharing(),
@@ -47,7 +46,7 @@ module.exports = function (SocketPosts) {
postData.display_move_tools = results.isAdmin || results.isModerator;
postData.display_change_owner_tools = results.isAdmin || results.isModerator;
postData.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !postData.selfPost;
postData.display_history = results.history && results.canViewHistory;
postData.display_history = results.history;
postData.flags = {
flagId: parseInt(results.posts.flagId, 10) || null,
can: results.canFlag.flag,

View File

@@ -493,13 +493,13 @@ describe('API', async () => {
try {
if (type === 'json') {
const searchParams = new URLSearchParams(qs);
result = await request[method](`${url}?${searchParams}`, {
result = await request[method](url, {
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null, // don't throw on non-200 (e.g. 302)
headers: headers,
body: body,
params: qs,
data: body,
});
} else if (type === 'form') {
result = await helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken);

View File

@@ -99,7 +99,7 @@ describe('authentication', () => {
const { body } = await request.post(`${nconf.get('url')}/register`, {
jar,
body: {
data: {
email: 'admin@nodebb.org',
username: 'admin',
password: 'adminpwd',
@@ -133,6 +133,7 @@ describe('authentication', () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/me`, {
jar: jar,
validateStatus: null,
});
assert.equal(response.statusCode, 401);
assert.strictEqual(body.status.code, 'not-authorised');
@@ -167,9 +168,9 @@ describe('authentication', () => {
function getCookieExpiry(response) {
const { headers } = response;
assert(headers['set-cookie']);
assert.strictEqual(headers['set-cookie'].includes('Expires'), true);
assert.strictEqual(headers['set-cookie'][0].includes('Expires'), true);
const values = headers['set-cookie'].split(';');
const values = headers['set-cookie'][0].split(';');
return values.reduce((memo, cur) => {
if (!memo) {
const [name, value] = cur.split('=');
@@ -205,7 +206,7 @@ describe('authentication', () => {
assert(response.headers);
assert(response.headers['set-cookie']);
assert.strictEqual(response.headers['set-cookie'].includes('Expires'), false);
assert.strictEqual(response.headers['set-cookie'][0].includes('Expires'), false);
});
it('should set a different expiry if sessionDuration is set', async () => {
@@ -271,11 +272,12 @@ describe('authentication', () => {
const csrf_token = await helpers.getCsrfToken(jar);
const { response } = await request.post(`${nconf.get('url')}/login`, {
body: {
data: {
username: 'regular',
password: 'regularpwd',
},
jar: jar,
validateStatus: () => true,
headers: {
'x-csrf-token': csrf_token,
'x-forwarded-for': '<script>alert("xss")</script>',
@@ -316,7 +318,7 @@ describe('authentication', () => {
});
it('should fail to login if password is longer than 4096', async () => {
let longPassword = '';
let longPassword;
for (let i = 0; i < 5000; i++) {
longPassword += 'a';
}
@@ -513,7 +515,10 @@ describe('authentication', () => {
});
it('should fail with invalid token', async () => {
const { response, body } = await helpers.request('get', `/api/self?_uid${newUid}`, {
const { response, body } = await helpers.request('get', `/api/self`, {
data: {
_uid: newUid,
},
jar: jar,
headers: {
Authorization: `Bearer sdfhaskfdja-jahfdaksdf`,
@@ -525,6 +530,7 @@ describe('authentication', () => {
it('should use a token tied to an uid', async () => {
const { response, body } = await helpers.request('get', `/api/self`, {
json: true,
headers: {
Authorization: `Bearer ${userToken}`,
},
@@ -536,6 +542,8 @@ describe('authentication', () => {
it('should fail if _uid is not passed in with master token', async () => {
const { response, body } = await helpers.request('get', `/api/self`, {
data: {},
json: true,
headers: {
Authorization: `Bearer ${masterToken}`,
},
@@ -546,7 +554,11 @@ describe('authentication', () => {
});
it('should use master api token and _uid', async () => {
const { response, body } = await helpers.request('get', `/api/self?_uid=${newUid}`, {
const { response, body } = await helpers.request('get', `/api/self`, {
data: {
_uid: newUid,
},
json: true,
headers: {
Authorization: `Bearer ${masterToken}`,
},

View File

@@ -77,7 +77,7 @@ describe('Categories', () => {
});
it('should load a category route', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/category/${categoryObj.cid}/test-category`);
const { response, body } = await request.get(`${nconf.get('url')}/api/category/${categoryObj.cid}/test-category`, { json: true });
assert.equal(response.statusCode, 200);
assert.equal(body.name, 'Test Category &amp; NodeBB');
assert(body);

View File

@@ -69,6 +69,7 @@ describe('Admin Controllers', () => {
({ jar } = await helpers.loginUser('admin', 'barbar'));
const { response, body } = await request.get(`${nconf.get('url')}/admin`, {
jar: jar,
validateStatus: null,
});
assert.equal(response.statusCode, 403);
@@ -165,7 +166,7 @@ describe('Admin Controllers', () => {
});
it('should 404 for edit/email page if user does not exist', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/user/doesnotexist/edit/email`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/user/doesnotexist/edit/email`, { jar: jar, validateStatus: null });
assert.equal(response.statusCode, 404);
});
@@ -242,7 +243,9 @@ describe('Admin Controllers', () => {
});
it('should 404 if users is not privileged', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/registration-queue`);
const { response, body } = await request.get(`${nconf.get('url')}/api/registration-queue`, {
validateStatus: null,
});
assert.equal(response.statusCode, 404);
assert(body);
});
@@ -278,7 +281,10 @@ describe('Admin Controllers', () => {
});
it('should return 403 if no referer', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/groups/administrators/csv`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/groups/administrators/csv`, {
jar,
validateStatus: null,
});
assert.equal(response.statusCode, 403);
assert.equal(body, '[[error:invalid-origin]]');
});
@@ -286,6 +292,7 @@ describe('Admin Controllers', () => {
it('should return 403 if referer is not /api/admin/groups/administrators/csv', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/groups/administrators/csv`, {
jar: jar,
validateStatus: null,
headers: {
referer: '/topic/1/test',
},
@@ -318,13 +325,16 @@ describe('Admin Controllers', () => {
});
it('should load /api/admin/advanced/cache/dump and 404 with no query param', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/cache/dump`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/cache/dump`, {
jar,
validateStatus: null,
});
assert.equal(response.statusCode, 404);
assert(body);
});
it('should load /api/admin/advanced/cache/dump', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/cache/dump?name=post`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/admin/advanced/cache/dump?name=post`, { jar: jar });
assert.equal(response.statusCode, 200);
assert(body);
});
@@ -447,7 +457,9 @@ describe('Admin Controllers', () => {
});
it('/post-queue should 404 for regular user', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/post-queue`);
const { response, body } = await request.get(`${nconf.get('url')}/api/post-queue`, {
validateStatus: null,
});
assert(body);
assert.equal(response.statusCode, 404);
});
@@ -459,7 +471,9 @@ describe('Admin Controllers', () => {
});
it('/ip-blacklist should 404 for regular user', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/ip-blacklist`);
const { response, body } = await request.get(`${nconf.get('url')}/api/ip-blacklist`, {
validateStatus: null,
});
assert(body);
assert.equal(response.statusCode, 404);
});
@@ -500,7 +514,9 @@ describe('Admin Controllers', () => {
});
it('should error with no privileges', async () => {
const { body } = await request.get(`${nconf.get('url')}/api/flags`);
const { body } = await request.get(`${nconf.get('url')}/api/flags`, {
validateStatus: null,
});
assert.deepStrictEqual(body, {
status: {
@@ -525,6 +541,7 @@ describe('Admin Controllers', () => {
headers: {
Accept: 'text/html, application/json',
},
validateStatus: null,
});
assert.strictEqual(response.statusCode, 404);
});
@@ -532,7 +549,7 @@ describe('Admin Controllers', () => {
it('should error when you attempt to flag a privileged user\'s post', async () => {
const { response, body } = await helpers.request('post', '/api/v3/flags', {
jar: regularJar,
body: {
data: {
id: pid,
type: 'post',
reason: 'spam',
@@ -548,7 +565,7 @@ describe('Admin Controllers', () => {
meta.config['min:rep:flag'] = 1000;
const { response, body } = await helpers.request('post', '/api/v3/flags', {
jar: regularJar,
body: {
data: {
id: regularPid,
type: 'post',
reason: 'spam',
@@ -566,7 +583,7 @@ describe('Admin Controllers', () => {
meta.config['min:rep:flag'] = 0;
await helpers.request('post', '/api/v3/flags', {
jar: regularJar,
body: {
data: {
id: regularPid,
type: 'post',
reason: 'spam',
@@ -619,7 +636,9 @@ describe('Admin Controllers', () => {
describe('admin page privileges', () => {
let uid;
const privileges = require('../src/privileges');
const requestOpts = {};
const requestOpts = {
validateStatus: null,
};
before(async () => {
uid = await user.create({ username: 'regularjoe', password: 'barbar' });
requestOpts.jar = (await helpers.loginUser('regularjoe', 'barbar')).jar;

View File

@@ -191,7 +191,7 @@ describe('Controllers', () => {
it('should 404 if custom does not exist', async () => {
await meta.configs.set('homePageRoute', 'this-route-does-not-exist');
const { response, body } = await request.get(nconf.get('url'));
const { response, body } = await request.get(nconf.get('url'), { validateStatus: null });
assert.equal(response.statusCode, 404);
assert(body);
});
@@ -224,7 +224,9 @@ describe('Controllers', () => {
const baseUrl = nconf.get('url');
testRoutes.forEach((route) => {
it(route.it, async () => {
const { response, body } = await request.get(`${baseUrl}/${route.url}`);
const { response, body } = await request.get(`${baseUrl}/${route.url}`, {
validateStatus: null,
});
assert.equal(response.statusCode, route.status || 200);
if (route.body) {
assert.strictEqual(String(body), route.body);
@@ -236,15 +238,17 @@ describe('Controllers', () => {
});
it('should load /register/complete', async () => {
const data = {
username: 'interstitial',
password: '123456',
'password-confirm': '123456',
email: 'test@me.com',
};
const jar = request.jar();
const csrf_token = await helpers.getCsrfToken(jar);
const { response, body } = await request.post(`${nconf.get('url')}/register`, {
body: {
username: 'interstitial',
password: '123456',
'password-confirm': '123456',
email: 'test@me.com',
},
data,
jar,
headers: {
'x-csrf-token': csrf_token,
@@ -293,12 +297,12 @@ describe('Controllers', () => {
it('email interstitial should still apply if empty email entered and requireEmailAddress is enabled', async () => {
const { response: res } = await request.post(`${nconf.get('url')}/register/complete`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
headers: {
'x-csrf-token': token,
},
body: {
data: {
email: '',
},
});
@@ -504,6 +508,7 @@ describe('Controllers', () => {
async function abortInterstitial() {
await request.post(`${nconf.get('url')}/register/abort`, {
jar,
validateStatus: null,
headers: {
'x-csrf-token': token,
},
@@ -515,12 +520,12 @@ describe('Controllers', () => {
const { response } = await request.post(`${nconf.get('url')}/register/complete`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
headers: {
'x-csrf-token': token,
},
body: {
data: {
email: `${utils.generateUUID().slice(0, 10)}@example.org`,
gdpr_agree_data: 'on',
gdpr_agree_email: 'on',
@@ -535,7 +540,8 @@ describe('Controllers', () => {
it('should allow access to regular resources after an email is entered, even if unconfirmed', async () => {
const { response } = await request.get(`${nconf.get('url')}/recent`, {
jar,
maxRedirect: 0,
maxRedirects: 0,
validateStatus: null,
});
assert.strictEqual(response.statusCode, 200);
@@ -548,8 +554,8 @@ describe('Controllers', () => {
await privileges.categories.give(['groups:read'], cid, ['verified-users']);
const { response } = await request.get(`${nconf.get('url')}/category/${cid}/${slugify(name)}`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
});
assert.strictEqual(response.statusCode, 307);
@@ -564,8 +570,8 @@ describe('Controllers', () => {
await privileges.categories.give(['groups:topics:read'], cid, 'verified-users');
const { response } = await request.get(`${nconf.get('url')}/category/${cid}/${slugify(name)}`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
});
assert.strictEqual(response.statusCode, 200);
@@ -575,8 +581,8 @@ describe('Controllers', () => {
const { topicData } = await topics.post({ uid, cid, title, content: utils.generateUUID() });
const { response: res2 } = await request.get(`${nconf.get('url')}/topic/${topicData.tid}/${slugify(title)}`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
});
assert.strictEqual(res2.statusCode, 307);
assert.strictEqual(res2.headers.location, `${nconf.get('relative_path')}/register/complete`);
@@ -601,12 +607,12 @@ describe('Controllers', () => {
it('registration should succeed once gdpr prompts are agreed to', async () => {
const { response } = await request.post(`${nconf.get('url')}/register/complete`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
headers: {
'x-csrf-token': token,
},
body: {
data: {
gdpr_agree_data: 'on',
gdpr_agree_email: 'on',
},
@@ -632,15 +638,15 @@ describe('Controllers', () => {
it('should terminate the session and send user back to index if interstitials remain', async () => {
const { response } = await request.post(`${nconf.get('url')}/register/abort`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
headers: {
'x-csrf-token': token,
},
});
assert.strictEqual(response.statusCode, 302);
assert.strictEqual(response.headers['set-cookie'], `express.sid=; Path=${nconf.get('relative_path') || '/'}; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`);
assert.strictEqual(response.headers['set-cookie'][0], `express.sid=; Path=${nconf.get('relative_path') || '/'}; Expires=Thu, 01 Jan 1970 00:00:00 GMT; SameSite=Lax`);
assert.strictEqual(response.headers.location, `${nconf.get('relative_path')}/`);
});
@@ -648,12 +654,12 @@ describe('Controllers', () => {
// Submit GDPR consent
await request.post(`${nconf.get('url')}/register/complete`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
headers: {
'x-csrf-token': token,
},
body: {
data: {
gdpr_agree_data: 'on',
gdpr_agree_email: 'on',
},
@@ -664,8 +670,8 @@ describe('Controllers', () => {
const { response } = await request.post(`${nconf.get('url')}/register/abort`, {
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
validateStatus: null,
headers: {
'x-csrf-token': token,
},
@@ -688,14 +694,14 @@ describe('Controllers', () => {
it('should return 404 if meta.config.termsOfUse is empty', async () => {
meta.config.termsOfUse = '';
const { response, body } = await request.get(`${nconf.get('url')}/tos`);
const { response, body } = await request.get(`${nconf.get('url')}/tos`, { validateStatus: null });
assert.equal(response.statusCode, 404);
assert(body);
});
it('should error if guests do not have search privilege', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/users?query=bar&section=sort-posts`);
const { response, body } = await request.get(`${nconf.get('url')}/api/users?query=bar&section=sort-posts`, { validateStatus: null });
assert.equal(response.statusCode, 500);
assert(body);
assert.equal(body.error, '[[error:no-privileges]]');
@@ -743,7 +749,7 @@ describe('Controllers', () => {
description: 'Foobar!',
hidden: 1,
});
const { response } = await request.get(`${nconf.get('url')}/groups/hidden-group/members`);
const { response } = await request.get(`${nconf.get('url')}/groups/hidden-group/members`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
@@ -764,6 +770,7 @@ describe('Controllers', () => {
it('should fail to revoke session with missing uuid', async () => {
const { response } = await request.del(`${nconf.get('url')}/api/user/revokeme/session`, {
jar: jar,
validateStatus: null,
headers: {
'x-csrf-token': csrf_token,
},
@@ -774,6 +781,7 @@ describe('Controllers', () => {
it('should fail if user doesn\'t exist', async () => {
const { response, body } = await request.del(`${nconf.get('url')}/api/v3/users/doesnotexist/sessions/1112233`, {
jar: jar,
validateStatus: null,
headers: {
'x-csrf-token': csrf_token,
},
@@ -906,12 +914,12 @@ describe('Controllers', () => {
});
it('should return 503 in maintenance mode', async () => {
const { response } = await request.get(`${nconf.get('url')}/recent`);
const { response } = await request.get(`${nconf.get('url')}/recent`, { validateStatus: null });
assert.equal(response.statusCode, 503);
});
it('should return 503 in maintenance mode', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/recent`);
const { response, body } = await request.get(`${nconf.get('url')}/api/recent`, { validateStatus: null });
assert.equal(response.statusCode, 503);
assert(body);
});
@@ -948,7 +956,7 @@ describe('Controllers', () => {
});
it('should 404 if uid is not a number', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/uid/test`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/uid/test`, { jar, validateStatus: null });
assert.equal(response.statusCode, 404);
});
@@ -967,7 +975,7 @@ describe('Controllers', () => {
});
it('should 404 if user does not exist', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/uid/123123`);
const { response } = await request.get(`${nconf.get('url')}/api/uid/123123`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
@@ -1001,12 +1009,12 @@ describe('Controllers', () => {
});
it('should 401 if user is not logged in', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/admin`);
const { response } = await request.get(`${nconf.get('url')}/api/admin`, { validateStatus: null });
assert.equal(response.statusCode, 401);
});
it('should 403 if user is not admin', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/admin`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/admin`, { jar, validateStatus: null });
assert.equal(response.statusCode, 403);
});
@@ -1017,7 +1025,7 @@ describe('Controllers', () => {
});
it('should 401 if not logged in', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/admin`);
const { response, body } = await request.get(`${nconf.get('url')}/api/admin`, { validateStatus: null });
assert.equal(response.statusCode, 401);
assert(body);
});
@@ -1164,7 +1172,7 @@ describe('Controllers', () => {
});
it('should 404 if user does not exist', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/user/email/doesnotexist`);
const { response, body } = await request.get(`${nconf.get('url')}/api/user/email/doesnotexist`, { validateStatus: null });
assert.equal(response.statusCode, 404);
assert(body);
});
@@ -1182,14 +1190,18 @@ describe('Controllers', () => {
});
it('should NOT load user by email (by default)', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/user/email/foo@test.com`);
const { response } = await request.get(`${nconf.get('url')}/api/user/email/foo@test.com`, {
validateStatus: null,
});
assert.strictEqual(response.statusCode, 404);
});
it('should load user by email if user has elected to show their email', async () => {
await user.setSetting(fooUid, 'showemail', 1);
const { response, body } = await request.get(`${nconf.get('url')}/api/user/email/foo@test.com`);
const { response, body } = await request.get(`${nconf.get('url')}/api/user/email/foo@test.com`, {
resolveWithFullResponse: true,
});
assert.strictEqual(response.statusCode, 200);
assert(body);
await user.setSetting(fooUid, 'showemail', 0);
@@ -1198,7 +1210,7 @@ describe('Controllers', () => {
it('should return 401 if user does not have view:users privilege', async () => {
await privileges.global.rescind(['groups:view:users'], 'guests');
const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo`);
const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo`, { validateStatus: null });
assert.equal(response.statusCode, 401);
assert.deepEqual(body, {
response: {},
@@ -1213,9 +1225,9 @@ describe('Controllers', () => {
it('should return false if user can not edit user', async () => {
await user.create({ username: 'regularJoe', password: 'barbar' });
const { jar } = await helpers.loginUser('regularJoe', 'barbar');
let { response } = await request.get(`${nconf.get('url')}/api/user/foo/info`, { jar });
let { response } = await request.get(`${nconf.get('url')}/api/user/foo/info`, { jar: jar, validateStatus: null });
assert.equal(response.statusCode, 403);
({ response } = await request.get(`${nconf.get('url')}/api/user/foo/edit`, { jar }));
({ response } = await request.get(`${nconf.get('url')}/api/user/foo/edit`, { jar: jar, validateStatus: null }));
assert.equal(response.statusCode, 403);
});
@@ -1231,7 +1243,7 @@ describe('Controllers', () => {
});
it('should 404 if user does not exist', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/user/doesnotexist`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/user/doesnotexist`, { jar: jar, validateStatus: null });
assert.equal(response.statusCode, 404);
});
@@ -1306,7 +1318,7 @@ describe('Controllers', () => {
it('should 404 if user does not exist', async () => {
await groups.join('administrators', fooUid);
const { response } = await request.get(`${nconf.get('url')}/api/user/doesnotexist/edit`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/user/doesnotexist/edit`, { jar: jar, validateStatus: null });
assert.equal(response.statusCode, 404);
await groups.leave('administrators', fooUid);
});
@@ -1317,13 +1329,16 @@ describe('Controllers', () => {
});
it('should render edit/email', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/edit/email`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/user/foo/edit/email`, {
jar,
});
assert.strictEqual(response.statusCode, 200);
assert.strictEqual(body, '/register/complete');
await request.post(`${nconf.get('url')}/register/abort`, {
jar,
validateStatus: null,
headers: {
'x-csrf-token': csrf_token,
},
@@ -1374,13 +1389,13 @@ describe('Controllers', () => {
});
it('should 404 for invalid pid', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/post/fail`);
const { response } = await request.get(`${nconf.get('url')}/api/post/fail`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
it('should 403 if user does not have read privilege', async () => {
await privileges.categories.rescind(['groups:topics:read'], category.cid, 'registered-users');
const { response } = await request.get(`${nconf.get('url')}/api/post/${pid}`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/post/${pid}`, { jar: jar, validateStatus: null });
assert.equal(response.statusCode, 403);
await privileges.categories.give(['groups:topics:read'], category.cid, 'registered-users');
});
@@ -1431,13 +1446,13 @@ describe('Controllers', () => {
});
it('should handle malformed uri ', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/user/a%AFc`);
const { response, body } = await request.get(`${nconf.get('url')}/user/a%AFc`, { validateStatus: null });
assert(body);
assert.equal(response.statusCode, 400);
});
it('should handle malformed uri in api', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/user/a%AFc`);
const { response, body } = await request.get(`${nconf.get('url')}/api/user/a%AFc`, { validateStatus: null });
assert.equal(response.statusCode, 400);
assert.equal(body.error, '[[global:400.title]]');
});
@@ -1452,7 +1467,7 @@ describe('Controllers', () => {
},
});
const { response } = await request.get(`${nconf.get('url')}/users`);
const { response } = await request.get(`${nconf.get('url')}/users`, { validateStatus: null });
plugins.loadedHooks['filter:router.page'] = [];
assert.equal(response.statusCode, 403);
});
@@ -1466,7 +1481,7 @@ describe('Controllers', () => {
next(err);
},
});
const { response, body } = await request.get(`${nconf.get('url')}/users`);
const { response, body } = await request.get(`${nconf.get('url')}/users`, { validateStatus: null });
plugins.loadedHooks['filter:router.page'] = [];
assert.equal(response.statusCode, 403);
assert.equal(body, 'blacklist error message');
@@ -1513,7 +1528,7 @@ describe('Controllers', () => {
next(err);
},
});
const { response, body } = await request.get(`${nconf.get('url')}/users`);
const { response, body } = await request.get(`${nconf.get('url')}/users`, { validateStatus: null });
plugins.loadedHooks['filter:router.page'] = [];
assert.equal(response.statusCode, 500);
assert(body);
@@ -1527,31 +1542,31 @@ describe('Controllers', () => {
});
it('should return 404 if cid is not a number', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/category/fail`);
const { response, body } = await request.get(`${nconf.get('url')}/api/category/fail`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
it('should return 404 if topic index is not a number', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}/invalidtopicindex`);
const { response, body } = await request.get(`${nconf.get('url')}/api/category/${category.slug}/invalidtopicindex`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
it('should 404 if category does not exist', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/category/123123`);
const { response, body } = await request.get(`${nconf.get('url')}/api/category/123123`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
it('should 404 if category is disabled', async () => {
const category = await categories.create({ name: 'disabled' });
await categories.setCategoryField(category.cid, 'disabled', 1);
const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`);
const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
it('should return 401 if not allowed to read', async () => {
const category = await categories.create({ name: 'hidden' });
await privileges.categories.rescind(['groups:read'], category.cid, 'guests');
const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`);
const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}`, { validateStatus: null });
assert.equal(response.statusCode, 401);
});
@@ -1563,7 +1578,7 @@ describe('Controllers', () => {
it('should 404 if page is not found', async () => {
await user.setSetting(fooUid, 'usePagination', 1);
const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}?page=100`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/category/${category.slug}?page=100`, { jar, validateStatus: null });
assert.equal(response.statusCode, 404);
});
@@ -1668,23 +1683,23 @@ describe('Controllers', () => {
});
it('should load unread page', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/unread`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/unread`, { jar: jar });
assert.equal(response.statusCode, 200);
});
it('should 404 if filter is invalid', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/unread/doesnotexist`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/unread/doesnotexist`, { jar: jar, validateStatus: null });
assert.equal(response.statusCode, 404);
});
it('should return total unread count', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/unread/total?filter=new`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/unread/total?filter=new`, { jar: jar });
assert.equal(response.statusCode, 200);
assert.equal(body, 0);
});
it('should redirect if page is out of bounds', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/unread?page=-1`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/unread?page=-1`, { jar: jar });
assert.equal(response.statusCode, 200);
assert.equal(response.headers['x-redirect'], '/unread?page=1');
assert.equal(body, '/unread?page=1');
@@ -1693,7 +1708,7 @@ describe('Controllers', () => {
describe('admin middlewares', () => {
it('should redirect to login', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/admin/advanced/database`);
const { response } = await request.get(`${nconf.get('url')}/api/admin/advanced/database`, { validateStatus: null });
assert.equal(response.statusCode, 401);
});
@@ -1751,48 +1766,51 @@ describe('Controllers', () => {
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
assert.equal(result.response.statusCode, 400);
result = await request.post(`${nconf.get('url')}/compose`, {
body: {
data: {
tid: tid,
},
jar: jar,
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
assert.equal(result.response.statusCode, 400);
});
it('should create a new topic and reply by composer route', async () => {
const data = {
cid: cid,
title: 'no js is good',
content: 'a topic with noscript',
};
let result = await request.post(`${nconf.get('url')}/compose`, {
body: {
cid: cid,
title: 'no js is good',
content: 'a topic with noscript',
},
data: data,
jar: jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
assert.equal(result.response.statusCode, 302);
result = await request.post(`${nconf.get('url')}/compose`, {
body: {
data: {
tid: tid,
content: 'a new reply',
},
jar: jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
assert.equal(result.response.statusCode, 302);
});
@@ -1800,37 +1818,38 @@ describe('Controllers', () => {
it('should create a new topic and reply by composer route as a guest', async () => {
const jar = request.jar();
const csrf_token = await helpers.getCsrfToken(jar);
const data = {
cid: cid,
title: 'no js is good',
content: 'a topic with noscript',
handle: 'guest1',
};
await privileges.categories.give(['groups:topics:create', 'groups:topics:reply'], cid, 'guests');
const result = await helpers.request('post', `/compose`, {
body: {
cid: cid,
title: 'no js is good',
content: 'a topic with noscript',
handle: 'guest1',
},
data: data,
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
assert.strictEqual(result.response.statusCode, 302);
const replyResult = await helpers.request('post', `/compose`, {
body: {
data: {
tid: tid,
content: 'a new reply',
handle: 'guest2',
},
jar,
maxRedirect: 0,
redirect: 'manual',
maxRedirects: 0,
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
assert.equal(replyResult.response.statusCode, 302);
await privileges.categories.rescind(['groups:topics:post', 'groups:topics:reply'], cid, 'guests');
@@ -1840,7 +1859,7 @@ describe('Controllers', () => {
describe('test routes', () => {
if (process.env.NODE_ENV === 'development') {
it('should load debug route', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/debug/test`);
const { response, body } = await request.get(`${nconf.get('url')}/debug/test`, { validateStatus: null });
assert.equal(response.statusCode, 404);
assert(body);
});
@@ -1858,7 +1877,7 @@ describe('Controllers', () => {
});
it('should load 404 for invalid type', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/debug/spec/doesnotexist`);
const { response, body } = await request.get(`${nconf.get('url')}/debug/spec/doesnotexist`, { validateStatus: null });
assert.equal(response.statusCode, 404);
assert(body);
});

View File

@@ -50,19 +50,19 @@ describe('feeds', () => {
];
for (const url of feedUrls) {
// eslint-disable-next-line no-await-in-loop
const { response } = await request.get(url);
const { response } = await request.get(url, { validateStatus: null });
assert.equal(response.statusCode, 404);
}
meta.config['feeds:disableRSS'] = 0;
});
it('should 404 if topic does not exist', async () => {
const { response } = await request.get(`${nconf.get('url')}/topic/${1000}.rss`);
const { response } = await request.get(`${nconf.get('url')}/topic/${1000}.rss`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
it('should 404 if category id is not a number', async () => {
const { response } = await request.get(`${nconf.get('url')}/category/invalid.rss`);
const { response } = await request.get(`${nconf.get('url')}/category/invalid.rss`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});
@@ -76,7 +76,7 @@ describe('feeds', () => {
});
it('should 404 if user is not found', async () => {
const { response } = await request.get(`${nconf.get('url')}/user/doesnotexist/topics.rss`);
const { response } = await request.get(`${nconf.get('url')}/user/doesnotexist/topics.rss`, { validateStatus: null });
assert.equal(response.statusCode, 404);
});

View File

@@ -894,7 +894,7 @@ describe('Flags', () => {
headers: {
'x-csrf-token': csrfToken,
},
body: {
data: {
type: 'post',
id: pid,
reason: 'foobar',
@@ -917,7 +917,7 @@ describe('Flags', () => {
headers: {
'x-csrf-token': csrfToken,
},
body: {
data: {
type: 'post',
id: postData.pid,
reason: '"<script>alert(\'ok\');</script>',
@@ -948,11 +948,12 @@ describe('Flags', () => {
headers: {
'x-csrf-token': csrfToken,
},
body: {
data: {
type: 'post',
id: result.postData.pid,
reason: 'foobar',
},
validateStatus: null,
});
assert.strictEqual(response.statusCode, 403);
@@ -976,7 +977,7 @@ describe('Flags', () => {
headers: {
'x-csrf-token': csrfToken,
},
body: {
data: {
state: 'wip',
},
});
@@ -1008,7 +1009,7 @@ describe('Flags', () => {
headers: {
'x-csrf-token': csrfToken,
},
body: {
data: {
note: 'lorem ipsum dolor sit amet',
datetime: 1626446956652,
},
@@ -1055,7 +1056,7 @@ describe('Flags', () => {
before(async () => {
uid = await User.create({ username: 'flags-access-control', password: 'abcdef' });
({ jar, csrf_token } = await helpers.loginUser('flags-access-control', 'abcdef'));
console.log('cs', csrfToken);
flaggerUid = await User.create({ username: 'flags-access-control-flagger', password: 'abcdef' });
});
@@ -1078,6 +1079,7 @@ describe('Flags', () => {
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
};
requests = new Set([
{
@@ -1089,7 +1091,7 @@ describe('Flags', () => {
...commonOpts,
method: 'put',
uri: `${nconf.get('url')}/api/v3/flags/${flagId}`,
body: {
data: {
state: 'wip',
},
},
@@ -1097,7 +1099,7 @@ describe('Flags', () => {
...commonOpts,
method: 'post',
uri: `${nconf.get('url')}/api/v3/flags/${flagId}/notes`,
body: {
data: {
note: 'test note',
datetime: noteTime,
},

View File

@@ -28,7 +28,9 @@ helpers.request = async function (method, uri, options = {}) {
if (csrf_token) {
options.headers['x-csrf-token'] = csrf_token;
}
return await request[lowercaseMethod](`${nconf.get('url')}${uri}`, options);
options.validateStatus = null;
const { response, body } = await request[lowercaseMethod](`${nconf.get('url')}${uri}`, options);
return { response, body };
};
helpers.loginUser = async (username, password, payload = {}) => {
@@ -37,8 +39,9 @@ helpers.loginUser = async (username, password, payload = {}) => {
const csrf_token = await helpers.getCsrfToken(jar);
const { response, body } = await request.post(`${nconf.get('url')}/login`, {
body: data,
data,
jar: jar,
validateStatus: () => true,
headers: {
'x-csrf-token': csrf_token,
},
@@ -50,8 +53,9 @@ helpers.loginUser = async (username, password, payload = {}) => {
helpers.logoutUser = async function (jar) {
const csrf_token = await helpers.getCsrfToken(jar);
const { response, body } = await request.post(`${nconf.get('url')}/logout`, {
body: {},
data: {},
jar,
validateStatus: () => true,
headers: {
'x-csrf-token': csrf_token,
},
@@ -61,7 +65,9 @@ helpers.logoutUser = async function (jar) {
helpers.connectSocketIO = function (res, csrf_token) {
const io = require('socket.io-client');
const cookie = res.headers['set-cookie'];
let cookies = res.headers['set-cookie'];
cookies = cookies.filter(c => /express.sid=[^;]+;/.test(c));
const cookie = cookies[0];
const socket = io(nconf.get('base_url'), {
path: `${nconf.get('relative_path')}/socket.io`,
extraHeaders: {
@@ -90,35 +96,26 @@ helpers.connectSocketIO = function (res, csrf_token) {
};
helpers.uploadFile = async function (uploadEndPoint, filePath, data, jar, csrf_token) {
const mime = require('mime');
const FormData = require('form-data');
const form = new FormData();
const file = await fs.promises.readFile(filePath);
const blob = new Blob([file], { type: mime.getType(filePath) });
form.append('files', blob, path.basename(filePath));
form.append('files', fs.createReadStream(filePath), path.basename(filePath));
if (data && data.params) {
form.append('params', data.params);
}
const response = await fetch(uploadEndPoint, {
method: 'post',
body: form,
const { response, body } = await request.post(uploadEndPoint, {
data: form,
jar: jar,
validateStatus: null,
headers: {
'x-csrf-token': csrf_token,
cookie: await jar.getCookieString(uploadEndPoint),
...form.getHeaders(),
},
});
const body = await response.json();
return {
body,
response: {
status: response.status,
statusCode: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
},
};
if (response.status !== 200) {
winston.error(JSON.stringify(data));
}
return { response, body };
};
helpers.registerUser = async function (data) {
@@ -130,8 +127,9 @@ helpers.registerUser = async function (data) {
}
const { response, body } = await request.post(`${nconf.get('url')}/register`, {
body: data,
data,
jar,
validateStatus: () => true,
headers: {
'x-csrf-token': csrf_token,
},
@@ -167,23 +165,25 @@ helpers.copyFile = function (source, target, callback) {
helpers.invite = async function (data, uid, jar, csrf_token) {
return await request.post(`${nconf.get('url')}/api/v3/users/${uid}/invites`, {
jar: jar,
body: data,
data: data,
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
};
helpers.createFolder = async function (path, folderName, jar, csrf_token) {
return await request.put(`${nconf.get('url')}/api/v3/files/folder`, {
jar,
body: {
data: {
path,
folderName,
},
headers: {
'x-csrf-token': csrf_token,
},
validateStatus: null,
});
};

View File

@@ -33,8 +33,9 @@ describe('Messaging Library', () => {
const callv3API = async (method, path, body, user) => {
const options = {
body,
data: body,
jar: mocks.users[user].jar,
validateStatus: null,
};
if (method !== 'get') {
@@ -301,7 +302,7 @@ describe('Messaging Library', () => {
const receiver = await User.create({ username: 'receiver' });
const { body } = await request.post(`${nconf.get('url')}/api/v3/chats`, {
jar: senderJar,
body: {
data: {
uids: [receiver],
},
headers: {
@@ -761,21 +762,29 @@ describe('Messaging Library', () => {
describe('controller', () => {
it('should 404 if chat is disabled', async () => {
meta.config.disableChat = 1;
const { response } = await request.get(`${nconf.get('url')}/user/baz/chats`);
const { response } = await request.get(`${nconf.get('url')}/user/baz/chats`, {
validateStatus: null,
});
assert.equal(response.statusCode, 404);
});
it('should 401 for guest with not-authorised status code', async () => {
meta.config.disableChat = 0;
const { response, body } = await request.get(`${nconf.get('url')}/api/user/baz/chats`);
const { response, body } = await request.get(`${nconf.get('url')}/api/user/baz/chats`, {
resolveWithFullResponse: true,
validateStatus: null,
});
assert.equal(response.statusCode, 401);
assert.equal(body.status.code, 'not-authorised');
});
it('should 404 for non-existent user', async () => {
const { response } = await request.get(`${nconf.get('url')}/user/doesntexist/chats`);
const { response } = await request.get(`${nconf.get('url')}/user/doesntexist/chats`, {
validateStatus: null,
});
assert.equal(response.statusCode, 404);
});
});
@@ -787,7 +796,10 @@ describe('Messaging Library', () => {
});
it('should return chats page data', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/user/herp/chats`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/user/herp/chats`, {
validateStatus: null,
jar,
});
assert.equal(response.statusCode, 200);
assert(Array.isArray(body.rooms));
@@ -796,7 +808,10 @@ describe('Messaging Library', () => {
});
it('should return room data', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/user/herp/chats/${roomId}`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/user/herp/chats/${roomId}`, {
validateStatus: null,
jar,
});
assert.equal(response.statusCode, 200);
assert.equal(body.roomId, roomId);
@@ -804,7 +819,10 @@ describe('Messaging Library', () => {
});
it('should redirect to chats page', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/api/chats`, { jar });
const { response, body } = await request.get(`${nconf.get('url')}/api/chats`, {
validateStatus: null,
jar,
});
assert.equal(response.statusCode, 200);
assert.equal(response.headers['x-redirect'], '/user/herp/chats');
@@ -813,7 +831,10 @@ describe('Messaging Library', () => {
it('should return 404 if user is not in room', async () => {
const data = await helpers.loginUser('baz', 'quuxquux');
const { response } = await request.get(`${nconf.get('url')}/api/user/baz/chats/${roomId}`, { jar: data.jar });
const { response } = await request.get(`${nconf.get('url')}/api/user/baz/chats/${roomId}`, {
validateStatus: null,
jar: data.jar,
});
assert.equal(response.statusCode, 404);
});

View File

@@ -493,6 +493,8 @@ describe('meta', () => {
it('Access-Control-Allow-Origin header should be empty', async () => {
const jar = request.jar();
const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, {
data: {},
validateStatus: null,
jar: jar,
});
@@ -504,10 +506,12 @@ describe('meta', () => {
const oldValue = meta.config['access-control-allow-origin'];
meta.config['access-control-allow-origin'] = 'test.com, mydomain.com';
const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, {
data: { },
jar: jar,
headers: {
origin: 'mydomain.com',
},
validateStatus: null,
});
assert.equal(response.headers['access-control-allow-origin'], 'mydomain.com');
@@ -524,6 +528,7 @@ describe('meta', () => {
headers: {
origin: 'notallowed.com',
},
validateStatus: null,
});
assert.equal(response.headers['access-control-allow-origin'], undefined);
meta.config['access-control-allow-origin'] = oldValue;
@@ -534,10 +539,12 @@ describe('meta', () => {
const oldValue = meta.config['access-control-allow-origin-regex'];
meta.config['access-control-allow-origin-regex'] = 'match\\.this\\..+\\.domain.com, mydomain\\.com';
const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, {
data: {},
jar: jar,
headers: {
origin: 'match.this.anything123.domain.com',
},
validateStatus: null,
});
assert.equal(response.headers['access-control-allow-origin'], 'match.this.anything123.domain.com');
@@ -549,10 +556,12 @@ describe('meta', () => {
const oldValue = meta.config['access-control-allow-origin-regex'];
meta.config['access-control-allow-origin-regex'] = 'match\\.this\\..+\\.domain.com, mydomain\\.com';
const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, {
data: {},
jar: jar,
headers: {
origin: 'notallowed.com',
},
validateStatus: null,
});
assert.equal(response.headers['access-control-allow-origin'], undefined);
meta.config['access-control-allow-origin-regex'] = oldValue;
@@ -563,10 +572,12 @@ describe('meta', () => {
const oldValue = meta.config['access-control-allow-origin-regex'];
meta.config['access-control-allow-origin-regex'] = '[match\\.this\\..+\\.domain.com, mydomain\\.com';
const { response } = await request.get(`${nconf.get('url')}/api/search?term=bug`, {
data: { },
jar: jar,
headers: {
origin: 'mydomain.com',
},
validateStatus: null,
});
assert.equal(response.headers['access-control-allow-origin'], 'mydomain.com');
meta.config['access-control-allow-origin-regex'] = oldValue;

View File

@@ -116,7 +116,9 @@ describe('Middlewares', () => {
});
it('should be absent on non-existent routes, for guests', async () => {
const { response } = await request.get(`${nconf.get('url')}/${utils.generateUUID()}`);
const { response } = await request.get(`${nconf.get('url')}/${utils.generateUUID()}`, {
validateStatus: null,
});
assert.strictEqual(response.statusCode, 404);
assert(!Object.keys(response.headers).includes('cache-control'));
@@ -124,6 +126,7 @@ describe('Middlewares', () => {
it('should be set to "private" on non-existent routes, for logged in users', async () => {
const { response } = await request.get(`${nconf.get('url')}/${utils.generateUUID()}`, {
validateStatus: null,
jar,
headers: {
accept: 'text/html',
@@ -136,21 +139,28 @@ describe('Middlewares', () => {
});
it('should be absent on regular routes, for guests', async () => {
const { response } = await request.get(nconf.get('url'));
const { response } = await request.get(nconf.get('url'), {
validateStatus: null,
});
assert.strictEqual(response.statusCode, 200);
assert(!Object.keys(response.headers).includes('cache-control'));
});
it('should be absent on api routes, for guests', async () => {
const { response } = await request.get(`${nconf.get('url')}/api`);
const { response } = await request.get(`${nconf.get('url')}/api`, {
validateStatus: null,
});
assert.strictEqual(response.statusCode, 200);
assert(!Object.keys(response.headers).includes('cache-control'));
});
it('should be set to "private" on regular routes, for logged-in users', async () => {
const { response } = await request.get(nconf.get('url'), { jar });
const { response } = await request.get(nconf.get('url'), {
validateStatus: null,
jar,
});
assert.strictEqual(response.statusCode, 200);
assert(Object.keys(response.headers).includes('cache-control'));
@@ -158,7 +168,10 @@ describe('Middlewares', () => {
});
it('should be set to "private" on api routes, for logged-in users', async () => {
const { response } = await request.get(`${nconf.get('url')}/api`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api`, {
validateStatus: null,
jar,
});
assert.strictEqual(response.statusCode, 200);
assert(Object.keys(response.headers).includes('cache-control'));
@@ -166,7 +179,10 @@ describe('Middlewares', () => {
});
it('should be set to "private" on apiv3 routes, for logged-in users', async () => {
const { response } = await request.get(`${nconf.get('url')}/api/v3/users/${uid}`, { jar });
const { response } = await request.get(`${nconf.get('url')}/api/v3/users/${uid}`, {
validateStatus: null,
jar,
});
assert.strictEqual(response.statusCode, 200);
assert(Object.keys(response.headers).includes('cache-control'));

View File

@@ -292,14 +292,14 @@ describe('Plugins', () => {
describe('static assets', () => {
it('should 404 if resource does not exist', async () => {
const { response, body } = await request.get(`${nconf.get('url')}/plugins/doesnotexist/should404.tpl`);
const { response, body } = await request.get(`${nconf.get('url')}/plugins/doesnotexist/should404.tpl`, { validateStatus: null });
assert.equal(response.statusCode, 404);
assert(body);
});
it('should 404 if resource does not exist', async () => {
const url = `${nconf.get('url')}/plugins/nodebb-plugin-dbsearch/dbsearch/templates/admin/plugins/should404.tpl`;
const { response, body } = await request.get(url);
const { response, body } = await request.get(url, { validateStatus: null });
assert.equal(response.statusCode, 404);
assert(body);
});

View File

@@ -2,6 +2,7 @@
const assert = require('assert');
const async = require('async');
const nconf = require('nconf');
const path = require('path');
@@ -33,22 +34,48 @@ describe('Post\'s', () => {
let topicData;
let cid;
before(async () => {
voterUid = await user.create({ username: 'upvoter' });
voteeUid = await user.create({ username: 'upvotee' });
globalModUid = await user.create({ username: 'globalmod', password: 'globalmodpwd' });
({ cid } = await categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
}));
before((done) => {
async.series({
voterUid: function (next) {
user.create({ username: 'upvoter' }, next);
},
voteeUid: function (next) {
user.create({ username: 'upvotee' }, next);
},
globalModUid: function (next) {
user.create({ username: 'globalmod', password: 'globalmodpwd' }, next);
},
category: function (next) {
categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
}, next);
},
}, (err, results) => {
if (err) {
return done(err);
}
({ topicData, postData } = await topics.post({
uid: voteeUid,
cid: cid,
title: 'Test Topic Title',
content: 'The content of test topic',
}));
await groups.join('Global Moderators', globalModUid);
voterUid = results.voterUid;
voteeUid = results.voteeUid;
globalModUid = results.globalModUid;
cid = results.category.cid;
topics.post({
uid: results.voteeUid,
cid: results.category.cid,
title: 'Test Topic Title',
content: 'The content of test topic',
}, (err, data) => {
if (err) {
return done(err);
}
postData = data.postData;
topicData = data.topicData;
groups.join('Global Moderators', globalModUid, done);
});
});
});
it('should update category teaser properly', async () => {
@@ -1031,10 +1058,20 @@ describe('Post\'s', () => {
});
});
it('should accept queued posts and submit', async () => {
const ids = await db.getSortedSetRange('post:queue', 0, -1);
await socketPosts.accept({ uid: globalModUid }, { id: ids[0] });
await socketPosts.accept({ uid: globalModUid }, { id: ids[1] });
it('should accept queued posts and submit', (done) => {
let ids;
async.waterfall([
function (next) {
db.getSortedSetRange('post:queue', 0, -1, next);
},
function (_ids, next) {
ids = _ids;
socketPosts.accept({ uid: globalModUid }, { id: ids[0] }, next);
},
function (next) {
socketPosts.accept({ uid: globalModUid }, { id: ids[1] }, next);
},
], done);
});
it('should not crash if id does not exist', (done) => {

View File

@@ -6,6 +6,7 @@ const path = require('path');
const os = require('os');
const nconf = require('nconf');
const async = require('async');
const crypto = require('crypto');
const db = require('../mocks/databasemock');
@@ -74,15 +75,24 @@ describe('upload methods', () => {
});
});
it('should remove an image if it is edited out of the post', async () => {
await posts.edit({
pid: pid,
uid,
content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png)... AND NO MORE!',
it('should remove an image if it is edited out of the post', (done) => {
async.series([
function (next) {
posts.edit({
pid: pid,
uid,
content: 'here is an image [alt text](/assets/uploads/files/abracadabra.png)... AND NO MORE!',
}, next);
},
async.apply(posts.uploads.sync, pid),
], (err) => {
assert.ifError(err);
db.sortedSetCard(`post:${pid}:uploads`, (err, length) => {
assert.ifError(err);
assert.strictEqual(1, length);
done();
});
});
await posts.uploads.sync(pid);
const length = await db.sortedSetCard(`post:${pid}:uploads`);
assert.strictEqual(1, length);
});
});
@@ -117,52 +127,85 @@ describe('upload methods', () => {
});
describe('.associate()', () => {
it('should add an image to the post\'s maintained list of uploads', async () => {
await posts.uploads.associate(pid, 'files/whoa.gif');
const uploads = await posts.uploads.list(pid);
assert.strictEqual(2, uploads.length);
assert.strictEqual(true, uploads.includes('files/whoa.gif'));
it('should add an image to the post\'s maintained list of uploads', (done) => {
async.waterfall([
async.apply(posts.uploads.associate, pid, 'files/whoa.gif'),
async.apply(posts.uploads.list, pid),
], (err, uploads) => {
assert.ifError(err);
assert.strictEqual(2, uploads.length);
assert.strictEqual(true, uploads.includes('files/whoa.gif'));
done();
});
});
it('should allow arrays to be passed in', async () => {
await posts.uploads.associate(pid, ['files/amazeballs.jpg', 'files/wut.txt']);
const uploads = await posts.uploads.list(pid);
assert.strictEqual(4, uploads.length);
assert.strictEqual(true, uploads.includes('files/amazeballs.jpg'));
assert.strictEqual(true, uploads.includes('files/wut.txt'));
it('should allow arrays to be passed in', (done) => {
async.waterfall([
async.apply(posts.uploads.associate, pid, ['files/amazeballs.jpg', 'files/wut.txt']),
async.apply(posts.uploads.list, pid),
], (err, uploads) => {
assert.ifError(err);
assert.strictEqual(4, uploads.length);
assert.strictEqual(true, uploads.includes('files/amazeballs.jpg'));
assert.strictEqual(true, uploads.includes('files/wut.txt'));
done();
});
});
it('should save a reverse association of md5sum to pid', async () => {
it('should save a reverse association of md5sum to pid', (done) => {
const md5 = filename => crypto.createHash('md5').update(filename).digest('hex');
await posts.uploads.associate(pid, ['files/test.bmp']);
const pids = await db.getSortedSetRange(`upload:${md5('files/test.bmp')}:pids`, 0, -1);
assert.strictEqual(true, Array.isArray(pids));
assert.strictEqual(true, pids.length > 0);
assert.equal(pid, pids[0]);
async.waterfall([
async.apply(posts.uploads.associate, pid, ['files/test.bmp']),
function (next) {
db.getSortedSetRange(`upload:${md5('files/test.bmp')}:pids`, 0, -1, next);
},
], (err, pids) => {
assert.ifError(err);
assert.strictEqual(true, Array.isArray(pids));
assert.strictEqual(true, pids.length > 0);
assert.equal(pid, pids[0]);
done();
});
});
it('should not associate a file that does not exist on the local disk', async () => {
await posts.uploads.associate(pid, ['files/nonexistant.xls']);
const uploads = await posts.uploads.list(pid);
assert.strictEqual(uploads.length, 5);
assert.strictEqual(false, uploads.includes('files/nonexistant.xls'));
it('should not associate a file that does not exist on the local disk', (done) => {
async.waterfall([
async.apply(posts.uploads.associate, pid, ['files/nonexistant.xls']),
async.apply(posts.uploads.list, pid),
], (err, uploads) => {
assert.ifError(err);
assert.strictEqual(uploads.length, 5);
assert.strictEqual(false, uploads.includes('files/nonexistant.xls'));
done();
});
});
});
describe('.dissociate()', () => {
it('should remove an image from the post\'s maintained list of uploads', async () => {
await posts.uploads.dissociate(pid, 'files/whoa.gif');
const uploads = await posts.uploads.list(pid);
assert.strictEqual(4, uploads.length);
assert.strictEqual(false, uploads.includes('files/whoa.gif'));
it('should remove an image from the post\'s maintained list of uploads', (done) => {
async.waterfall([
async.apply(posts.uploads.dissociate, pid, 'files/whoa.gif'),
async.apply(posts.uploads.list, pid),
], (err, uploads) => {
assert.ifError(err);
assert.strictEqual(4, uploads.length);
assert.strictEqual(false, uploads.includes('files/whoa.gif'));
done();
});
});
it('should allow arrays to be passed in', async () => {
await posts.uploads.dissociate(pid, ['files/amazeballs.jpg', 'files/wut.txt']);
const uploads = await posts.uploads.list(pid);
assert.strictEqual(2, uploads.length);
assert.strictEqual(false, uploads.includes('files/amazeballs.jpg'));
assert.strictEqual(false, uploads.includes('files/wut.txt'));
it('should allow arrays to be passed in', (done) => {
async.waterfall([
async.apply(posts.uploads.dissociate, pid, ['files/amazeballs.jpg', 'files/wut.txt']),
async.apply(posts.uploads.list, pid),
], (err, uploads) => {
assert.ifError(err);
assert.strictEqual(2, uploads.length);
assert.strictEqual(false, uploads.includes('files/amazeballs.jpg'));
assert.strictEqual(false, uploads.includes('files/wut.txt'));
done();
});
});
it('should remove the image\'s user association, if present', async () => {
@@ -354,14 +397,21 @@ describe('post uploads management', () => {
});
});
it('should automatically sync uploads on post edit', async () => {
await posts.edit({
pid: reply.pid,
uid,
content: 'no uploads',
it('should automatically sync uploads on post edit', (done) => {
async.waterfall([
async.apply(posts.edit, {
pid: reply.pid,
uid,
content: 'no uploads',
}),
function (postData, next) {
posts.uploads.list(reply.pid, next);
},
], (err, uploads) => {
assert.ifError(err);
assert.strictEqual(true, Array.isArray(uploads));
assert.strictEqual(0, uploads.length);
done();
});
const uploads = await posts.uploads.list(reply.pid);
assert.strictEqual(true, Array.isArray(uploads));
assert.strictEqual(0, uploads.length);
});
});

View File

@@ -2,6 +2,8 @@
const assert = require('assert');
const async = require('async');
const nconf = require('nconf');
const db = require('./mocks/databasemock');
@@ -25,45 +27,82 @@ describe('Search', () => {
let cid2;
let cid3;
before(async () => {
phoebeUid = await user.create({ username: 'phoebe' });
gingerUid = await user.create({ username: 'ginger' });
cid1 = (await categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
})).cid;
before((done) => {
async.waterfall([
function (next) {
async.series({
phoebe: function (next) {
user.create({ username: 'phoebe' }, next);
},
ginger: function (next) {
user.create({ username: 'ginger' }, next);
},
category1: function (next) {
categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
}, next);
},
category2: function (next) {
categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
}, next);
},
}, next);
},
function (results, next) {
phoebeUid = results.phoebe;
gingerUid = results.ginger;
cid1 = results.category1.cid;
cid2 = results.category2.cid;
cid2 = (await categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
})).cid;
async.waterfall([
function (next) {
categories.create({
name: 'Child Test Category',
description: 'Test category created by testing script',
parentCid: cid2,
}, next);
},
function (category, next) {
cid3 = category.cid;
topics.post({
uid: phoebeUid,
cid: cid1,
title: 'nodebb mongodb bugs',
content: 'avocado cucumber apple orange fox',
tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'],
}, next);
},
function (results, next) {
topic1Data = results.topicData;
post1Data = results.postData;
cid3 = (await categories.create({
name: 'Child Test Category',
description: 'Test category created by testing script',
parentCid: cid2,
})).cid;
({ topicData: topic1Data, postData: post1Data } = await topics.post({
uid: phoebeUid,
cid: cid1,
title: 'nodebb mongodb bugs',
content: 'avocado cucumber apple orange fox',
tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'jquery'],
}));
({ topicData: topic2Data, postData: post2Data } = await topics.post({
uid: gingerUid,
cid: cid2,
title: 'java mongodb redis',
content: 'avocado cucumber carrot armadillo',
tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'],
}));
post3Data = await topics.reply({
uid: phoebeUid,
content: 'reply post apple',
tid: topic2Data.tid,
});
topics.post({
uid: gingerUid,
cid: cid2,
title: 'java mongodb redis',
content: 'avocado cucumber carrot armadillo',
tags: ['nodebb', 'bug', 'plugin', 'nodebb-plugin', 'javascript'],
}, next);
},
function (results, next) {
topic2Data = results.topicData;
post2Data = results.postData;
topics.reply({
uid: phoebeUid,
content: 'reply post apple',
tid: topic2Data.tid,
}, next);
},
function (_post3Data, next) {
post3Data = _post3Data;
setTimeout(next, 500);
},
], next);
},
], done);
});
it('should search term in titles and posts', async () => {
@@ -181,24 +220,33 @@ describe('Search', () => {
});
});
it('should search child categories', async () => {
await topics.post({
uid: gingerUid,
cid: cid3,
title: 'child category topic',
content: 'avocado cucumber carrot armadillo',
});
const result = await search.search({
query: 'avocado',
searchIn: 'titlesposts',
categories: [cid2],
searchChildren: true,
sortBy: 'topic.timestamp',
sortDirection: 'desc',
});
assert(result.posts.length, 2);
assert(result.posts[0].topic.title === 'child category topic');
assert(result.posts[1].topic.title === 'java mongodb redis');
it('should search child categories', (done) => {
async.waterfall([
function (next) {
topics.post({
uid: gingerUid,
cid: cid3,
title: 'child category topic',
content: 'avocado cucumber carrot armadillo',
}, next);
},
function (result, next) {
search.search({
query: 'avocado',
searchIn: 'titlesposts',
categories: [cid2],
searchChildren: true,
sortBy: 'topic.timestamp',
sortDirection: 'desc',
}, next);
},
function (result, next) {
assert(result.posts.length, 2);
assert(result.posts[0].topic.title === 'child category topic');
assert(result.posts[1].topic.title === 'java mongodb redis');
next();
},
], done);
});
it('should return json search data with no categories', async () => {

View File

@@ -9,8 +9,10 @@ const util = require('util');
const sleep = util.promisify(setTimeout);
const assert = require('assert');
const async = require('async');
const nconf = require('nconf');
const db = require('./mocks/databasemock');
const user = require('../src/user');
const groups = require('../src/groups');
@@ -431,38 +433,20 @@ describe('socket.io', () => {
});
});
describe('install/upgrade plugin', () => {
it('should toggle plugin install', function (done) {
this.timeout(0);
const oldValue = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
socketAdmin.plugins.toggleInstall({
uid: adminUid,
}, {
id: 'nodebb-plugin-location-to-map',
version: 'latest',
}, (err, data) => {
assert.ifError(err);
assert.equal(data.name, 'nodebb-plugin-location-to-map');
process.env.NODE_ENV = oldValue;
done();
});
});
it('should upgrade plugin', function (done) {
this.timeout(0);
const oldValue = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
socketAdmin.plugins.upgrade({
uid: adminUid,
}, {
id: 'nodebb-plugin-location-to-map',
version: 'latest',
}, (err) => {
assert.ifError(err);
process.env.NODE_ENV = oldValue;
done();
});
it('should toggle plugin install', function (done) {
this.timeout(0);
const oldValue = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
socketAdmin.plugins.toggleInstall({
uid: adminUid,
}, {
id: 'nodebb-plugin-location-to-map',
version: 'latest',
}, (err, data) => {
assert.ifError(err);
assert.equal(data.name, 'nodebb-plugin-location-to-map');
process.env.NODE_ENV = oldValue;
done();
});
});
@@ -491,6 +475,22 @@ describe('socket.io', () => {
});
});
it('should upgrade plugin', function (done) {
this.timeout(0);
const oldValue = process.env.NODE_ENV;
process.env.NODE_ENV = 'development';
socketAdmin.plugins.upgrade({
uid: adminUid,
}, {
id: 'nodebb-plugin-location-to-map',
version: 'latest',
}, (err) => {
assert.ifError(err);
process.env.NODE_ENV = oldValue;
done();
});
});
it('should error with invalid data', (done) => {
socketAdmin.widgets.set({ uid: adminUid }, null, (err) => {
assert.equal(err.message, '[[error:invalid-data]]');
@@ -683,43 +683,60 @@ describe('socket.io', () => {
assert(pwExpiry > then && pwExpiry < Date.now());
});
it('should not error on valid email', async () => {
await socketUser.reset.send({ uid: 0 }, 'regular@test.com');
const [count, eventsData] = await Promise.all([
db.sortedSetCount('reset:issueDate', 0, Date.now()),
events.getEvents('', 0, 0),
]);
assert.strictEqual(count, 2);
it('should not error on valid email', (done) => {
socketUser.reset.send({ uid: 0 }, 'regular@test.com', (err) => {
assert.ifError(err);
// Event validity
assert.strictEqual(eventsData.length, 1);
const event = eventsData[0];
assert.strictEqual(event.type, 'password-reset');
assert.strictEqual(event.text, '[[success:success]]');
async.parallel({
count: async.apply(db.sortedSetCount.bind(db), 'reset:issueDate', 0, Date.now()),
event: async.apply(events.getEvents, '', 0, 0),
}, (err, data) => {
assert.ifError(err);
assert.strictEqual(data.count, 2);
// Event validity
assert.strictEqual(data.event.length, 1);
const event = data.event[0];
assert.strictEqual(event.type, 'password-reset');
assert.strictEqual(event.text, '[[success:success]]');
done();
});
});
});
it('should not generate code if rate limited', async () => {
await assert.rejects(
socketUser.reset.send({ uid: 0 }, 'regular@test.com'),
{ message: '[[error:reset-rate-limited]]' },
);
const [count, eventsData] = await Promise.all([
db.sortedSetCount('reset:issueDate', 0, Date.now()),
events.getEvents('', 0, 0),
]);
assert.strictEqual(count, 2);
it('should not generate code if rate limited', (done) => {
socketUser.reset.send({ uid: 0 }, 'regular@test.com', (err) => {
assert(err);
// Event validity
assert.strictEqual(eventsData.length, 1);
const event = eventsData[0];
assert.strictEqual(event.type, 'password-reset');
assert.strictEqual(event.text, '[[error:reset-rate-limited]]');
async.parallel({
count: async.apply(db.sortedSetCount.bind(db), 'reset:issueDate', 0, Date.now()),
event: async.apply(events.getEvents, '', 0, 0),
}, (err, data) => {
assert.ifError(err);
assert.strictEqual(data.count, 2);
// Event validity
assert.strictEqual(data.event.length, 1);
const event = data.event[0];
assert.strictEqual(event.type, 'password-reset');
assert.strictEqual(event.text, '[[error:reset-rate-limited]]');
done();
});
});
});
it('should not error on invalid email (but not generate reset code)', async () => {
await socketUser.reset.send({ uid: 0 }, 'irregular@test.com');
const count = await db.sortedSetCount('reset:issueDate', 0, Date.now());
assert.strictEqual(count, 2);
it('should not error on invalid email (but not generate reset code)', (done) => {
socketUser.reset.send({ uid: 0 }, 'irregular@test.com', (err) => {
assert.ifError(err);
db.sortedSetCount('reset:issueDate', 0, Date.now(), (err, count) => {
assert.ifError(err);
assert.strictEqual(count, 2);
done();
});
});
});
it('should error on no email', (done) => {

File diff suppressed because it is too large Load Diff

View File

@@ -425,13 +425,14 @@ describe('Upload Controllers', () => {
it('should fail to delete a file as a non-admin', async () => {
const { response, body } = await request.delete(`${nconf.get('url')}/api/v3/files`, {
body: {
data: {
path: '/system/test.png',
},
jar: regularJar,
headers: {
'x-csrf-token': regular_csrf_token,
},
validateStatus: null,
});
assert.strictEqual(response.statusCode, 403);
assert.deepStrictEqual(body.status, {

View File

@@ -1,12 +1,12 @@
'use strict';
const assert = require('assert');
const async = require('async');
const fs = require('fs');
const path = require('path');
const nconf = require('nconf');
const validator = require('validator');
const jwt = require('jsonwebtoken');
const { setTimeout } = require('node:timers/promises');
const db = require('./mocks/databasemock');
const User = require('../src/user');
@@ -173,7 +173,7 @@ describe('User', () => {
});
describe('.uniqueUsername()', () => {
it('should deal with collisions', async () => {
it('should deal with collisions', (done) => {
const users = [];
for (let i = 0; i < 10; i += 1) {
users.push({
@@ -181,16 +181,25 @@ describe('User', () => {
email: `jane.doe${i}@example.com`,
});
}
for (const user of users) {
// eslint-disable-next-line no-await-in-loop
await User.create(user);
}
const username = await User.uniqueUsername({
username: 'Jane Doe',
userslug: 'jane-doe',
});
assert.strictEqual(username, 'Jane Doe 9');
async.series([
function (next) {
async.eachSeries(users, (user, next) => {
User.create(user, next);
}, next);
},
function (next) {
User.uniqueUsername({
username: 'Jane Doe',
userslug: 'jane-doe',
}, (err, username) => {
assert.ifError(err);
assert.strictEqual(username, 'Jane Doe 9');
next();
});
},
], done);
});
});
@@ -242,10 +251,12 @@ describe('User', () => {
});
describe('.getModeratorUids()', () => {
before(async () => {
await groups.create({ name: 'testGroup' });
await groups.join('cid:1:privileges:groups:moderate', 'testGroup');
await groups.join('testGroup', 1);
before((done) => {
async.series([
async.apply(groups.create, { name: 'testGroup' }),
async.apply(groups.join, 'cid:1:privileges:groups:moderate', 'testGroup'),
async.apply(groups.join, 'testGroup', 1),
], done);
});
it('should retrieve all users with moderator bit in category privilege', (done) => {
@@ -257,13 +268,38 @@ describe('User', () => {
});
});
after(async () => {
groups.leave('cid:1:privileges:groups:moderate', 'testGroup');
groups.destroy('testGroup');
after((done) => {
async.series([
async.apply(groups.leave, 'cid:1:privileges:groups:moderate', 'testGroup'),
async.apply(groups.destroy, 'testGroup'),
], done);
});
});
describe('.isReadyToPost()', () => {
it('should error when a user makes two posts in quick succession', (done) => {
meta.config = meta.config || {};
meta.config.postDelay = '10';
async.series([
async.apply(Topics.post, {
uid: testUid,
title: 'Topic 1',
content: 'lorem ipsum',
cid: testCid,
}),
async.apply(Topics.post, {
uid: testUid,
title: 'Topic 2',
content: 'lorem ipsum',
cid: testCid,
}),
], (err) => {
assert(err);
done();
});
});
it('should allow a post if the last post time is > 10 seconds', (done) => {
User.setUserField(testUid, 'lastposttime', +new Date() - (11 * 1000), () => {
Topics.post({
@@ -318,7 +354,7 @@ describe('User', () => {
const titles = new Array(10).fill('topic title');
const res = await Promise.allSettled(titles.map(async (title) => {
const { body } = await helpers.request('post', '/api/v3/topics', {
body: {
data: {
cid: testCid,
title: title,
content: 'the content',
@@ -448,19 +484,32 @@ describe('User', () => {
assert.equal(data.users[0].username, 'ipsearch_filter');
});
it('should sort results by username', async () => {
await User.create({ username: 'brian' });
await User.create({ username: 'baris' });
await User.create({ username: 'bzari' });
const data = await User.search({
uid: testUid,
query: 'b',
sortBy: 'username',
paginate: false,
it('should sort results by username', (done) => {
async.waterfall([
function (next) {
User.create({ username: 'brian' }, next);
},
function (uid, next) {
User.create({ username: 'baris' }, next);
},
function (uid, next) {
User.create({ username: 'bzari' }, next);
},
function (uid, next) {
User.search({
uid: testUid,
query: 'b',
sortBy: 'username',
paginate: false,
}, next);
},
], (err, data) => {
assert.ifError(err);
assert.equal(data.users[0].username, 'baris');
assert.equal(data.users[1].username, 'brian');
assert.equal(data.users[2].username, 'bzari');
done();
});
assert.equal(data.users[0].username, 'baris');
assert.equal(data.users[1].username, 'brian');
assert.equal(data.users[2].username, 'bzari');
});
});
@@ -945,7 +994,7 @@ describe('User', () => {
headers: {
'x-csrf-token': token,
},
body: {
data: {
type: 'external',
url: 'https://example.org/picture.jpg',
},
@@ -1168,6 +1217,7 @@ describe('User', () => {
// Accessing this page will mark the user's account as needing an updated email, below code undo's.
await request.post(`${nconf.get('url')}/register/abort`, {
jar,
validateStatus: null,
headers: {
'x-csrf-token': csrf_token,
},
@@ -1214,12 +1264,30 @@ describe('User', () => {
assert.equal(data[0].timestamp, now);
});
it('should return the correct ban reason', async () => {
await User.bans.ban(testUserUid, 0, '');
const data = await User.getModerationHistory(testUserUid);
assert.equal(data.bans.length, 1, 'one ban');
assert.equal(data.bans[0].reason, '[[user:info.banned-no-reason]]', 'no ban reason');
await User.bans.unban(testUserUid);
it('should return the correct ban reason', (done) => {
async.series([
function (next) {
User.bans.ban(testUserUid, 0, '', (err) => {
assert.ifError(err);
next(err);
});
},
function (next) {
User.getModerationHistory(testUserUid, (err, data) => {
assert.ifError(err);
assert.equal(data.bans.length, 1, 'one ban');
assert.equal(data.bans[0].reason, '[[user:info.banned-no-reason]]', 'no ban reason');
next(err);
});
},
], (err) => {
assert.ifError(err);
User.bans.unban(testUserUid, (err) => {
assert.ifError(err);
done();
});
});
});
it('should ban user permanently', (done) => {
@@ -1233,14 +1301,22 @@ describe('User', () => {
});
});
it('should ban user temporarily', async () => {
await User.bans.ban(testUserUid, Date.now() + 2000);
let isBanned = await User.bans.isBanned(testUserUid);
assert.equal(isBanned, true);
await setTimeout(3000);
isBanned = await User.bans.isBanned(testUserUid);
assert.equal(isBanned, false);
await User.bans.unban(testUserUid);
it('should ban user temporarily', (done) => {
User.bans.ban(testUserUid, Date.now() + 2000, (err) => {
assert.ifError(err);
User.bans.isBanned(testUserUid, (err, isBanned) => {
assert.ifError(err);
assert.equal(isBanned, true);
setTimeout(() => {
User.bans.isBanned(testUserUid, (err, isBanned) => {
assert.ifError(err);
assert.equal(isBanned, false);
User.bans.unban(testUserUid, done);
});
}, 3000);
});
});
});
it('should error if until is NaN', (done) => {
@@ -1318,19 +1394,26 @@ describe('User', () => {
describe('Digest.getSubscribers', () => {
const uidIndex = {};
before(async () => {
before((done) => {
const testUsers = ['daysub', 'offsub', 'nullsub', 'weeksub'];
await Promise.all(testUsers.map(async (username) => {
const uid = await User.create({ username, email: `${username}@example.com` });
if (username === 'nullsub') {
return;
}
uidIndex[username] = uid;
async.each(testUsers, (username, next) => {
async.waterfall([
async.apply(User.create, { username: username, email: `${username}@example.com` }),
function (uid, next) {
if (username === 'nullsub') {
return setImmediate(next);
}
const sub = username.slice(0, -3);
await User.updateDigestSetting(uid, sub);
await User.setSetting(uid, 'dailyDigestFreq', sub);
}));
uidIndex[username] = uid;
const sub = username.slice(0, -3);
async.parallel([
async.apply(User.updateDigestSetting, uid, sub),
async.apply(User.setSetting, uid, 'dailyDigestFreq', sub),
], next);
},
], next);
}, done);
});
it('should accurately build digest list given ACP default "null" (not set)', (done) => {
@@ -1342,38 +1425,71 @@ describe('User', () => {
});
});
it('should accurately build digest list given ACP default "day"', async () => {
await meta.configs.set('dailyDigestFreq', 'day');
const subs = await User.digest.getSubscribers('day');
it('should accurately build digest list given ACP default "day"', (done) => {
async.series([
async.apply(meta.configs.set, 'dailyDigestFreq', 'day'),
function (next) {
User.digest.getSubscribers('day', (err, subs) => {
assert.ifError(err);
assert.strictEqual(subs.includes(uidIndex.daysub.toString()), true); // daysub does get emailed
assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), false); // weeksub does not get emailed
assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub doesn't get emailed
assert.strictEqual(subs.includes(uidIndex.daysub.toString()), true); // daysub does get emailed
assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), false); // weeksub does not get emailed
assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub doesn't get emailed
next();
});
},
], done);
});
it('should accurately build digest list given ACP default "week"', async () => {
await meta.configs.set('dailyDigestFreq', 'week');
const subs = await User.digest.getSubscribers('week');
it('should accurately build digest list given ACP default "week"', (done) => {
async.series([
async.apply(meta.configs.set, 'dailyDigestFreq', 'week'),
function (next) {
User.digest.getSubscribers('week', (err, subs) => {
assert.ifError(err);
assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), true); // weeksub gets emailed
assert.strictEqual(subs.includes(uidIndex.daysub.toString()), false); // daysub gets emailed
assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub does not get emailed
assert.strictEqual(subs.includes(uidIndex.weeksub.toString()), true); // weeksub gets emailed
assert.strictEqual(subs.includes(uidIndex.daysub.toString()), false); // daysub gets emailed
assert.strictEqual(subs.includes(uidIndex.offsub.toString()), false); // offsub does not get emailed
next();
});
},
], done);
});
it('should accurately build digest list given ACP default "off"', async () => {
await meta.configs.set('dailyDigestFreq', 'off');
const subs = await User.digest.getSubscribers('day');
assert.strictEqual(subs.length, 1);
it('should accurately build digest list given ACP default "off"', (done) => {
async.series([
async.apply(meta.configs.set, 'dailyDigestFreq', 'off'),
function (next) {
User.digest.getSubscribers('day', (err, subs) => {
assert.ifError(err);
assert.strictEqual(subs.length, 1);
next();
});
},
], done);
});
});
describe('digests', () => {
let uid;
before(async () => {
uid = await User.create({ username: 'digestuser', email: 'test@example.com' });
await User.updateDigestSetting(uid, 'day');
await User.setSetting(uid, 'dailyDigestFreq', 'day');
await User.setSetting(uid, 'notificationType_test', 'notificationemail');
before((done) => {
async.waterfall([
function (next) {
User.create({ username: 'digestuser', email: 'test@example.com' }, next);
},
function (_uid, next) {
uid = _uid;
User.updateDigestSetting(uid, 'day', next);
},
function (next) {
User.setSetting(uid, 'dailyDigestFreq', 'day', next);
},
function (next) {
User.setSetting(uid, 'notificationType_test', 'notificationemail', next);
},
], done);
});
it('should send digests', async () => {
@@ -1449,7 +1565,7 @@ describe('User', () => {
uid: uid,
}, nconf.get('secret'));
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`);
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`, { validateStatus: null });
assert.strictEqual(response.statusCode, 404);
});
@@ -1459,12 +1575,12 @@ describe('User', () => {
uid: uid,
}, nconf.get('secret'));
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`);
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`, { validateStatus: null });
assert.strictEqual(response.statusCode, 404);
});
it('should return errors on missing token', async () => {
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/`);
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/`, { validateStatus: null });
assert.strictEqual(response.statusCode, 404);
});
@@ -1475,7 +1591,7 @@ describe('User', () => {
uid: uid,
}, `${nconf.get('secret')}aababacaba`);
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`);
const { response } = await request.post(`${nconf.get('url')}/email/unsubscribe/${token}`, { validateStatus: null });
assert.strictEqual(response.statusCode, 403);
});
});
@@ -1676,17 +1792,36 @@ describe('User', () => {
}
});
it('should set moderation note', async () => {
const adminUid = await User.create({ username: 'noteadmin' });
await groups.join('administrators', adminUid);
await socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'this is a test user' });
await setTimeout(50);
await socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: '<svg/onload=alert(document.location);//' });
const notes = await User.getModerationNotes(testUid, 0, -1);
assert.equal(notes[0].note, '&lt;svg&#x2F;onload=alert(document.location);&#x2F;&#x2F;');
assert.equal(notes[0].uid, adminUid);
assert.equal(notes[1].note, 'this is a test user');
assert(notes[0].timestamp);
it('should set moderation note', (done) => {
let adminUid;
async.waterfall([
function (next) {
User.create({ username: 'noteadmin' }, next);
},
function (_adminUid, next) {
adminUid = _adminUid;
groups.join('administrators', adminUid, next);
},
function (next) {
socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: 'this is a test user' }, next);
},
function (next) {
setTimeout(next, 50);
},
function (next) {
socketUser.setModerationNote({ uid: adminUid }, { uid: testUid, note: '<svg/onload=alert(document.location);//' }, next);
},
function (next) {
User.getModerationNotes(testUid, 0, -1, next);
},
], (err, notes) => {
assert.ifError(err);
assert.equal(notes[0].note, '&lt;svg&#x2F;onload=alert(document.location);&#x2F;&#x2F;');
assert.equal(notes[0].uid, adminUid);
assert.equal(notes[1].note, 'this is a test user');
assert(notes[0].timestamp);
done();
});
});
it('should get unread count 0 for guest', async () => {