diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 166f0b20b1..8ef171106e 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -20,6 +20,9 @@ - **What you expected:** diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index d812450608..a648be6ce0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -80,7 +80,7 @@ jobs: - run: cp install/package.json package.json - name: Install Node - uses: actions/setup-node@v1 + uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 969c75f886..dfd9ca2d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,142 @@ +#### v1.16.0 (2020-12-17) + +##### Breaking Changes + +* enable topic thumbnails across the board [breaking] (9342d611) +* #8808, remove utils.slugify (4a0d8833) + +##### Chores + +* **deps:** + * update dependency husky to v4.3.6 (2371b432) + * bump persona to 10.3.9 (91899329) + * bump composer to 6.5.1, re: #9067 (228cfa67) + * update dependency husky to v4.3.5 (48a31763) + * update dependency husky to v4.3.4 (cf5c482d) + * update dependency eslint to v7.15.0 (f4c4d671) + * update dependency lint-staged to v10.5.3 (3e6f7359) +* appease codeclimate (9f62df15) +* add comment for clarification (6037f5ee) +* incrementing version number - v1.15.5 (57cd1343) +* update changelog for v1.15.5 (b0299326) +* **i18n:** fallbacks for new topic thumb keys (15f1a089) + +##### New Features + +* add new client side hooks (a15ef53c) +* remove max age since cache is cleared when thumbs change (ab96f526) +* show alt text instead of images in teasers (#9107) (d28581eb) +* migration of old topic thumbs to new format (74d73313) +* allow plugins to override ACP relogin challenge (4c87f301) +* add user.email.confirmByUid for sso plugins (80de572a) +* add thumbs to category data return (24e754d1) +* broken test for bad topic thumbs logic (ce8057f3) +* clent-side modal for managing topic thumbs (a30c8ab5) +* raise maximum thumb size to 512 (37c367d6) +* associate topic thumbs with post uploads (for the mainPid) (1c5cdb51) +* helper method to get thumbs by pid (cb7e4cda) +* closes #9048, tests for topic thumbs routes, write API schema (59506833) +* tests for topic thumbs (4152aa55) +* server-side work for #9047 (ef7d6db9) +* core work for #9042, thumb deletion now accepts uuids (b5d910f5) +* more work on topic thumbs refactor (90497e3e) +* expose uploaded thumbnails to client-side via API (1257aa98) +* server-side routes for handling multiple topic thumbnails (7e9e08f7) +* allow uploadThumb controller to be called in code (98cd9e35) +* move upgrade script and make it shorter (60e7de0d) +* allow clicks on navigator, clean dupe code (74274b60) +* socket.io 3 changes (#8845) (1c45fa1b) +* **deps:** update lavender to allow category sections (6d186be0) + +##### Bug Fixes + +* **deps:** + * update dependency nodebb-plugin-composer-default to v6.5.4 (#9120) (fff0cea6) + * update dependency nodebb-theme-slick to v1.3.7 (#9112) (30688b1b) + * update dependency nodebb-theme-lavender to v5.0.17 (#9111) (877f4673) + * update dependency nodebb-theme-vanilla to v11.3.10 (ff18cdfa) + * update dependency validator to v13.5.2 (#9094) (5d718348) + * update dependency nodebb-theme-vanilla to v11.3.9 (#9091) (f37dbeed) + * update dependency nodebb-plugin-composer-default to v6.5.3 (d036408d) + * update dependency nodebb-plugin-composer-default to v6.5.2 (b07fb9ab) + * bump composer-default to 6.5.0 (0db49121) + * update dependency autoprefixer to v10.1.0 (024d1fef) + * update dependency nodebb-theme-persona to v10.3.8 (#9084) (25f697b1) + * update socket.io packages to v3.0.4 (62463430) + * update dependency nodebb-theme-persona to v10.3.7 (c22cdb51) + * update dependency nodebb-theme-persona to v10.3.6 (#9077) (5937fbaf) + * update dependency nodebb-plugin-mentions to v2.13.6 (#9071) (a535350f) + * update dependency nodebb-theme-slick to v1.3.6 (#9072) (19c438c6) + * update dependency nodebb-widget-essentials to v5 (#9070) (d7f5efd9) + * update dependency nodebb-plugin-markdown to v8.12.4 (8fb814ba) + * update dependency nodebb-theme-persona to v10.3.5 (#9060) (0d082280) + * update dependency nodebb-theme-persona to v10.3.4 (#9059) (84e4e480) + * update dependency nodebb-theme-persona to v10.3.3 (3d7e2e1e) + * update dependency nodebb-theme-persona to v10.3.2 (#9056) (f49ce4ad) + * update dependency nodebb-theme-persona to v10.3.1 (#9054) (344caf5c) + * update dependency nodebb-theme-lavender to v5.0.15 (#9053) (e7d72d8a) + * update dependency nodebb-theme-persona to v10.3.0 (#9052) (dcd6fbaf) +* api usage (feecd665) +* #9117, lower query before search (4404e32e) +* #9114, fix client side groups update for memberPostCids (3ed55799) +* test (2dee3cbe) +* don't check "select all" if there are no enabled checkboxes (3ba05755) +* #9074, fix svg uploads (8f938eba) +* #9100 topic thumbs in OG image tags (ab987408) +* update version removal comments to 1.17 for some features (378a3a69) +* postgres is slow:tm: (05dd8597) +* derp? (f8dff94a) +* attempted fix for psql test in topic thumbs (9a4ea04a) +* use getSortedSetRange instead of getSortedSetsMembers (edf67f34) +* tests (bd5c4a5c) +* bad topic thumbs logic on local thumb upload (e83baa97) +* #9092, Topic thumbnails do not work with third-party uploaders (3e54b70c) +* move topic thumb tests to root level, so they actually get run by mocha (dd448e2b) +* tests for topic thumbs (9681557f) +* iteration logic bug (2170c400) +* spec (ae943974) +* changes to thumb resizing logic (67cf5e83) +* use file lib instead of direct fs module access (08736b18) +* added back missing topic thumb tests that were removed in last commit (c043cfeb) +* tests (5ec3b3d0) +* hack uploader to handle a response from v3 write api (41379e27) +* #9055, non-standard API response from addThumbs route (340387c1) +* do not allow thumb deletion route to arbitrarily delete other files in uploads folder (c09c238e) +* missing file added (ef10b6b7) +* references to since-removed Topics.thumbs.resizeAndUpload (1f0c1cd2) +* #9041, remove Topics.thumbs.resizeAndUpload() (43dc3e3e) +* #9040 (708b1c33) +* spec (1949d20a) +* #9085, dont prevent admins from deleting other users (0f480be6) +* show errors when user delete fails (ff2aa17b) +* dont start logout timer if adminReloginDuration is disabled (dd9ed236) +* #9045, no post usage info if '/files/' path received (efa4eca0) +* reconnectin no longer fires on socket.io 3 (13d5a144) +* default values, clamp postsPerPage/topicsPerPage to max (1f32d387) +* #9081, load raw settings before merging (9da0ed40) +* #9068 (86f0f82b) +* remove old utils.slugify tests (10cfdd4c) +* dont strip tags (792e9e70) +* #9065, settings v2/v3 conflict (91c20cec) +* #9063, missing handler for passwordless accounts in admin.checkPrivileges middleware (970ccb5a) +* timeago in navigation (a389a31b) +* navigation fixes (163d1a39) +* cache some jquery objects (73d2f51d) +* add ev.cancelable (63d08395) +* #9046, pretranslate string (790f4e45) +* redirect external with absolute urls (648f6215) +* external path for subfolder installs (458bfc0f) +* **spec:** broken link to status component (d31aae16) + +##### Performance Improvements + +* don't load thumbs if disabled globally, cache thumb results (2d5a224b) +* dont build identical langs (bb6cc49c) + +##### Refactors + +* topic thumbs lib to topics.thumbs (4fc9da81) + #### v1.15.5 (2020-12-03) ##### Chores diff --git a/install/data/defaults.json b/install/data/defaults.json index 1f08ca4ad8..556cf055c7 100644 --- a/install/data/defaults.json +++ b/install/data/defaults.json @@ -150,5 +150,7 @@ "useCompression": 0, "updateUrlWithPostIndex": 1, "composer:showHelpTab": 1, - "composer:allowPluginHelp": 1 + "composer:allowPluginHelp": 1, + "maxReconnectionAttempts": 5, + "reconnectionDelay": 1500 } \ No newline at end of file diff --git a/install/package.json b/install/package.json index eb79ff2527..d721792a69 100644 --- a/install/package.json +++ b/install/package.json @@ -2,7 +2,7 @@ "name": "nodebb", "license": "GPL-3.0", "description": "NodeBB Forum", - "version": "1.16.0", + "version": "1.16.1-beta.0", "homepage": "http://www.nodebb.org", "repository": { "type": "git", @@ -39,9 +39,9 @@ "ace-builds": "^1.4.9", "archiver": "^5.0.0", "async": "^3.2.0", - "autoprefixer": "10.1.0", + "autoprefixer": "10.2.0", "bcryptjs": "2.4.3", - "benchpressjs": "2.3.0", + "benchpressjs": "2.4.0", "body-parser": "^1.19.0", "bootbox": "4.4.0", "bootstrap": "^3.4.1", @@ -69,7 +69,7 @@ "express-useragent": "^1.0.13", "graceful-fs": "^4.2.3", "helmet": "^4.0.0", - "html-to-text": "^5.1.1", + "html-to-text": "6.0.0", "ipaddr.js": "^2.0.0", "jquery": "3.5.1", "jquery-deserialize": "2.0.0-rc1", @@ -91,7 +91,7 @@ "mousetrap": "^1.6.5", "@nodebb/bootswatch": "3.4.2", "nconf": "^0.11.0", - "nodebb-plugin-composer-default": "6.5.4", + "nodebb-plugin-composer-default": "6.5.5", "nodebb-plugin-dbsearch": "4.1.2", "nodebb-plugin-emoji": "^3.3.0", "nodebb-plugin-emoji-android": "2.0.0", @@ -101,10 +101,10 @@ "nodebb-plugin-spam-be-gone": "0.7.7", "nodebb-rewards-essentials": "0.1.4", "nodebb-theme-lavender": "5.0.17", - "nodebb-theme-persona": "10.3.9", + "nodebb-theme-persona": "10.3.17", "nodebb-theme-slick": "1.3.7", "nodebb-theme-vanilla": "11.3.10", - "nodebb-widget-essentials": "5.0.0", + "nodebb-widget-essentials": "5.0.2", "nodemailer": "^6.4.6", "nprogress": "0.2.0", "passport": "^0.4.1", @@ -125,12 +125,12 @@ "sanitize-html": "^2.0.0", "semver": "^7.2.1", "serve-favicon": "^2.5.0", - "sharp": "0.26.3", + "sharp": "0.27.0", "sitemap": "^6.1.0", "slideout": "1.0.1", - "socket.io": "3.0.4", + "socket.io": "3.0.5", "socket.io-adapter-cluster": "^1.0.1", - "socket.io-client": "3.0.4", + "socket.io-client": "3.0.5", "socket.io-redis": "6.0.1", "sortablejs": "1.10.2", "spdx-license-list": "^6.1.0", @@ -153,7 +153,7 @@ "@commitlint/cli": "11.0.0", "@commitlint/config-angular": "11.0.0", "coveralls": "3.1.0", - "eslint": "7.15.0", + "eslint": "7.17.0", "eslint-config-airbnb-base": "14.2.1", "eslint-plugin-import": "2.22.1", "grunt": "1.3.0", diff --git a/public/language/ar/admin/manage/privileges.json b/public/language/ar/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/ar/admin/manage/privileges.json +++ b/public/language/ar/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/ar/error.json b/public/language/ar/error.json index 2346aded87..078a2d95c9 100644 --- a/public/language/ar/error.json +++ b/public/language/ar/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/bg/admin/manage/privileges.json b/public/language/bg/admin/manage/privileges.json index 0b481eb69f..d61fb1ad39 100644 --- a/public/language/bg/admin/manage/privileges.json +++ b/public/language/bg/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Категории", "admin-privileges": "Правомощия", "admin-users": "Потребители", + "admin-admins-mods": "Администратори и модератори", + "admin-groups": "Групи", + "admin-tags": "Етикети", "admin-settings": "Настройки", "alert.confirm-moderate": "Наистина ли искате да дадете правомощието за модериране на тази потребителска група? Тази група е публична и всеки може свободно да се присъедини към нея.", + "alert.confirm-admins-mods": "Наистина ли искате да дадете правото „Администратори и модератори“ на този потребител/група? Потребителите с това право могат да променят правомощията на други групи, включително да им дават правото на супер администратори", "alert.confirm-save": "Моля, потвърдете желанието си да запазите тези правомощия", "alert.saved": "Промените по правомощията са запазени и приложени", "alert.confirm-discard": "Наистина ли искате да отхвърлите промените по правомощията?", diff --git a/public/language/bg/error.json b/public/language/bg/error.json index d61e536cf1..0a6461682d 100644 --- a/public/language/bg/error.json +++ b/public/language/bg/error.json @@ -175,5 +175,6 @@ "already-blocked": "Този потребител вече е блокиран", "already-unblocked": "Този потребител вече е отблокиран", "no-connection": "Изглежда има проблем с връзката Ви с Интернет", + "socket-reconnect-failed": "В момента сървърът е недостъпен. Натиснете тук, за да опитате отново, или опитайте пак по-късно.", "plugin-not-whitelisted": "Добавката не може да бъде инсталирана – само добавки, одобрени от пакетния мениджър на NodeBB могат да бъдат инсталирани чрез ACP" } \ No newline at end of file diff --git a/public/language/bn/admin/manage/privileges.json b/public/language/bn/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/bn/admin/manage/privileges.json +++ b/public/language/bn/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/bn/error.json b/public/language/bn/error.json index f2b1b22551..6fd47fffaf 100644 --- a/public/language/bn/error.json +++ b/public/language/bn/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/cs/admin/manage/privileges.json b/public/language/cs/admin/manage/privileges.json index 5e3a34dc4b..35e7a0dedd 100644 --- a/public/language/cs/admin/manage/privileges.json +++ b/public/language/cs/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Kategorie", "admin-privileges": "Oprávnění", "admin-users": "Uživatelé", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Nastavení", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/cs/error.json b/public/language/cs/error.json index 1854352793..fbedf86258 100644 --- a/public/language/cs/error.json +++ b/public/language/cs/error.json @@ -175,5 +175,6 @@ "already-blocked": "Tento uživatel již byl zablokován.", "already-unblocked": "Tento uživatel již byl odblokován", "no-connection": "Zdá se, že nastal problém s připojením k internetu", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/da/admin/manage/privileges.json b/public/language/da/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/da/admin/manage/privileges.json +++ b/public/language/da/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/da/error.json b/public/language/da/error.json index b4550eb4dc..9f2b9c8eff 100644 --- a/public/language/da/error.json +++ b/public/language/da/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/de/admin/manage/privileges.json b/public/language/de/admin/manage/privileges.json index 767631b11b..c771cd2ded 100644 --- a/public/language/de/admin/manage/privileges.json +++ b/public/language/de/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Nutzende Personen", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Einstellungen", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/de/error.json b/public/language/de/error.json index 9a67930c82..e20858a89d 100644 --- a/public/language/de/error.json +++ b/public/language/de/error.json @@ -175,5 +175,6 @@ "already-blocked": "Dieser Nutzer ist bereits gesperrt", "already-unblocked": "Dieser Nutzer ist bereits entsperrt", "no-connection": "Es scheint als gäbe es ein Problem mit deiner Internetverbindung", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/el/admin/manage/privileges.json b/public/language/el/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/el/admin/manage/privileges.json +++ b/public/language/el/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/el/error.json b/public/language/el/error.json index 727400039b..695f0f3d1e 100644 --- a/public/language/el/error.json +++ b/public/language/el/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/en-GB/admin/manage/privileges.json b/public/language/en-GB/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/en-GB/admin/manage/privileges.json +++ b/public/language/en-GB/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 63b5ebdee5..7a08489cb5 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -208,6 +208,7 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } diff --git a/public/language/en-US/admin/manage/privileges.json b/public/language/en-US/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/en-US/admin/manage/privileges.json +++ b/public/language/en-US/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/en-US/error.json b/public/language/en-US/error.json index c541eff485..ebf6de4ec7 100644 --- a/public/language/en-US/error.json +++ b/public/language/en-US/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/en-x-pirate/admin/manage/privileges.json b/public/language/en-x-pirate/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/en-x-pirate/admin/manage/privileges.json +++ b/public/language/en-x-pirate/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/en-x-pirate/error.json b/public/language/en-x-pirate/error.json index c541eff485..ebf6de4ec7 100644 --- a/public/language/en-x-pirate/error.json +++ b/public/language/en-x-pirate/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/es/admin/manage/privileges.json b/public/language/es/admin/manage/privileges.json index 118cc2d722..4a2b3d375f 100644 --- a/public/language/es/admin/manage/privileges.json +++ b/public/language/es/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/es/error.json b/public/language/es/error.json index d0ac6ac9e1..91c3a067e4 100644 --- a/public/language/es/error.json +++ b/public/language/es/error.json @@ -175,5 +175,6 @@ "already-blocked": "Este usuario ya está bloqueado.", "already-unblocked": "Este usuario ya está desbloqueado.", "no-connection": "Parece haber un problema con tu conexión a internet", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/et/admin/manage/privileges.json b/public/language/et/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/et/admin/manage/privileges.json +++ b/public/language/et/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/et/error.json b/public/language/et/error.json index c9c7704681..081c2623d0 100644 --- a/public/language/et/error.json +++ b/public/language/et/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/fa-IR/admin/manage/privileges.json b/public/language/fa-IR/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/fa-IR/admin/manage/privileges.json +++ b/public/language/fa-IR/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/fa-IR/error.json b/public/language/fa-IR/error.json index a6ca1049a3..5b695d287f 100644 --- a/public/language/fa-IR/error.json +++ b/public/language/fa-IR/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "به نظر می رسد اینترنت شما مشکل دارد", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/fi/admin/manage/privileges.json b/public/language/fi/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/fi/admin/manage/privileges.json +++ b/public/language/fi/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/fi/error.json b/public/language/fi/error.json index b61ce646d6..7828030e84 100644 --- a/public/language/fi/error.json +++ b/public/language/fi/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/fr/admin/manage/privileges.json b/public/language/fr/admin/manage/privileges.json index 487bb04fdb..2ed0770103 100644 --- a/public/language/fr/admin/manage/privileges.json +++ b/public/language/fr/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Catégories", "admin-privileges": "Privilèges", "admin-users": "Utilisateurs", + "admin-admins-mods": "Admin & Modo", + "admin-groups": "Groupes", + "admin-tags": "Mots Clés", "admin-settings": "Paramètres", "alert.confirm-moderate": "Voulez-vous vraiment accorder le privilège de modération à ce groupe d'utilisateurs ? Ce groupe est public et tous les utilisateurs peuvent le rejoindre à volonté.", + "alert.confirm-admins-mods": "Voulez-vous vraiment attribuer les droits aux & quot; d'Administrations & amp; Modérations & quot; à cet utilisateur / groupe? Les utilisateurs disposant de ce privilège peuvent promouvoir et rétrograder d'autres utilisateurs à des postes privilégiés, y compris le super administrateur", "alert.confirm-save": "Veuillez confirmer votre intention de sauvegarder ces privilèges", "alert.saved": "Changements de privilèges enregistrés et appliqués", "alert.confirm-discard": "Êtes-vous sûr de vouloir annuler vos modifications de privilèges ?", diff --git a/public/language/fr/error.json b/public/language/fr/error.json index 17a38aa8f4..9c712fd0a7 100644 --- a/public/language/fr/error.json +++ b/public/language/fr/error.json @@ -175,5 +175,6 @@ "already-blocked": "Cet utilisateur est déjà bloqué", "already-unblocked": "Cet utilisateur est déjà débloqué", "no-connection": "Il semble y avoir un problème avec votre connexion Internet", + "socket-reconnect-failed": "Serveur inaccessible pour le moment. Cliquez ici pour réessayer ou réessayez plus tard", "plugin-not-whitelisted": "Impossible d'installer le plug-in – seuls les plugins mis en liste blanche dans le gestionnaire de packages NodeBB peuvent être installés via l'ACP" } \ No newline at end of file diff --git a/public/language/gl/admin/manage/privileges.json b/public/language/gl/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/gl/admin/manage/privileges.json +++ b/public/language/gl/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/gl/error.json b/public/language/gl/error.json index 569d9e04f0..41356ebc76 100644 --- a/public/language/gl/error.json +++ b/public/language/gl/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/he/admin/appearance/skins.json b/public/language/he/admin/appearance/skins.json index 4f49fb6e2b..ea33d800bc 100644 --- a/public/language/he/admin/appearance/skins.json +++ b/public/language/he/admin/appearance/skins.json @@ -1,9 +1,9 @@ { - "loading": "טוען סקין...", - "homepage": "דף הבית", - "select-skin": "סקין נבחר", - "current-skin": "סקין נוכחי", - "skin-updated": "סקין מעודכן", - "applied-success": "העיצוב %1 הוחל בהצלחה", - "revert-success": "העיצובהוחזר לצבעים הבסיסיים" + "loading": "טוען עיצובים", + "homepage": "דף הפרוייקט", + "select-skin": "בחר עיצוב זה", + "current-skin": "עיצוב נוכחי", + "skin-updated": "עיצוב עודכן", + "applied-success": "עיצוב %1 הוחל בהצלחה", + "revert-success": "עיצוב הוחזר לצבעים בסיסיים" } \ No newline at end of file diff --git a/public/language/he/admin/development/logger.json b/public/language/he/admin/development/logger.json index 6ab9558149..4fdf500f14 100644 --- a/public/language/he/admin/development/logger.json +++ b/public/language/he/admin/development/logger.json @@ -1,12 +1,12 @@ { - "logger-settings": "Logger Settings", - "description": "By enabling the check boxes, you will receive logs to your terminal. If you specify a path, logs will then be saved to a file instead. HTTP logging is useful for collecting statistics about who, when, and what people access on your forum. In addition to logging HTTP requests, we can also log socket.io events. Socket.io logging, in combination with redis-cli monitor, can be very helpful for learning NodeBB's internals.", - "explanation": "Simply check/uncheck the logging settings to enable or disable logging on the fly. No restart needed.", - "enable-http": "Enable HTTP logging", - "enable-socket": "Enable socket.io event logging", - "file-path": "Path to log file", + "logger-settings": "הגדרות מנהל הרישום", + "description": "על-ידי הפיכת תיבות הסימון לזמינות, תקבל יומני רישום למסוף שלך. אם תציין נתיב, יומני הרישום יישמרו בקובץ במקום זאת. רישום HTTP שימושי לאיסוף נתונים סטטיסטיים אודות מי ומתי אנשים נכנסים לפורום שלך. בנוסף לרישום בקשות ה-HTTP, אנו יכולים גם לרשום אירועי Socket.io, אשר בשילוב עם מודד redis-cli, יכול להיות מאוד מועיל ללימוד הפנימיים של NodeBB.", + "explanation": "הפעל או ​בטל את סימון הגדרות הרישום בכדי לאפשר או להשבית כניסה במהירות. אין צורך בהפעלה מחדש.", + "enable-http": "הפעל רישום HTTP", + "enable-socket": "הפעל רישום אירועים ב-socket.io", + "file-path": "נתיב קובץ יומן רישום", "file-path-placeholder": "/path/to/log/file.log ::: leave blank to log to your terminal", - "control-panel": "Logger Control Panel", - "update-settings": "Update Logger Settings" + "control-panel": "לוח בקרת מנהל רישום", + "update-settings": "עדכן הגדרות מנהל רישום" } \ No newline at end of file diff --git a/public/language/he/admin/extend/rewards.json b/public/language/he/admin/extend/rewards.json index 3c4310ffef..aebf2d1986 100644 --- a/public/language/he/admin/extend/rewards.json +++ b/public/language/he/admin/extend/rewards.json @@ -1,17 +1,17 @@ { - "rewards": "פרסים", - "condition-if-users": "אם המשתמשים", - "condition-is": "הוא:", - "condition-then": "אז:", - "max-claims": "מספר הפעמים הניתן לדרוש פרס", + "rewards": "תגמולים", + "condition-if-users": "אם משתמש", + "condition-is": "Is:", + "condition-then": "תגמל ב:", + "max-claims": "מספר פעמים בה ניתן לדרוש תגמול", "zero-infinite": "הזן 0 לאינסוף", "delete": "מחק", "enable": "הפעל", - "disable": "בטל", - "control-panel": "בקרת פרסים", - "new-reward": "פרס חדש", + "disable": "השבת", + "control-panel": "בקרת תגמולים", + "new-reward": "תגמול חדש", - "alert.delete-success": "הפרס נמחק בהצלחה", - "alert.no-inputs-found": "פרס לא חוקי - לא נמצא מידע!", - "alert.save-success": "הפרסים נשמרו בהצלחה" + "alert.delete-success": "תגמול נמחק בהצלחה", + "alert.no-inputs-found": "תגמול לא חוקי - לא נמצא מידע!", + "alert.save-success": "תגמולים נשמרו בהצלחה" } \ No newline at end of file diff --git a/public/language/he/admin/manage/privileges.json b/public/language/he/admin/manage/privileges.json index fe96ec8647..a2bff34095 100644 --- a/public/language/he/admin/manage/privileges.json +++ b/public/language/he/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "קטגוריות", "admin-privileges": "הרשאות", "admin-users": "משתמשים", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "הגדרות", "alert.confirm-moderate": "האם אתה בטוח שברצונך להעניק הרשאות מודרטור לקבוצת משתמשים זו? הקבוצה היא ציבורית, וכל משתמש יכול להצטרף כרצונו.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "נא אשר את הגדרת ההרשאות", "alert.saved": "שינויי הרשאות נשמרו והוחלו", "alert.confirm-discard": "האם אתה בטוח שברצונך לבטל את שינויי ההרשאות שלך?", diff --git a/public/language/he/admin/menu.json b/public/language/he/admin/menu.json index 7c79dd8167..ed2932db94 100644 --- a/public/language/he/admin/menu.json +++ b/public/language/he/admin/menu.json @@ -2,7 +2,7 @@ "dashboard": "לוח מחוונים", "section-general": "כללי", - "section-manage": "נהל", + "section-manage": "ניהול", "manage/categories": "קטגוריות", "manage/privileges": "הרשאות", "manage/tags": "תגיות", @@ -63,7 +63,7 @@ "advanced/logs": "רישומים", "advanced/errors": "שגיאות", "advanced/cache": "עוגיות", - "development/logger": "כותב הרישומים", + "development/logger": "מנהל הרישומים", "development/info": "מידע", "rebuild-and-restart-forum": "בנה והפעל מחדש את הפורום", diff --git a/public/language/he/admin/settings/chat.json b/public/language/he/admin/settings/chat.json index 67898611e7..23fbc77000 100644 --- a/public/language/he/admin/settings/chat.json +++ b/public/language/he/admin/settings/chat.json @@ -1,12 +1,12 @@ { - "chat-settings": "Chat Settings", - "disable": "Disable chat", - "disable-editing": "Disable chat message editing/deletion", - "disable-editing-help": "Administrators and global moderators are exempt from this restriction", - "max-length": "Maximum length of chat messages", - "max-room-size": "Maximum number of users in chat rooms", - "delay": "Time between chat messages in milliseconds", - "notification-delay": "Notification delay for chat messages. (0 for no delay)", - "restrictions.seconds-edit-after": "Number of seconds a chat message will remain editable. (0 disabled)", - "restrictions.seconds-delete-after": "Number of seconds a chat message will remain deletable. (0 disabled)" + "chat-settings": "הגדרות צ'אט", + "disable": "השבת צ'אט", + "disable-editing": "השבת עריכה/מחיקה של הודעות צ'אט", + "disable-editing-help": "מנהלים ומודרטורים גולבליים אינם נכללים בהגבלה זו", + "max-length": "אורך מקסימלי של הודעת צ'אט", + "max-room-size": "מספר המשתמשים המרבי בחדרי צ'אט", + "delay": "זמן המתנה בין הודעות צ'אט - באלפיות שניה", + "notification-delay": "עיכוב התראות להודעות צ'אט. (0 ללא עיכוב)", + "restrictions.seconds-edit-after": "מספר השניות בה ניתן לערוך הודעת צ'אט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", + "restrictions.seconds-delete-after": "מספר השניות בה ניתן למחוק הודעת צ'אט מרגע פרסומו (כתבו 0 להפוך ללא זמין)" } \ No newline at end of file diff --git a/public/language/he/admin/settings/post.json b/public/language/he/admin/settings/post.json index c5e3c1838b..ecca8e24f7 100644 --- a/public/language/he/admin/settings/post.json +++ b/public/language/he/admin/settings/post.json @@ -20,9 +20,9 @@ "restrictions.seconds-between-new": "שניות בין פוסטים עבור משתמשים חדשים", "restrictions.rep-threshold": "סף המוניטין לפני ביטול המגבלות הללו", "restrictions.seconds-before-new": "שניות לפני שמשתמש חדש יוכל לפרסם את הפוסט הראשון שלו", - "restrictions.seconds-edit-after": "מספר השניות בה ניתן לערוך פוסט מרגע פרסומו (כתוב 0 בכדי להפוך ללא זמין)", - "restrictions.seconds-delete-after": "מספר השניות בה ניתן למחוק פוסט מרגע פרסומו (כתוב 0 בכדי להפוך ללא זמין)", - "restrictions.replies-no-delete": "מספר תשובות בנושא שלאחריו לא יוכל מפרסם הנושא למחקו (כתוב 0 בכדי להפוך ללא זמין)", + "restrictions.seconds-edit-after": "מספר השניות בה ניתן לערוך פוסט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", + "restrictions.seconds-delete-after": "מספר השניות בה ניתן למחוק פוסט מרגע פרסומו (כתבו 0 להפוך ללא זמין)", + "restrictions.replies-no-delete": "מספר תשובות בנושא שלאחריו לא יוכל מפרסם הנושא למחקו (כתבו 0 להפוך ללא זמין)", "restrictions.min-title-length": "אורך כותרת מינימלי", "restrictions.max-title-length": "אורך כותרת מקסימלי", "restrictions.min-post-length": "אורך פוסט מינימלי", diff --git a/public/language/he/error.json b/public/language/he/error.json index 58eb587197..3f07d4829d 100644 --- a/public/language/he/error.json +++ b/public/language/he/error.json @@ -175,5 +175,6 @@ "already-blocked": "המשתמש כבר חסום", "already-unblocked": "המשתמש כבר לא חסום", "no-connection": "נראה שיש בעיות בחיבור האינטרנט שלך..", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "לא ניתן להתקין את התוסף – ניתן להתקין דרך הניהול רק תוספים שנמצאים ברשימה הלבנה של מנהל החבילות של NodeBB." } \ No newline at end of file diff --git a/public/language/he/global.json b/public/language/he/global.json index 305f0018b8..ae63ba9ad9 100644 --- a/public/language/he/global.json +++ b/public/language/he/global.json @@ -3,27 +3,27 @@ "search": "חיפוש", "buttons.close": "סגור", "403.title": "גישה נדחתה", - "403.message": "נראה שהגעת לעמוד שאין לך הרשאה לצפות בו", - "403.login": "נסה להתחבר.", + "403.message": "הגעתם לעמוד שאין לכם הרשאת צפייה בו", + "403.login": "נסו להתחבר.", "404.title": "לא נמצא", - "404.message": "נראה שהגעת לעמוד שלא קיים. חזור לעמוד הבית", + "404.message": "הגעתם לעמוד שאינו קיים. חזרו לעמוד הבית", "500.title": "שגיאה פנימית.", "500.message": "אופס! נראה שמשהו השתבש!", "400.title": "בקשה שגויה", - "400.message": "נראה שהלינק הזה לא תקין, בדוק ונסה שוב. אחרת, חזור לעמוד הבית.", + "400.message": "לינק לא תקין, בדקו ונסו שוב. או, חזרו לעמוד הבית.", "register": "הרשמה", "login": "התחברות", - "please_log_in": "אנא התחבר", + "please_log_in": "נא להתחבר", "logout": "יציאה", - "posting_restriction_info": "העלאת פוסטים מוגבלת למשתמשים רשומים בלבד כרגע, ", + "posting_restriction_info": "כתיבת פוסטים מאופשר למשתמשים רשומים בלבד, לחצו כאן כדי להתחבר.", "welcome_back": "ברוכים השבים", - "you_have_successfully_logged_in": "התחברת בהצלחה", + "you_have_successfully_logged_in": "התחברתם בהצלחה", "save_changes": "שמור שינויים", "save": "שמור", "close": "סגור", - "pagination": "עימוד", + "pagination": "הגדרות עמוד", "pagination.out_of": "%1 מתוך %2", - "pagination.enter_index": "יש להכניס אינדקס", + "pagination.enter_index": "הכניסו אינדקס", "header.admin": "ניהול", "header.categories": "קטגוריות", "header.recent": "פוסטים אחרונים", @@ -40,14 +40,14 @@ "header.navigation": "ניווט", "notifications.loading": "טוען התראות", "chats.loading": "טוען צ'אטים", - "motd.welcome": "ברוכים הבאים ל NodeBB, פלטפורמת הדיון של העתיד", + "motd.welcome": "ברוכים הבאים ל-NodeBB, פלטפורמות הדיון העתידני", "previouspage": "העמוד הקודם", "nextpage": "העמוד הבא", "alert.success": "הצלחה", "alert.error": "שגיאה", "alert.banned": "מורחק", - "alert.banned.message": "זה עתה נחסם חשבונך, הנך יוצא מן המערכת.", - "alert.unfollow": "אתה כבר לא עוקב אחרי %1!", + "alert.banned.message": "חשבונך נחסם, מתבצע יציאה מהמערכת.", + "alert.unfollow": "אינך עוקב יותר אחרי %1!", "alert.follow": "אתה עכשיו עוקב אחרי %1", "users": "משתמשים", "topics": "נושאים", @@ -108,13 +108,13 @@ "uploads": "העלאות", "allowed-file-types": "פורמטי הקבצים המורשים הם %1", "unsaved-changes": "יש לך שינויים שאינם נשמרו. האם הנך בטוח שברצונך להמשיך?", - "reconnecting-message": "נראה שההתחברות שלך אל %1 אבדה, אנא המתן בזמן שהמערכת מנסה לחבר אותך מחדש", + "reconnecting-message": "החיבור ל-%1 אבד, אנא המתינו בזמן שאנו מנסים לחבר אתכם מחדש", "play": "נגן", "cookies.message": "אתר זה משתמש ב cookies על מנת לשפר את חוויות המשתמש.", "cookies.accept": "קיבלתי!", "cookies.learn_more": "למד עוד", "edited": "נערך", - "disabled": "לא מאופשר", + "disabled": "מושבת", "select": "בחר", - "user-search-prompt": "נסה כאן למציאת משתמשים" + "user-search-prompt": "הקלד כאן משהו על מנת למצוא משתמשים..." } \ No newline at end of file diff --git a/public/language/he/register.json b/public/language/he/register.json index 0905771a84..463e6c6a26 100644 --- a/public/language/he/register.json +++ b/public/language/he/register.json @@ -17,12 +17,12 @@ "terms_of_use": "תנאי שימוש", "agree_to_terms_of_use": "אני מסכים לתנאי השימוש", "terms_of_use_error": "אתה מוכרח להסכים לתנאי השימוש", - "registration-added-to-queue": "הבקשה שלך להרשמה נשלחה. תקבל בקרוב מייל אישור לכתובת האימייל שהכנסת כשמנהל יאשר את הבקשה.", + "registration-added-to-queue": "בקשתך להרשמה נשלחה. במידה ובקשתך יאושר, ישלח אישור לכתובת האימייל שהכנסת.", "registration-queue-average-time": "הזמן הממוצע לאישור משתמשים הוא %1 שעות ו-%2 דקות.", "registration-queue-auto-approve-time": "חשבונך יאושר תוך %1 שעות.", "interstitial.intro": "אנו דורשים מידע נוסף לפני שנוכל ליצור עבורך את החשבון.", "interstitial.errors-found": "לא הצלחנו להשלים את הרישום שלך:", "gdpr_agree_data": "אני מסכים שפורום זה יאגור ויעבד את נתוני האישיים", "gdpr_agree_email": "אני מסכים לקבל מדי פעם מיילים מפורום זה עם סיכום נושאים מעניינים שפורסמו", - "gdpr_consent_denied": "אתה מוכרח להסכים לתנאים על מנת להרשם" + "gdpr_consent_denied": "אין אפשרות להירשם ללא אישור הסכמה על תנאים אלו." } \ No newline at end of file diff --git a/public/language/he/topic.json b/public/language/he/topic.json index ba4036c164..dd50f34cae 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -81,11 +81,11 @@ "thread_tools.purge_confirm": "האם אתה בטוח שאתה רוצה למחוק נושא זה?", "thread_tools.merge_topics": "מזג נושאים", "thread_tools.merge": "מזג", - "topic_move_success": "נושא זה יועבר תיקף ל\"%1\". לחץ כאן כדי לבטל.", - "topic_move_multiple_success": "נושאים אלו יועברו תיקף ל\"%1\" . לחץ כאן לביטול.", - "topic_move_all_success": "כל הנושאים יועברו תיקף ל\"%1\". לחץ כאן לביטול.", + "topic_move_success": "נושא זה יועבר מיד ל\"%1\". לחץ כאן כדי לבטל.", + "topic_move_multiple_success": "נושאים אלו יועברו מיד ל\"%1\" . לחץ כאן לביטול.", + "topic_move_all_success": "כל הנושאים יועברו מיד ל\"%1\". לחץ כאן לביטול.", "topic_move_undone": "העברת הנושא בוטל", - "topic_move_posts_success": "הפוסטים יועברו תיקף ל\"%1\" . לחץ כאן לביטול.", + "topic_move_posts_success": "הפוסטים יועברו מיד ל\"%1\" . לחץ כאן לביטול.", "topic_move_posts_undone": "העברת הפוסט בוטל", "post_delete_confirm": "האם אתה בטוח שאתה רוצה למחוק פוסט זה?", "post_restore_confirm": "האם אתה בטוח שאתה רוצה לשחזר פוסט זה?", diff --git a/public/language/he/user.json b/public/language/he/user.json index 7f070c79f5..165bf72e38 100644 --- a/public/language/he/user.json +++ b/public/language/he/user.json @@ -31,7 +31,7 @@ "profile": "פרופיל", "profile_views": "צפיות בפרופיל", "reputation": "מוניטין", - "bookmarks": "סימניות", + "bookmarks": "מועדפים", "watched_categories": "קטגוריות במעקב", "change_all": "שנה הכל", "watched": "נצפה", @@ -118,11 +118,11 @@ "upvote-notif-freq.everyTen": "כל 10 הצבעות חיוביות", "upvote-notif-freq.threshold": "ב-1, 5, 10, 25, 50, 100, 150, 200...", "upvote-notif-freq.logarithmic": "ב-10, 100, 1000...", - "upvote-notif-freq.disabled": "מבוטל", + "upvote-notif-freq.disabled": "מושבת", "browsing": "הגדרות ניווט", "open_links_in_new_tab": "פתח קישורים חיצוניים בכרטיסייה חדשה", "enable_topic_searching": "הפעל חיפוש בתוך נושא", - "topic_search_help": "אם מופעל, החיפוש בתוך הנושא יעקוף את שיטת החיפוש של הדפדפן, ויאפשר לך לחפש בכל הנושא - ולא רק במה שמוצג על המסך, עם זאת בלחיצה שניה על Ctrl+5 ייפתח לך החיפוש הרגיל של הדפדפן", + "topic_search_help": "החיפוש בתוך הנושא יעקוף את שיטת החיפוש של הדפדפן, ויאפשר לך לחפש בכל הנושא - ולא רק במה שמוצג על המסך, עם זאת בלחיצה נוספת על Ctrl+5 ייפתח לך החיפוש הרגיל של הדפדפן", "update_url_with_post_index": "עדכון כתובת ה-URL עם אינדקס הפוסט בעת גלישה בנושאים", "scroll_to_my_post": "הצג את הפוסט לאחר פרסום התגובה", "follow_topics_you_reply_to": "עקוב אחר נושאים שהגבת עליהם", diff --git a/public/language/hr/admin/manage/privileges.json b/public/language/hr/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/hr/admin/manage/privileges.json +++ b/public/language/hr/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/hr/error.json b/public/language/hr/error.json index aa8cc0b726..3daf75a236 100644 --- a/public/language/hr/error.json +++ b/public/language/hr/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/hu/admin/manage/privileges.json b/public/language/hu/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/hu/admin/manage/privileges.json +++ b/public/language/hu/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/hu/error.json b/public/language/hu/error.json index 76b74781eb..708f3c775d 100644 --- a/public/language/hu/error.json +++ b/public/language/hu/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/id/admin/manage/privileges.json b/public/language/id/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/id/admin/manage/privileges.json +++ b/public/language/id/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/id/error.json b/public/language/id/error.json index 1b60c295bc..0a0ac11943 100644 --- a/public/language/id/error.json +++ b/public/language/id/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/it/admin/manage/privileges.json b/public/language/it/admin/manage/privileges.json index 0b45ece233..6269fa30ae 100644 --- a/public/language/it/admin/manage/privileges.json +++ b/public/language/it/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categorie", "admin-privileges": "Privilegi", "admin-users": "Utenti", + "admin-admins-mods": "Amministratore & Moderatori", + "admin-groups": "Gruppi", + "admin-tags": "Tag", "admin-settings": "Impostazioni", "alert.confirm-moderate": "Sei sicuro di voler concedere il privilegio di moderazione a questo gruppo di utenti? Questo gruppo è pubblico e tutti gli utenti possono iscriversi a piacimento.", + "alert.confirm-admins-mods": "Sei sicuro di voler concedere i privilegi di "Amministratori & Moderatori" a questo utente/gruppo? Gli utenti con questo privilegio possono promuovere e retrocedere altri utenti in posizioni privilegiate, compreso il super amministratore", "alert.confirm-save": "Si prega di confermare l'intenzione di salvare questi privilegi", "alert.saved": "Modifiche ai privilegi salvate e applicate", "alert.confirm-discard": "Sei sicuro di voler annullare le modifiche ai privilegi?", diff --git a/public/language/it/error.json b/public/language/it/error.json index f5d2d3fc79..1218441858 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -175,5 +175,6 @@ "already-blocked": "Questo utente è già bloccato", "already-unblocked": "Questo utente è già sbloccato", "no-connection": "Sembra ci sia un problema con la tua connessione internet", + "socket-reconnect-failed": "Impossibile raggiungere il server al momento. Clicca qui per riprovare o riprova in un secondo momento", "plugin-not-whitelisted": "Impossibile installare il plug-in & solo i plugin nella whitelist del Gestione Pacchetti di NodeBB possono essere installati tramite ACP" } \ No newline at end of file diff --git a/public/language/ja/admin/manage/privileges.json b/public/language/ja/admin/manage/privileges.json index 8afef9882b..2e4296f089 100644 --- a/public/language/ja/admin/manage/privileges.json +++ b/public/language/ja/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/ja/error.json b/public/language/ja/error.json index fe2b5b17c4..c4eac6aeca 100644 --- a/public/language/ja/error.json +++ b/public/language/ja/error.json @@ -175,5 +175,6 @@ "already-blocked": "このユーザーは既にブロックされています", "already-unblocked": "このユーザーは既にブロック解除されています", "no-connection": "インターネット接続に問題があるようです", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/ko/admin/manage/privileges.json b/public/language/ko/admin/manage/privileges.json index f135f9a1b8..ab386b05d7 100644 --- a/public/language/ko/admin/manage/privileges.json +++ b/public/language/ko/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/ko/error.json b/public/language/ko/error.json index 5af2bd0f25..f879589785 100644 --- a/public/language/ko/error.json +++ b/public/language/ko/error.json @@ -175,5 +175,6 @@ "already-blocked": "이 사용자는 이미 차단 되었습니다", "already-unblocked": "이 사용자는 이미 차단 해제 되었습니다", "no-connection": "사용자님의 인터넷 연결에 문제가있는 것 같습니다", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/lt/admin/manage/privileges.json b/public/language/lt/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/lt/admin/manage/privileges.json +++ b/public/language/lt/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/lt/error.json b/public/language/lt/error.json index a4404c6164..3598ac9193 100644 --- a/public/language/lt/error.json +++ b/public/language/lt/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "Panašu, jog yra problema su jūsų interneto prieiga", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/lv/admin/manage/privileges.json b/public/language/lv/admin/manage/privileges.json index 1bd5ca057c..73d7c71ee3 100644 --- a/public/language/lv/admin/manage/privileges.json +++ b/public/language/lv/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/lv/error.json b/public/language/lv/error.json index 3c7c6b2e40..ce80b1aee3 100644 --- a/public/language/lv/error.json +++ b/public/language/lv/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "Šķiet, ka pastāv problēma ar Tavu interneta savienojumu", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/ms/admin/manage/privileges.json b/public/language/ms/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/ms/admin/manage/privileges.json +++ b/public/language/ms/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/ms/error.json b/public/language/ms/error.json index 02c455b8ca..5106040b82 100644 --- a/public/language/ms/error.json +++ b/public/language/ms/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/nb/admin/manage/privileges.json b/public/language/nb/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/nb/admin/manage/privileges.json +++ b/public/language/nb/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/nb/error.json b/public/language/nb/error.json index a43035886e..755ebfd9b2 100644 --- a/public/language/nb/error.json +++ b/public/language/nb/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/nl/admin/manage/privileges.json b/public/language/nl/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/nl/admin/manage/privileges.json +++ b/public/language/nl/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/nl/error.json b/public/language/nl/error.json index e0d5360d3a..ea797e2ea1 100644 --- a/public/language/nl/error.json +++ b/public/language/nl/error.json @@ -175,5 +175,6 @@ "already-blocked": "Deze gebruiker is al geblokkeerd", "already-unblocked": "Deze gebruiker is al gedeblokkeerd", "no-connection": "Er lijkt een probleem te zijn met je internetverbinding", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Kan plugin niet installeren – alleen plugins toegestaan door de NodeBB Package Manager kunnen via de ACP geinstalleerd worden" } \ No newline at end of file diff --git a/public/language/pl/admin/manage/privileges.json b/public/language/pl/admin/manage/privileges.json index 4e845d4e35..856b6cd66b 100644 --- a/public/language/pl/admin/manage/privileges.json +++ b/public/language/pl/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Kategorie", "admin-privileges": "Uprawnienia", "admin-users": "Użytkownicy", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Ustawienia", "alert.confirm-moderate": "Czy na pewno chcesz przyznać uprawnienia moderacji dla tej grupy użytkowników? Ta grupa jest publiczna i każdy użytkownik może do niej dołączyć.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Potwierdź zamiar zapisania uprawnień", "alert.saved": "Zapisano i zastosowano zmiany w uprawnieniach", "alert.confirm-discard": "Czy na pewno chcesz odrzucić wprowadzone zmiany w uprawnieniach?", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 6fb1b1fcbf..b0c6f69db0 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -175,5 +175,6 @@ "already-blocked": "Ten użytkownik jest już zablokowany", "already-unblocked": "Ten użytkownik jest już odblokowany", "no-connection": "Sprawdź swoje połączenie z internetem", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/pt-BR/admin/manage/privileges.json b/public/language/pt-BR/admin/manage/privileges.json index 8c669d01a5..e181f0310c 100644 --- a/public/language/pt-BR/admin/manage/privileges.json +++ b/public/language/pt-BR/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/pt-BR/error.json b/public/language/pt-BR/error.json index d0e66efc6a..4f65019307 100644 --- a/public/language/pt-BR/error.json +++ b/public/language/pt-BR/error.json @@ -175,5 +175,6 @@ "already-blocked": "Este usuário já foi bloqueado", "already-unblocked": "Este usuário já foi desbloqueado", "no-connection": "Parece haver um problema com a sua conexão com a internet", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/pt-PT/admin/manage/privileges.json b/public/language/pt-PT/admin/manage/privileges.json index ca35e46541..e6621e8a6c 100644 --- a/public/language/pt-PT/admin/manage/privileges.json +++ b/public/language/pt-PT/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categorias", "admin-privileges": "Privilégios", "admin-users": "Utilizadores", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Definições", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/pt-PT/error.json b/public/language/pt-PT/error.json index b06df94fe9..ddc52e0b16 100644 --- a/public/language/pt-PT/error.json +++ b/public/language/pt-PT/error.json @@ -175,5 +175,6 @@ "already-blocked": "Este utilizador já está bloqueado", "already-unblocked": "Este utilizador já está desbloqueado", "no-connection": "Parece haver um problema com a tua conexão à Internet", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/ro/admin/manage/privileges.json b/public/language/ro/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/ro/admin/manage/privileges.json +++ b/public/language/ro/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/ro/error.json b/public/language/ro/error.json index b9fc0c03d8..4b494f2870 100644 --- a/public/language/ro/error.json +++ b/public/language/ro/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/ru/admin/manage/privileges.json b/public/language/ru/admin/manage/privileges.json index a09c20ab31..fb73b8749a 100644 --- a/public/language/ru/admin/manage/privileges.json +++ b/public/language/ru/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Категории", "admin-privileges": "Права доступа", "admin-users": "Пользователи", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Настройки", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/ru/error.json b/public/language/ru/error.json index 807d415728..6cfcdda27e 100644 --- a/public/language/ru/error.json +++ b/public/language/ru/error.json @@ -175,5 +175,6 @@ "already-blocked": "Этот пользователь уже заблокирован", "already-unblocked": "Этот пользователь уже разблокирован", "no-connection": "Похоже, есть проблема с вашим подключением к Интернету", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/rw/admin/manage/privileges.json b/public/language/rw/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/rw/admin/manage/privileges.json +++ b/public/language/rw/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/rw/error.json b/public/language/rw/error.json index 29f41719d2..56efaaab99 100644 --- a/public/language/rw/error.json +++ b/public/language/rw/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/sc/admin/manage/privileges.json b/public/language/sc/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/sc/admin/manage/privileges.json +++ b/public/language/sc/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/sc/error.json b/public/language/sc/error.json index c541eff485..ebf6de4ec7 100644 --- a/public/language/sc/error.json +++ b/public/language/sc/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/sk/admin/manage/privileges.json b/public/language/sk/admin/manage/privileges.json index b787c7dd0f..c8a7ea4171 100644 --- a/public/language/sk/admin/manage/privileges.json +++ b/public/language/sk/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/sk/error.json b/public/language/sk/error.json index 68a6990a26..b4dc7108c1 100644 --- a/public/language/sk/error.json +++ b/public/language/sk/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "Zdá sa, že máte problém s pripojením k internetu", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/sl/admin/manage/privileges.json b/public/language/sl/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/sl/admin/manage/privileges.json +++ b/public/language/sl/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/sl/error.json b/public/language/sl/error.json index e244684a98..a186c812ba 100644 --- a/public/language/sl/error.json +++ b/public/language/sl/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/sr/admin/manage/privileges.json b/public/language/sr/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/sr/admin/manage/privileges.json +++ b/public/language/sr/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/sr/error.json b/public/language/sr/error.json index 2f06b7476c..d2500b60d0 100644 --- a/public/language/sr/error.json +++ b/public/language/sr/error.json @@ -175,5 +175,6 @@ "already-blocked": "Овај корисник је већ блокиран", "already-unblocked": "Овај корисник је већ одблокиран", "no-connection": "Изгледа да постоји проблем са вашом интернет везом", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/sv/admin/manage/privileges.json b/public/language/sv/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/sv/admin/manage/privileges.json +++ b/public/language/sv/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/sv/error.json b/public/language/sv/error.json index 05a40764ab..2a245b4fdf 100644 --- a/public/language/sv/error.json +++ b/public/language/sv/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "Det verkar vara något problem med din internetanslutning", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/th/admin/manage/privileges.json b/public/language/th/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/th/admin/manage/privileges.json +++ b/public/language/th/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/th/error.json b/public/language/th/error.json index 8cb12ec719..f21336e4ed 100644 --- a/public/language/th/error.json +++ b/public/language/th/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "There seems to be a problem with your internet connection", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/tr/admin/manage/privileges.json b/public/language/tr/admin/manage/privileges.json index fd11b7201d..d6e2024a65 100644 --- a/public/language/tr/admin/manage/privileges.json +++ b/public/language/tr/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Kategoriler", "admin-privileges": "Ayrıcalıklar", "admin-users": "Kullanıcılar", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Ayarlar", "alert.confirm-moderate": "Bu gruba yönetim ayrıcalıkları vermek istediğinize emin misiniz? Bu grup genele açık olduğundan her kullanıcı gruba katılabilir. ", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Lütfen ayrıcalıkları kaydetme isteğinizi onaylayınız", "alert.saved": "Ayrıcalık değişiklikleri kaydedildi ve uygulandı", "alert.confirm-discard": "Ayrıcalık değişikliklerini iptal etmek istediğinize emin misiniz?", diff --git a/public/language/tr/error.json b/public/language/tr/error.json index 08168b4644..bff70f0089 100644 --- a/public/language/tr/error.json +++ b/public/language/tr/error.json @@ -175,5 +175,6 @@ "already-blocked": "Bu kullanıcı zaten engellendi", "already-unblocked": "Bu kullanıcı zaten engellenmedi", "no-connection": "İnternet bağlantınızda sorun var gibi görünüyor", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "– eklentisi yüklenemedi, sadece NodeBB Paket Yöneticisi tarafından onaylanan eklentiler kontrol panelinden kurulabilir" } \ No newline at end of file diff --git a/public/language/uk/admin/manage/privileges.json b/public/language/uk/admin/manage/privileges.json index d1ddb942f3..fc3c530b4b 100644 --- a/public/language/uk/admin/manage/privileges.json +++ b/public/language/uk/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 9914ff6e53..8229219d14 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -175,5 +175,6 @@ "already-blocked": "Цей користувач вже заблокований", "already-unblocked": "Цей користувач вже розблокований", "no-connection": "Схоже, виникла проблема з вашим Інтернет-з'єднанням", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/vi/admin/manage/privileges.json b/public/language/vi/admin/manage/privileges.json index d90fb7893d..a6b39716fd 100644 --- a/public/language/vi/admin/manage/privileges.json +++ b/public/language/vi/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "Categories", "admin-privileges": "Privileges", "admin-users": "Users", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "Settings", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/vi/error.json b/public/language/vi/error.json index a3bdacd594..e0f6e0c346 100644 --- a/public/language/vi/error.json +++ b/public/language/vi/error.json @@ -175,5 +175,6 @@ "already-blocked": "This user is already blocked", "already-unblocked": "This user is already unblocked", "no-connection": "Kết nối internet của bạn có vấn đề.", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/language/zh-CN/admin/manage/privileges.json b/public/language/zh-CN/admin/manage/privileges.json index 3d8b8bbd72..b4124dee8e 100644 --- a/public/language/zh-CN/admin/manage/privileges.json +++ b/public/language/zh-CN/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "版块", "admin-privileges": "权限", "admin-users": "用户", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "设置", "alert.confirm-moderate": "您确定要将审核权限授予此用户组吗?此用户组是公开的,任何用户都可以随意加入。", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "请验证您保存这些权限的目的", "alert.saved": "权限修改已保存并应用", "alert.confirm-discard": "您确定要取消权限修改吗?", diff --git a/public/language/zh-CN/error.json b/public/language/zh-CN/error.json index 1265b06fe5..9d586203bf 100644 --- a/public/language/zh-CN/error.json +++ b/public/language/zh-CN/error.json @@ -175,5 +175,6 @@ "already-blocked": "此用户已被屏蔽", "already-unblocked": "此用户已被取消屏蔽", "no-connection": "您的网络连接似乎存在问题", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "无法安装插件 – 只有被NodeBB包管理器列入白名单的插件才能通过ACP安装。" } \ No newline at end of file diff --git a/public/language/zh-TW/admin/manage/privileges.json b/public/language/zh-TW/admin/manage/privileges.json index 88d37d2f19..1fd71dbb3b 100644 --- a/public/language/zh-TW/admin/manage/privileges.json +++ b/public/language/zh-TW/admin/manage/privileges.json @@ -39,9 +39,13 @@ "admin-categories": "版面", "admin-privileges": "權限", "admin-users": "使用者", + "admin-admins-mods": "Admins & Mods", + "admin-groups": "Groups", + "admin-tags": "Tags", "admin-settings": "設定", "alert.confirm-moderate": "Are you sure you wish to grant the moderation privilege to this user group? This group is public, and any users can join at will.", + "alert.confirm-admins-mods": "Are you sure you wish to grant the "Admins & Mods" privilege to this user/group? Users with this privilege are able to promote and demote other users into privileged positions, including super administrator", "alert.confirm-save": "Please confirm your intention to save these privileges", "alert.saved": "Privilege changes saved and applied", "alert.confirm-discard": "Are you sure you wish to discard your privilege changes?", diff --git a/public/language/zh-TW/error.json b/public/language/zh-TW/error.json index 9dae827138..509624f544 100644 --- a/public/language/zh-TW/error.json +++ b/public/language/zh-TW/error.json @@ -175,5 +175,6 @@ "already-blocked": "此使用者已被封鎖", "already-unblocked": "此使用者已被取消封鎖", "no-connection": "您的網路連線似乎有問題", + "socket-reconnect-failed": "Unable to reach the server at this time. Click here to try again, or try again later", "plugin-not-whitelisted": "Unable to install plugin – only plugins whitelisted by the NodeBB Package Manager can be installed via the ACP" } \ No newline at end of file diff --git a/public/less/generics.less b/public/less/generics.less index 254c8fe505..4e2102bd37 100644 --- a/public/less/generics.less +++ b/public/less/generics.less @@ -155,7 +155,7 @@ } } -.topic .necro-post { +.necro-post { color: rgba(127,127,127,.5); font-size: 1.5em; margin-bottom: 20px; diff --git a/public/openapi/components/schemas/CategoryObj.yaml b/public/openapi/components/schemas/CategoryObj.yaml deleted file mode 100644 index 7c3ac6a83f..0000000000 --- a/public/openapi/components/schemas/CategoryObj.yaml +++ /dev/null @@ -1,72 +0,0 @@ -CategoryObj: - properties: - cid: - type: number - example: 1 - name: - type: string - example: My New Category - description: - type: string - example: Lorem ipsum, dolor sit amet - descriptionParsed: - type: string - example: Lorem ipsum, dolor sit amet - icon: - type: string - example: bullhorn - bgColor: - type: string - example: '#ffffff' - color: - type: string - example: '#000000' - slug: - type: string - example: 1/my-new-category - parentCid: - type: number - example: 0 - topic_count: - type: number - example: 0 - post_count: - type: number - example: 0 - disabled: - type: number - example: 0 - order: - type: number - example: 5 - link: - type: number - example: 'https://example.org' - numRecentReplies: - type: number - example: 1 - class: - type: string - example: col-md-3 col-xs-6 - imageClass: - type: string - example: cover - isSection: - type: number - example: 0 - totalPostCount: - type: number - example: 0 - totalTopicCount: - type: number - example: 0 - tagWhitelist: - type: array - example: - - some-tag - - another-tag - unread-class: - type: string - backgroundImage: - type: string - example: '/assets/images/covers/Circuit1.png' \ No newline at end of file diff --git a/public/openapi/components/schemas/CategoryObject.yaml b/public/openapi/components/schemas/CategoryObject.yaml index c19a5f192c..384349bc9e 100644 --- a/public/openapi/components/schemas/CategoryObject.yaml +++ b/public/openapi/components/schemas/CategoryObject.yaml @@ -1,73 +1,85 @@ CategoryObject: - type: object - properties: - cid: - type: number - description: A category identifier assigned upon category creation (this value cannot be changed) - name: - type: string - description: The category's name/title - description: - type: string - description: A variable-length description of the category (usually displayed underneath the category name) - descriptionParsed: - type: string - description: A variable-length description of the category (usually displayed underneath the category name). Unlike `description`, this value here will have been run through any parsers installed on the forum (e.g. Markdown) - icon: - type: string - description: A FontAwesome icon string - example: fa-comments-o - bgColor: - type: string - description: Theme-related, a six-character hexadecimal string representing the background colour of the category - color: - type: string - description: Theme-related, a six-character hexadecimal string representing the foreground/text colour of the category - slug: - type: string - description: An URL-safe variant of the category title. This value is automatically generated. - readOnly: true - parentCid: - type: number - description: The category identifier for the category that is the immediate ancestor of the current category - topic_count: - type: number - description: The number of topics in the category - post_count: - type: number - description: The number of posts in the category - disabled: - type: number - description: Whether or not this category is disabled. - order: - type: number - description: A number representing the category's place in the hierarchy - link: - type: string - description: If set, attempting to access the forum will go to this external link instead (theme-specific) - numRecentReplies: - type: number - description: The number of posts to render in the API response (this is mostly used at the theme level) - class: - type: string - description: Values that are appended to the `class` attribute of the category's parent/root element - imageClass: - type: string - enum: [auto, cover, contain] - description: The `background-position` of the category background image, if one is set - isSection: - type: number - postQueue: - type: number - totalPostCount: - type: number - description: The number of posts in the category - totalTopicCount: - type: number - description: The number of topics in the category - minTags: - type: number - description: Minimum tags per topic in this category - maxTags: - type: number - description: Maximum tags per topic in this category \ No newline at end of file + allOf: + - type: object + properties: + cid: + type: number + description: A category identifier assigned upon category creation (this value cannot be changed) + name: + type: string + description: The category's name/title + description: + type: string + description: A variable-length description of the category (usually displayed underneath the category name) + descriptionParsed: + type: string + description: A variable-length description of the category (usually displayed underneath the category name). Unlike `description`, this value here will have been run through any parsers installed on the forum (e.g. Markdown) + icon: + type: string + description: A FontAwesome icon string + example: fa-comments-o + bgColor: + type: string + description: Theme-related, a six-character hexadecimal string representing the background colour of the category + color: + type: string + description: Theme-related, a six-character hexadecimal string representing the foreground/text colour of the category + slug: + type: string + description: An URL-safe variant of the category title. This value is automatically generated. + readOnly: true + parentCid: + type: number + description: The category identifier for the category that is the immediate ancestor of the current category + topic_count: + type: number + description: The number of topics in the category + post_count: + type: number + description: The number of posts in the category + disabled: + type: number + description: Whether or not this category is disabled. + order: + type: number + description: A number representing the category's place in the hierarchy + link: + type: string + description: If set, attempting to access the forum will go to this external link instead (theme-specific) + numRecentReplies: + type: number + description: The number of posts to render in the API response (this is mostly used at the theme level) + class: + type: string + description: Values that are appended to the `class` attribute of the category's parent/root element + imageClass: + type: string + enum: [auto, cover, contain] + description: The `background-position` of the category background image, if one is set + isSection: + type: number + minTags: + type: number + description: Minimum tags per topic in this category + maxTags: + type: number + description: Maximum tags per topic in this category + postQueue: + type: number + totalPostCount: + type: number + description: The number of posts in the category + totalTopicCount: + type: number + description: The number of topics in the category + - type: object + description: Optional properties that may or may not be present (except for `cid`, which is always present, and is only here as a hack to pass validation) + properties: + cid: + type: number + description: A category identifier + backgroundImage: + type: string + description: Relative URL to the category's background image + required: + - cid \ No newline at end of file diff --git a/public/openapi/components/schemas/TopicObject.yaml b/public/openapi/components/schemas/TopicObject.yaml index 45114a696b..9c95a4a5ed 100644 --- a/public/openapi/components/schemas/TopicObject.yaml +++ b/public/openapi/components/schemas/TopicObject.yaml @@ -1,62 +1,10 @@ TopicObject: allOf: + - $ref: '#/TopicObjectSlim' - type: object properties: - tid: - type: number - description: A topic identifier - uid: - type: number - description: A user identifier - cid: - type: number - description: A category identifier - mainPid: - type: number - description: The post id of the first post in this topic (also called the "original post") - title: - type: string - slug: - type: string - timestamp: - type: number lastposttime: type: number - postcount: - type: number - viewcount: - type: number - postercount: - type: number - teaserPid: - oneOf: - - type: number - - type: string - nullable: true - upvotes: - type: number - downvotes: - type: number - deleted: - type: number - locked: - type: number - pinned: - type: number - description: Whether or not this particular topic is pinned to the top of the - category - deleterUid: - type: number - titleRaw: - type: string - timestampISO: - type: string - description: An ISO 8601 formatted date string (complementing `timestamp`) - lastposttimeISO: - type: string - description: An ISO 8601 formatted date string (complementing `lastposttime`) - votes: - type: number thumbs: type: array items: @@ -259,6 +207,10 @@ TopicObjectSlim: cid: type: number description: A category identifier + title: + type: string + slug: + type: string mainPid: type: number description: The post id of the first post in this topic (also called the "original post") @@ -272,6 +224,8 @@ TopicObjectSlim: type: number deleterUid: type: number + titleRaw: + type: string locked: type: number pinned: @@ -299,4 +253,9 @@ TopicObjectSlim: downvotes: type: number votes: - type: number \ No newline at end of file + type: number + teaserPid: + oneOf: + - type: number + - type: string + nullable: true \ No newline at end of file diff --git a/public/openapi/read.yaml b/public/openapi/read.yaml index 94481e01ba..f33624b521 100644 --- a/public/openapi/read.yaml +++ b/public/openapi/read.yaml @@ -54,11 +54,17 @@ tags: description: Disparate method of categorizing topics - name: shorthand description: Convenience and utility routes for accessing other part of the API + - name: other + description: Other one-off routes that do not fit in a section of their own paths: /api/: $ref: 'read/index.yaml' + /api/admin: + $ref: 'read/admin.yaml' /api/admin/dashboard: $ref: 'read/admin/dashboard.yaml' + "/api/admin/settings/{term}": + $ref: 'read/admin/settings/term.yaml' /api/admin/settings/languages: $ref: 'read/admin/settings/languages.yaml' /api/admin/settings/navigation: @@ -67,6 +73,12 @@ paths: $ref: 'read/admin/settings/homepage.yaml' /api/admin/settings/social: $ref: 'read/admin/settings/social.yaml' + /api/admin/settings/email: + $ref: 'read/admin/settings/email.yaml' + /api/admin/settings/user: + $ref: 'read/admin/settings/user.yaml' + /api/admin/settings/post: + $ref: 'read/admin/settings/post.yaml' /api/admin/manage/categories: $ref: 'read/admin/manage/categories.yaml' "/api/admin/manage/categories/{category_id}": @@ -91,8 +103,6 @@ paths: $ref: 'read/admin/manage/uploads.yaml' /api/admin/manage/digest: $ref: 'read/admin/manage/digest.yaml' - "/api/admin/settings/{term}": - $ref: 'read/admin/settings/term.yaml' "/api/admin/appearance/{term}": $ref: 'read/admin/appearance/term.yaml' /api/admin/extend/plugins: @@ -115,6 +125,8 @@ paths: $ref: 'read/admin/advanced/errors/export.yaml' /api/admin/advanced/cache: $ref: 'read/admin/advanced/cache.yaml' + /api/admin/advanced/cache/dump: + $ref: 'read/admin/advanced/cache/dump.yaml' /api/admin/development/logger: $ref: 'read/admin/development/logger.yaml' /api/admin/development/info: @@ -157,16 +169,18 @@ paths: $ref: 'read/user/uid/userslug/export/uploads.yaml' "/api/user/uid/{userslug}/export/profile": $ref: 'read/user/uid/userslug/export/profile.yaml' - "/api/post/pid/{id}": + "/api/{type}/pid/{id}": $ref: 'read/post/pid/id.yaml' - "/api/topic/tid/{id}": + "/api/{type}/tid/{id}": $ref: 'read/topic/tid/id.yaml' - "/api/category/cid/{id}": + "/api/{type}/cid/{id}": $ref: 'read/category/cid/id.yaml' /api/categories: $ref: 'read/categories.yaml' "/api/categories/{cid}/moderators": $ref: 'read/categories/cid/moderators.yaml' + "/api/topic/{topic_id}/{slug}": + $ref: 'read/topic/topic_id.yaml' "/api/topic/{topic_id}/{slug}/{post_index}": $ref: 'read/topic/topic_id.yaml' /api/recent: @@ -189,6 +203,12 @@ paths: $ref: 'read/login.yaml' /api/register: $ref: 'read/register.yaml' + /api/register/complete: + $ref: 'read/register/complete.yaml' + "/api/confirm/{code}": + $ref: 'read/confirm/code.yaml' + /api/tos: + $ref: 'read/tos.yaml' /api/search: $ref: 'read/search.yaml' "/api/reset": @@ -217,6 +237,8 @@ paths: $ref: 'read/popular.yaml' /api/top: $ref: 'read/top.yaml' + "/api/category/{category_id}/{slug}": + $ref: 'read/category/category_id.yaml' "/api/category/{category_id}/{slug}/{topic_index}": $ref: 'read/category/category_id.yaml' /api/self: @@ -225,7 +247,7 @@ paths: $ref: 'read/me.yaml' /api/me/*: $ref: 'read/me.yaml' - "/api/uid/{uid}/*": + "/api/uid/{uid*}": $ref: 'read/uid/uid.yaml' "/api/user/{userslug}": $ref: 'read/user/userslug.yaml' @@ -286,4 +308,6 @@ paths: "/api/groups/{slug}": $ref: 'read/groups/slug.yaml' "/api/groups/{slug}/members": - $ref: 'read/groups/slug/members.yaml' \ No newline at end of file + $ref: 'read/groups/slug/members.yaml' + /api/outgoing: + $ref: 'read/outgoing.yaml' \ No newline at end of file diff --git a/public/openapi/read/admin.yaml b/public/openapi/read/admin.yaml new file mode 100644 index 0000000000..27ec1b9d93 --- /dev/null +++ b/public/openapi/read/admin.yaml @@ -0,0 +1,19 @@ +get: + tags: + - admin + summary: Get administrative index + description: | + Internally, NodeBB will redirect you to a different page based on your privilege levels. + + The default is "dashboard" for superadmins and those with the "dashboard" privilege. If the requesting user is neither, then they will be redirected to a page that they have privileges to view (e.g. `/categories`, `/privileges`, `/users`, or `/settings/general`). + + Failing that, the request will be denied. + responses: + "200": + description: | + A JSON object containing data for the default admin index. + content: + application/json: + schema: + properties: {} + additionalProperties: {} \ No newline at end of file diff --git a/public/openapi/read/admin/advanced/cache.yaml b/public/openapi/read/admin/advanced/cache.yaml index 06d7890971..c6daf4fbea 100644 --- a/public/openapi/read/admin/advanced/cache.yaml +++ b/public/openapi/read/admin/advanced/cache.yaml @@ -2,6 +2,15 @@ get: tags: - admin summary: Get system cache info + parameters: + - in: query + name: name + schema: + type: string + enum: ['post', 'object', 'group', 'local'] + required: false + description: Specify cache to dump if calling `/dump` + example: 'post' responses: "200": description: "" diff --git a/public/openapi/read/admin/advanced/cache/dump.yaml b/public/openapi/read/admin/advanced/cache/dump.yaml new file mode 100644 index 0000000000..2be4e38f9b --- /dev/null +++ b/public/openapi/read/admin/advanced/cache/dump.yaml @@ -0,0 +1,23 @@ +get: + tags: + - admin + summary: Get system cache info + parameters: + - in: query + name: name + schema: + type: string + enum: ['post', 'object', 'group', 'local'] + required: false + description: Specify cache to dump if calling `/dump` + example: 'post' + responses: + "200": + description: "" + content: + application/json: + schema: + type: object + properties: {} + additionalProperties: + description: The type of response is dependent on the database used. Please examine the output. \ No newline at end of file diff --git a/public/openapi/read/admin/groups/groupname/csv.yaml b/public/openapi/read/admin/groups/groupname/csv.yaml index 43a5bf8e59..e774a1b4a1 100644 --- a/public/openapi/read/admin/groups/groupname/csv.yaml +++ b/public/openapi/read/admin/groups/groupname/csv.yaml @@ -9,6 +9,12 @@ get: type: string required: true example: /admin/manage/groups + - in: path + name: groupname + schema: + type: string + required: true + example: registered-users responses: "200": description: "A CSV file containing all users in the group" diff --git a/public/openapi/read/admin/settings/email.yaml b/public/openapi/read/admin/settings/email.yaml new file mode 100644 index 0000000000..e2d9b76257 --- /dev/null +++ b/public/openapi/read/admin/settings/email.yaml @@ -0,0 +1,43 @@ +get: + tags: + - admin + summary: Get emailer settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + emails: + type: array + items: + type: object + properties: + path: + type: string + description: The name of the email template + fullpath: + type: string + description: Full system path to the email template + text: + type: string + description: Customized email template text, if applicable, otherwise identical to `original` + original: + type: string + description: The email template text as provided by NodeBB core + isCustom: + type: boolean + sendable: + type: array + items: + type: string + description: The name of the email template + services: + type: array + items: + type: string + description: A list of email services which can be used to send emails on behalf of NodeBB + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/post.yaml b/public/openapi/read/admin/settings/post.yaml new file mode 100644 index 0000000000..f8273c8b8c --- /dev/null +++ b/public/openapi/read/admin/settings/post.yaml @@ -0,0 +1,18 @@ +get: + tags: + - admin + summary: Get post settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + groupsExemptFromPostQueue: + type: array + items: + $ref: ../../../components/schemas/GroupObject.yaml#/GroupDataObject + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/admin/settings/user.yaml b/public/openapi/read/admin/settings/user.yaml new file mode 100644 index 0000000000..6dccccedee --- /dev/null +++ b/public/openapi/read/admin/settings/user.yaml @@ -0,0 +1,25 @@ +get: + tags: + - admin + summary: Get user settings + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + notificationSettings: + type: array + items: + type: object + properties: + name: + type: string + description: The notification type + label: + type: string + description: The language key for the notification type (for localisation client-side) + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/categories.yaml b/public/openapi/read/categories.yaml index 723501a410..36f7b3b797 100644 --- a/public/openapi/read/categories.yaml +++ b/public/openapi/read/categories.yaml @@ -13,7 +13,7 @@ get: Subcategories are also returned, nested under a category's `children` property. responses: "200": - description: A list of category objectscurrently available to the accessing user + description: A list of category objects currently available to the accessing user content: application/json: schema: diff --git a/public/openapi/read/category/cid/id.yaml b/public/openapi/read/category/cid/id.yaml index e70f774b07..298147dea3 100644 --- a/public/openapi/read/category/cid/id.yaml +++ b/public/openapi/read/category/cid/id.yaml @@ -3,6 +3,12 @@ get: - shorthand summary: Get category data parameters: + - name: type + in: path + required: true + schema: + type: string + example: category - name: id in: path required: true diff --git a/public/openapi/read/confirm/code.yaml b/public/openapi/read/confirm/code.yaml new file mode 100644 index 0000000000..c3e75649b8 --- /dev/null +++ b/public/openapi/read/confirm/code.yaml @@ -0,0 +1,21 @@ +get: + tags: + - authentication + summary: Verify an email address + responses: + "200": + description: Email address verified, or confirmation code was incorrect + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + error: + type: string + description: Translation key for client-side localisation + required: + - title + - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/email/unsubscribe/token.yaml b/public/openapi/read/email/unsubscribe/token.yaml index 71c02c8809..3b1c9deaf8 100644 --- a/public/openapi/read/email/unsubscribe/token.yaml +++ b/public/openapi/read/email/unsubscribe/token.yaml @@ -1,8 +1,51 @@ -# TODO: Need GET route here as well +get: + tags: + - emails + summary: Unsubscribe user from email type (user variant) + parameters: + - name: token + in: path + required: true + schema: + type: string + example: testToken + responses: + "200": + description: "" + content: + application/json: + schema: + allOf: + - type: object + properties: + payload: + type: object + properties: + uid: + type: number + template: + type: string + description: The type of email template to unsubscribe from. + enum: + - digest + - notification + type: + type: string + description: Only used if `template` is `notification`, signifies the type of notification to unsubscribe from. + nullable: true + iat: + type: number + description: Reflection of the token's "issued at" claim + required: + - uid + - template + - $ref: ../../../components/schemas/CommonProps.yaml#/CommonProps + "500": + description: "Server-side error (likely token verification failure)" post: tags: - emails - summary: Unsubscribe user from email type + summary: Unsubscribe user from email type (auto variant) parameters: - name: token in: path diff --git a/public/openapi/read/login.yaml b/public/openapi/read/login.yaml index a480ef74b9..4e5b3944a9 100644 --- a/public/openapi/read/login.yaml +++ b/public/openapi/read/login.yaml @@ -1,7 +1,7 @@ get: tags: - authentication - summary: /api/login + summary: Log in a user responses: "200": description: "" diff --git a/public/openapi/read/outgoing.yaml b/public/openapi/read/outgoing.yaml new file mode 100644 index 0000000000..ffde8242bf --- /dev/null +++ b/public/openapi/read/outgoing.yaml @@ -0,0 +1,29 @@ +get: + tags: + - other + summary: Warn before navigating externally + parameters: + - in: query + name: url + schema: + type: string + description: URL of the page to warn the user about + example: https://example.org + description: This route presents a warning to a user notifying them that the page they are about to view is hosted externally. They then have the option of continuing onwards or going back to where they came from. + responses: + "200": + description: Warning page presented + content: + application/json: + schema: + allOf: + - type: object + properties: + outgoing: + type: string + description: Escaped URL of the page to navigate to + title: + description: The page title + type: string + - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs + - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/post/pid/id.yaml b/public/openapi/read/post/pid/id.yaml index 527ee4ec29..58dd822f30 100644 --- a/public/openapi/read/post/pid/id.yaml +++ b/public/openapi/read/post/pid/id.yaml @@ -3,6 +3,12 @@ get: - shorthand summary: Get post data parameters: + - name: type + in: path + required: true + schema: + type: string + example: post - name: id in: path required: true diff --git a/public/openapi/read/register.yaml b/public/openapi/read/register.yaml index 199b496e03..b4ec6d4fbc 100644 --- a/public/openapi/read/register.yaml +++ b/public/openapi/read/register.yaml @@ -1,7 +1,7 @@ get: tags: - authentication - summary: /api/register + summary: Register a new user responses: "200": description: "" @@ -54,28 +54,4 @@ get: title: type: string - $ref: ../components/schemas/Breadcrumbs.yaml#/Breadcrumbs - - $ref: ../components/schemas/CommonProps.yaml#/CommonProps - # /api/register/complete: - # get: - # tags: - # - authentication - # summary: /api/register/complete - # responses: - # "200": - # description: "" - # content: - # application/json: - # schema: - # allOf: - # - type: object - # properties: - # title: - # type: string - # errors: - # type: array - # items: {} - # sections: - # type: array - # items: - # type: string - # - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file + - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/register/complete.yaml b/public/openapi/read/register/complete.yaml new file mode 100644 index 0000000000..d6f8e48a35 --- /dev/null +++ b/public/openapi/read/register/complete.yaml @@ -0,0 +1,30 @@ +get: + tags: + - authentication + summary: Complete a user's registration + responses: + "302": + description: If there are no additional registration steps to complete, then the user is redirected back to the registration page (`/register`) + headers: + Location: + schema: + type: string + example: /register + "200": + description: '' + content: + application/json: + schema: + allOf: + - type: object + properties: + title: + type: string + errors: + type: array + items: {} + sections: + type: array + items: + type: string + - $ref: ../../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/topic/tid/id.yaml b/public/openapi/read/topic/tid/id.yaml index 5305a99c7f..dd77ad2670 100644 --- a/public/openapi/read/topic/tid/id.yaml +++ b/public/openapi/read/topic/tid/id.yaml @@ -3,6 +3,12 @@ get: - shorthand summary: Get topic data parameters: + - name: type + in: path + required: true + schema: + type: string + example: topic - name: id in: path required: true diff --git a/public/openapi/read/tos.yaml b/public/openapi/read/tos.yaml new file mode 100644 index 0000000000..1c66f4bdc9 --- /dev/null +++ b/public/openapi/read/tos.yaml @@ -0,0 +1,18 @@ +get: + tags: + - authentication + summary: Get forum terms of service + description: This route allows you to view the forum terms of service. + responses: + "200": + description: Terms of service retrieved. + content: + application/json: + schema: + allOf: + - type: object + properties: + termsOfUse: + type: string + description: Full text of the configured terms of service/terms of use. + - $ref: ../components/schemas/CommonProps.yaml#/CommonProps \ No newline at end of file diff --git a/public/openapi/read/uid/uid.yaml b/public/openapi/read/uid/uid.yaml index 6552e78b9e..dccb23e01a 100644 --- a/public/openapi/read/uid/uid.yaml +++ b/public/openapi/read/uid/uid.yaml @@ -8,7 +8,7 @@ get: For example, to go to `uid` 15's list of topics made, you can navigate to `/api/uid/15/topics`, which will send you to the appropriate canonical URL for that user's topics. parameters: - - name: uid + - name: uid* in: path required: true schema: diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 73555fe7ac..2b6266d64c 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -23,11 +23,25 @@ info: servers: - url: /api/v3 tags: + - name: utilities + description: Utility calls to test Write API functionality - name: users - description: 'Account related calls (create, modify, delete, etc.)' + description: Account related calls (create, modify, delete, etc.) + - name: groups + description: Calls related to user groups - name: categories description: Administrative calls to manage categories + - name: topics + description: Topic-based calls (create, modify, delete, etc.) + - name: posts + description: Individual post-related calls (create, modify, delete, etc.) + - name: admin + description: Administrative calls + - name: files + description: File upload routes paths: + /ping: + $ref: 'write/ping.yaml' /users/: $ref: 'write/users.yaml' /users/{uid}: @@ -44,6 +58,8 @@ paths: $ref: 'write/users/uid/follow.yaml' /users/{uid}/ban: $ref: 'write/users/uid/ban.yaml' + /users/{uid}/tokens: + $ref: 'write/users/uid/tokens.yaml' /users/{uid}/tokens/{token}: $ref: 'write/users/uid/tokens/token.yaml' /users/{uid}/sessions/{uuid}: @@ -52,15 +68,19 @@ paths: $ref: 'write/users/uid/invites.yaml' /users/{uid}/invites/groups: $ref: 'write/users/uid/invites/groups.yaml' - /categories/: - $ref: 'write/categories.yaml' /groups/: $ref: 'write/groups.yaml' /groups/{slug}: $ref: 'write/groups/slug.yaml' /groups/{slug}/membership/{uid}: $ref: 'write/groups/slug/membership/uid.yaml' - /topics: + /groups/{slug}/ownership/{uid}: + $ref: 'write/groups/slug/ownership/uid.yaml' + /categories/: + $ref: 'write/categories.yaml' + /categories/{cid}: + $ref: 'write/categories/cid.yaml' + /topics/: $ref: 'write/topics.yaml' /topics/{tid}: $ref: 'write/topics/tid.yaml' @@ -88,5 +108,5 @@ paths: $ref: 'write/posts/pid/bookmark.yaml' /admin/settings/{setting}: $ref: 'write/admin/settings/setting.yaml' - /files: + /files/: $ref: 'write/files.yaml' \ No newline at end of file diff --git a/public/openapi/write/categories.yaml b/public/openapi/write/categories.yaml index dc90d8f41f..5c26b53633 100644 --- a/public/openapi/write/categories.yaml +++ b/public/openapi/write/categories.yaml @@ -54,4 +54,13 @@ post: status: $ref: ../components/schemas/Status.yaml#/Status response: - $ref: ../components/schemas/CategoryObj.yaml#/CategoryObj \ No newline at end of file + allOf: + - $ref: ../components/schemas/CategoryObject.yaml#/CategoryObject + - type: object + properties: + tagWhitelist: + type: array + items: + type: string + unread-class: + type: string \ No newline at end of file diff --git a/public/openapi/write/categories/cid.yaml b/public/openapi/write/categories/cid.yaml new file mode 100644 index 0000000000..bcb50225a5 --- /dev/null +++ b/public/openapi/write/categories/cid.yaml @@ -0,0 +1,93 @@ +get: + tags: + - categories + summary: get a category + description: This operation retrieves a category's data + parameters: + - in: path + name: cid + schema: + type: string + required: true + description: a valid category id + example: 2 + responses: + '200': + description: Category successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject +put: + tags: + - categories + summary: update a category + description: This operation updates an existing category. + parameters: + - in: path + name: cid + schema: + type: number + required: true + description: a valid category id + example: 2 + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: {} + additionalProperties: {} + responses: + '200': + description: category successfully updated + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + allOf: + - $ref: ../../components/schemas/CategoryObject.yaml#/CategoryObject + - type: object + properties: + tagWhitelist: + type: array + items: + type: string + unread-class: + type: string +delete: + tags: + - categories + summary: delete a category + description: This operation deletes and purges a category and all of its topics and posts (careful, there is no confirmation!) + parameters: + - in: path + name: cid + schema: + type: number + required: true + description: a valid category id + example: 2 + responses: + '200': + description: Category successfully deleted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} \ No newline at end of file diff --git a/public/openapi/write/groups/slug.yaml b/public/openapi/write/groups/slug.yaml index 28077e290c..4959da9f12 100644 --- a/public/openapi/write/groups/slug.yaml +++ b/public/openapi/write/groups/slug.yaml @@ -15,6 +15,43 @@ head: description: group found '404': description: group not found +put: + tags: + - groups + summary: update group data + parameters: + - in: path + name: slug + schema: + type: string + required: true + description: slug of the group you wish to update + example: my-test-group + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + icon: + type: string + example: fa-times + additionalProperties: + description: An object of group properties you wish to update + example: + responses: + '200': + description: group successfully updated + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + $ref: ../../components/schemas/GroupObject.yaml#/GroupDataObject delete: tags: - groups diff --git a/public/openapi/write/groups/slug/ownership/uid.yaml b/public/openapi/write/groups/slug/ownership/uid.yaml new file mode 100644 index 0000000000..ba8e16558e --- /dev/null +++ b/public/openapi/write/groups/slug/ownership/uid.yaml @@ -0,0 +1,66 @@ +put: + tags: + - groups + summary: grant group ownership + description: This operation grants ownership privilege to a user. + parameters: + - in: path + name: slug + schema: + type: string + required: true + description: slug of the group you would like to grant ownership + example: test-group + - in: path + name: uid + schema: + type: number + required: true + description: uid of the user to grant ownership + example: 1 + responses: + '200': + description: ownership successfully granted + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} +delete: + tags: + - groups + summary: rescind group ownership + description: 'This operation rescinds ownership privilege from a user. **Note**: Every group needs at least one owner, so if you are attempting to remove the last owner of a group, this call will fail.' + parameters: + - in: path + name: slug + schema: + type: string + required: true + description: slug of the group you would like to rescind ownership + example: test-group + - in: path + name: uid + schema: + type: number + required: true + description: uid of the user to rescind ownership from + example: 2 + responses: + '200': + description: ownership successfully rescinded + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: {} \ No newline at end of file diff --git a/public/openapi/write/ping.yaml b/public/openapi/write/ping.yaml new file mode 100644 index 0000000000..67d3dd62ba --- /dev/null +++ b/public/openapi/write/ping.yaml @@ -0,0 +1,57 @@ +get: + tags: + - utilities + summary: test route + description: This route responds with a simple `200 OK` if the Write API is enabled. Since there is no way of disabling the Write API, this will always return a success. However, it is also a good way to ensure the instance you are calling supports v3 of the Write API. + responses: + '200': + description: pingback + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../components/schemas/Status.yaml#/Status + response: + type: object + properties: + pong: + type: boolean + example: true +post: + tags: + - utilities + summary: test route + description: | + Requires authentication. This route bounces back the data payload sent to it, and the uid the token resolved to. + + It is also a good way to ensure the instance you are calling supports v3 of the Write API. Also, as it requires authentication, it is a good way to check if the passed-in token is a valid token. + requestBody: + required: false + content: + application/json: + schema: + type: object + properties: {} + additionalProperties: {} + responses: + '200': + description: pingback + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../components/schemas/Status.yaml#/Status + response: + type: object + properties: + uid: + type: number + description: The `uid` that the passed-in token resolves to. + received: + type: object + description: A free-form object containing the data that was passed to it. It reflects the data payload as the Write API understands it, and it may be useful to call this route to see how a request body is parsed, if at all. + additionalProperties: {} \ No newline at end of file diff --git a/public/openapi/write/posts/pid.yaml b/public/openapi/write/posts/pid.yaml index 2d439bd8f0..593a7acd01 100644 --- a/public/openapi/write/posts/pid.yaml +++ b/public/openapi/write/posts/pid.yaml @@ -1,3 +1,69 @@ +get: + tags: + - posts + summary: get a post + description: This operation retrieves a post's data + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 1 + responses: + '200': + description: Post successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + pid: + type: number + uid: + type: number + description: A user identifier + tid: + type: number + description: A topic identifier + content: + type: string + timestamp: + type: number + flagId: + type: number + deleted: + type: number + upvotes: + type: number + downvotes: + type: number + deleterUid: + type: number + edited: + type: number + replies: + type: number + bookmarks: + type: number + votes: + type: number + timestampISO: + type: string + description: An ISO 8601 formatted date string (complementing `timestamp`) + editedISO: + type: string + description: An ISO 8601 formatted date string (complementing `timestamp`) + upvoted: + type: boolean + downvoted: + type: boolean put: tags: - posts diff --git a/public/openapi/write/topics/tid.yaml b/public/openapi/write/topics/tid.yaml index 04a5e98aee..8e68efe25a 100644 --- a/public/openapi/write/topics/tid.yaml +++ b/public/openapi/write/topics/tid.yaml @@ -1,3 +1,28 @@ +get: + tags: + - topics + summary: get a topic + description: This operation retrieves a topic's data + parameters: + - in: path + name: tid + schema: + type: string + required: true + description: a valid topic id + example: 1 + responses: + '200': + description: Topic successfully retrieved + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../components/schemas/Status.yaml#/Status + response: + $ref: ../../components/schemas/TopicObject.yaml#/TopicObjectSlim post: tags: - topics diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js index 792560bf2b..5eb41bab9f 100644 --- a/public/src/admin/manage/group.js +++ b/public/src/admin/manage/group.js @@ -6,8 +6,9 @@ define('admin/manage/group', [ 'translator', 'categorySelector', 'groupSearch', + 'slugify', 'api', -], function (memberList, iconSelect, translator, categorySelector, groupSearch, api) { +], function (memberList, iconSelect, translator, categorySelector, groupSearch, slugify, api) { var Groups = {}; Groups.init = function () { @@ -38,7 +39,7 @@ define('admin/manage/group', [ groupLabelPreview.css('color', changeGroupTextColor.val() || '#ffffff'); }); - setupGroupMembersMenu(groupName); + setupGroupMembersMenu(); $('#group-icon, #group-icon-label').on('click', function () { var currentIcon = groupIcon.attr('value'); @@ -68,27 +69,20 @@ define('admin/manage/group', [ }); $('#save').on('click', function () { - socket.emit('admin.groups.update', { - groupName: groupName, - values: { - name: $('#change-group-name').val(), - userTitle: changeGroupUserTitle.val(), - description: $('#change-group-desc').val(), - icon: groupIcon.attr('value'), - labelColor: changeGroupLabelColor.val(), - textColor: changeGroupTextColor.val(), - userTitleEnabled: $('#group-userTitleEnabled').is(':checked'), - private: $('#group-private').is(':checked'), - hidden: $('#group-hidden').is(':checked'), - memberPostCids: $('#memberPostCids').val(), - disableJoinRequests: $('#group-disableJoinRequests').is(':checked'), - disableLeave: $('#group-disableLeave').is(':checked'), - }, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - + api.put(`/groups/${slugify(groupName)}`, { + name: $('#change-group-name').val(), + userTitle: changeGroupUserTitle.val(), + description: $('#change-group-desc').val(), + icon: groupIcon.attr('value'), + labelColor: changeGroupLabelColor.val(), + textColor: changeGroupTextColor.val(), + userTitleEnabled: $('#group-userTitleEnabled').is(':checked'), + private: $('#group-private').is(':checked'), + hidden: $('#group-hidden').is(':checked'), + memberPostCids: $('#memberPostCids').val(), + disableJoinRequests: $('#group-disableJoinRequests').is(':checked'), + disableLeave: $('#group-disableLeave').is(':checked'), + }).then(() => { var newName = $('#change-group-name').val(); // If the group name changed, change url @@ -97,12 +91,12 @@ define('admin/manage/group', [ } app.alertSuccess('[[admin/manage/groups:edit.save-success]]'); - }); + }).catch(app.alertError); return false; }); }; - function setupGroupMembersMenu(groupName) { + function setupGroupMembersMenu() { $('[component="groups/members"]').on('click', '[data-action]', function () { var btnEl = $(this); var userRow = btnEl.parents('[data-uid]'); @@ -113,15 +107,9 @@ define('admin/manage/group', [ switch (action) { case 'toggleOwnership': - socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), { - toUid: uid, - groupName: groupName, - }, function (err) { - if (err) { - return app.alertError(err.message); - } + api[isOwner ? 'del' : 'put'](`/groups/${ajaxify.data.group.slug}/ownership/${uid}`, {}).then(() => { ownerFlagEl.toggleClass('invisible'); - }); + }).catch(app.alertError); break; case 'kick': diff --git a/public/src/admin/manage/groups.js b/public/src/admin/manage/groups.js index c61c64aaf0..b7b181776a 100644 --- a/public/src/admin/manage/groups.js +++ b/public/src/admin/manage/groups.js @@ -2,8 +2,9 @@ define('admin/manage/groups', [ 'categorySelector', + 'slugify', 'api', -], function (categorySelector, api) { +], function (categorySelector, slugify, api) { var Groups = {}; var intervalId = 0; @@ -61,15 +62,7 @@ define('admin/manage/groups', [ case 'delete': bootbox.confirm('[[admin/manage/groups:alerts.confirm-delete]]', function (confirm) { if (confirm) { - socket.emit('groups.delete', { - groupName: groupName, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - - ajaxify.refresh(); - }); + api.del(`/groups/${slugify(groupName)}`, {}).then(ajaxify.refresh).catch(app.alertError); } }); break; diff --git a/public/src/admin/manage/privileges.js b/public/src/admin/manage/privileges.js index 0fab84ed20..49757db41e 100644 --- a/public/src/admin/manage/privileges.js +++ b/public/src/admin/manage/privileges.js @@ -50,6 +50,15 @@ define('admin/manage/privileges', [ checkboxEl.prop('checked', !checkboxEl.prop('checked')); } }); + } else if (privilege.endsWith('admin:admins-mods') && state) { + bootbox.confirm('[[admin/manage/privileges:alert.confirm-admins-mods]]', function (confirm) { + if (confirm) { + wrapperEl.attr('data-delta', delta); + Privileges.exposeAssumedPrivileges(); + } else { + checkboxEl.prop('checked', !checkboxEl.prop('checked')); + } + }); } else { wrapperEl.attr('data-delta', delta); Privileges.exposeAssumedPrivileges(); diff --git a/public/src/admin/settings/email.js b/public/src/admin/settings/email.js index 458163f8d7..8f2f7f46ed 100644 --- a/public/src/admin/settings/email.js +++ b/public/src/admin/settings/email.js @@ -22,6 +22,7 @@ define('admin/settings/email', ['ace/ace', 'admin/settings'], function (ace) { $('button[data-action="email.test"]').off('click').on('click', function () { socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) { if (err) { + console.error(err.message); return app.alertError(err.message); } app.alertSuccess('Test Email Sent'); diff --git a/public/src/ajaxify.js b/public/src/ajaxify.js index a5a6cae576..a7c91aaffb 100644 --- a/public/src/ajaxify.js +++ b/public/src/ajaxify.js @@ -14,7 +14,10 @@ ajaxify = window.ajaxify || {}; ajaxify.currentPage = null; ajaxify.go = function (url, callback, quiet) { + // Automatically reconnect to socket and re-ajaxify on success if (!socket.connected) { + app.reconnect(); + if (ajaxify.reconnectAction) { $(window).off('action:reconnected', ajaxify.reconnectAction); } @@ -409,6 +412,8 @@ ajaxify = window.ajaxify || {}; require(['translator', 'benchpress'], function (translator, Benchpress) { translator.translate('[[error:no-connection]]'); + translator.translate('[[error:socket-reconnect-failed]]'); + translator.translate(`[[global:reconnecting-message, ${config.siteTitle}]]`); Benchpress.registerLoader(ajaxify.loadTemplate); Benchpress.setGlobal('config', config); }); diff --git a/public/src/app.js b/public/src/app.js index 5e87d55086..ee1178e1b5 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -786,7 +786,7 @@ app.cacheBuster = null; function registerServiceWorker() { if ('serviceWorker' in navigator) { - navigator.serviceWorker.register(config.relative_path + '/service-worker.js') + navigator.serviceWorker.register(config.relative_path + '/assets/src/service-worker.js') .then(function () { console.info('ServiceWorker registration succeeded.'); }).catch(function (err) { diff --git a/public/src/client/account/profile.js b/public/src/client/account/profile.js index f69941e192..d75e451615 100644 --- a/public/src/client/account/profile.js +++ b/public/src/client/account/profile.js @@ -19,7 +19,7 @@ define('forum/account/profile', [ }; function processPage() { - $('[component="posts"] img:not(.not-responsive), [component="aboutme"] img:not(.not-responsive)').addClass('img-responsive'); + $('[component="posts"] [component="post/content"] img:not(.not-responsive), [component="aboutme"] img:not(.not-responsive)').addClass('img-responsive'); } function onUserStatusChange(data) { diff --git a/public/src/client/groups/details.js b/public/src/client/groups/details.js index 4680fa7171..9b240fc5d3 100644 --- a/public/src/client/groups/details.js +++ b/public/src/client/groups/details.js @@ -64,16 +64,9 @@ define('forum/groups/details', [ switch (action) { case 'toggleOwnership': - socket.emit('groups.' + (isOwner ? 'rescind' : 'grant'), { - toUid: uid, - groupName: groupName, - }, function (err) { - if (!err) { - ownerFlagEl.toggleClass('invisible'); - } else { - app.alertError(err.message); - } - }); + api[isOwner ? 'del' : 'put'](`/groups/${ajaxify.data.group.slug}/ownership/${uid}`, {}).then(() => { + ownerFlagEl.toggleClass('invisible'); + }).catch(app.alertError); break; case 'kick': @@ -83,16 +76,7 @@ define('forum/groups/details', [ return; } - socket.emit('groups.kick', { - uid: uid, - groupName: groupName, - }, function (err) { - if (!err) { - userRow.slideUp().remove(); - } else { - app.alertError(err.message); - } - }); + api.del(`/groups/${ajaxify.data.group.slug}/membership/${uid}`, undefined).then(() => userRow.slideUp().remove()).catch(app.alertError); }); }); break; @@ -203,14 +187,7 @@ define('forum/groups/details', [ } }); - socket.emit('groups.update', { - groupName: groupName, - values: settings, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - + api.put(`/groups/${ajaxify.data.group.slug}`, settings).then(() => { if (settings.name) { var pathname = window.location.pathname; pathname = pathname.substr(1, pathname.lastIndexOf('/')); @@ -220,7 +197,7 @@ define('forum/groups/details', [ } app.alertSuccess('[[groups:event.updated]]'); - }); + }).catch(app.alertError); } }; @@ -229,16 +206,10 @@ define('forum/groups/details', [ if (confirm) { bootbox.prompt('Please enter the name of this group in order to delete it:', function (response) { if (response === groupName) { - socket.emit('groups.delete', { - groupName: groupName, - }, function (err) { - if (!err) { - app.alertSuccess('[[groups:event.deleted, ' + utils.escapeHTML(groupName) + ']]'); - ajaxify.go('groups'); - } else { - app.alertError(err.message); - } - }); + api.del(`/groups/${ajaxify.data.group.slug}`, {}).then(() => { + app.alertSuccess('[[groups:event.deleted, ' + utils.escapeHTML(groupName) + ']]'); + ajaxify.go('groups'); + }).catch(app.alertError); } }); } diff --git a/public/src/client/topic/events.js b/public/src/client/topic/events.js index bb42e1e026..d98195434b 100644 --- a/public/src/client/topic/events.js +++ b/public/src/client/topic/events.js @@ -145,6 +145,8 @@ define('forum/topic/events', [ $(window).trigger('action:posts.edited', data); }); }); + } else { + $(window).trigger('action:posts.edited', data); } if (data.topic.tags && tagsUpdated(data.topic.tags)) { diff --git a/public/src/client/topic/move-post.js b/public/src/client/topic/move-post.js index 029891c8ca..16eb4590fb 100644 --- a/public/src/client/topic/move-post.js +++ b/public/src/client/topic/move-post.js @@ -30,12 +30,18 @@ define('forum/topic/move-post', [ postSelect.togglePostSelection(postEl, postEl.attr('data-pid')); } - $(window).off('action:axajify.end', checkMoveButtonEnable) + $(window).off('action:ajaxify.end', checkMoveButtonEnable) .on('action:ajaxify.end', checkMoveButtonEnable); moveCommit.on('click', function () { + if (!ajaxify.data.template.topic || !ajaxify.data.tid) { + return; + } moveCommit.attr('disabled', true); - + var data = { + pids: postSelect.pids.slice(), + tid: ajaxify.data.tid, + }; alerts.alert({ alert_id: 'pids_move_' + postSelect.pids.join('-'), title: '[[topic:thread_tools.move-posts]]', @@ -43,7 +49,7 @@ define('forum/topic/move-post', [ type: 'success', timeout: 10000, timeoutfn: function () { - movePosts(); + movePosts(data); }, clickfn: function (alert, params) { delete params.timeoutfn; @@ -90,15 +96,15 @@ define('forum/topic/move-post', [ checkMoveButtonEnable(); } - function movePosts() { - if (!ajaxify.data.template.topic || !ajaxify.data.tid) { + function movePosts(data) { + if (!ajaxify.data.template.topic || !data.tid) { return; } - socket.emit('posts.movePosts', { pids: postSelect.pids, tid: ajaxify.data.tid }, function (err) { + socket.emit('posts.movePosts', { pids: data.pids, tid: data.tid }, function (err) { if (err) { return app.alertError(err.message); } - postSelect.pids.forEach(function (pid) { + data.pids.forEach(function (pid) { components.get('post', 'pid', pid).fadeOut(500, function () { $(this).remove(); }); @@ -116,6 +122,5 @@ define('forum/topic/move-post', [ } } - return MovePost; }); diff --git a/public/src/client/topic/move.js b/public/src/client/topic/move.js index 706ca635f8..03c40b56d3 100644 --- a/public/src/client/topic/move.js +++ b/public/src/client/topic/move.js @@ -60,6 +60,12 @@ define('forum/topic/move', ['categorySelector', 'alerts'], function (categorySel } else if (!Move.tids) { message = '[[topic:topic_move_all_success, ' + selectedCategory.name + ']]'; } + var data = { + tids: Move.tids ? Move.tids.slice() : null, + cid: selectedCategory.cid, + currentCid: Move.currentCid, + onComplete: Move.onComplete, + }; alerts.alert({ alert_id: 'tids_move_' + (Move.tids ? Move.tids.join('-') : 'all'), title: '[[topic:thread_tools.move]]', @@ -67,7 +73,7 @@ define('forum/topic/move', ['categorySelector', 'alerts'], function (categorySel type: 'success', timeout: 10000, timeoutfn: function () { - moveTopics(); + moveTopics(data); }, clickfn: function (alert, params) { delete params.timeoutfn; @@ -77,26 +83,19 @@ define('forum/topic/move', ['categorySelector', 'alerts'], function (categorySel } } - function moveTopics() { - var data = { - tids: Move.tids, - cid: selectedCategory.cid, - currentCid: Move.currentCid, - }; - + function moveTopics(data) { $(window).trigger('action:topic.move', data); - socket.emit(Move.moveAll ? 'topics.moveAll' : 'topics.move', data, function (err) { + socket.emit(!data.tids ? 'topics.moveAll' : 'topics.move', data, function (err) { if (err) { return app.alertError(err.message); } - if (typeof Move.onComplete === 'function') { - Move.onComplete(); + if (typeof data.onComplete === 'function') { + data.onComplete(); } }); } - return Move; }); diff --git a/public/src/service-worker.js b/public/src/service-worker.js index 57a2243742..babbf24f19 100644 --- a/public/src/service-worker.js +++ b/public/src/service-worker.js @@ -1,6 +1,14 @@ 'use strict'; self.addEventListener('fetch', function (event) { + // This is the code that ignores post requests + // https://github.com/NodeBB/NodeBB/issues/9151 + // https://github.com/w3c/ServiceWorker/issues/1141 + // https://stackoverflow.com/questions/54448367/ajax-xmlhttprequest-progress-monitoring-doesnt-work-with-service-workers + if (event.request.method === 'POST') { + return; + } + event.respondWith(caches.match(event.request).then(function (response) { if (!response) { return fetch(event.request); diff --git a/public/src/sockets.js b/public/src/sockets.js index e19e2f08ba..80b639b1d1 100644 --- a/public/src/sockets.js +++ b/public/src/sockets.js @@ -39,14 +39,37 @@ socket = window.socket; addHandlers(); } + window.app.reconnect = () => { + if (socket.connected) { + return; + } + + var reconnectEl = $('#reconnect'); + $('#reconnect-alert') + .removeClass('alert-danger pointer') + .addClass('alert-warning') + .find('p') + .translateText(`[[global:reconnecting-message, ${config.siteTitle}]]`); + + reconnectEl.html(''); + socket.connect(); + }; + function addHandlers() { socket.on('connect', onConnect); socket.on('disconnect', onDisconnect); - socket.on('reconnect_failed', function () { - // Wait ten times the reconnection delay and then start over - setTimeout(socket.connect.bind(socket), parseInt(config.reconnectionDelay, 10) * 10); + socket.io.on('reconnect_failed', function () { + var reconnectEl = $('#reconnect'); + reconnectEl.html(''); + + $('#reconnect-alert') + .removeClass('alert-warning') + .addClass('alert-danger pointer') + .find('p') + .translateText('[[error:socket-reconnect-failed]]') + .one('click', app.reconnect); }); socket.on('checkSession', function (uid) { @@ -103,7 +126,7 @@ socket = window.socket; var reconnectAlert = $('#reconnect-alert'); reconnectEl.tooltip('destroy'); - reconnectEl.html(''); + reconnectEl.html(''); reconnectAlert.fadeOut(500); reconnecting = false; diff --git a/src/api/categories.js b/src/api/categories.js index 673155b436..a3df7860c9 100644 --- a/src/api/categories.js +++ b/src/api/categories.js @@ -2,9 +2,22 @@ const categories = require('../categories'); const events = require('../events'); +const privileges = require('../privileges'); const categoriesAPI = module.exports; +categoriesAPI.get = async function (caller, data) { + const [userPrivileges, category] = await Promise.all([ + privileges.categories.get(data.cid, caller.uid), + categories.getCategoryData(data.cid), + ]); + if (!category || !userPrivileges.read) { + return null; + } + + return category; +}; + categoriesAPI.create = async function (caller, data) { const response = await categories.create(data); const categoryObjs = await categories.getCategories([response.cid], caller.uid); diff --git a/src/api/groups.js b/src/api/groups.js index 426ec1dcde..9f68d31afe 100644 --- a/src/api/groups.js +++ b/src/api/groups.js @@ -33,6 +33,16 @@ groupsAPI.create = async function (caller, data) { return groupData; }; +groupsAPI.update = async function (caller, data) { + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + await isOwner(caller, groupName); + + delete data.slug; + await groups.update(groupName, data); + + return await groups.getGroupData(data.name || groupName); +}; + groupsAPI.delete = async function (caller, data) { const groupName = await groups.getGroupNameByGroupSlug(data.slug); await isOwner(caller, groupName); @@ -167,6 +177,28 @@ groupsAPI.leave = async function (caller, data) { }); }; +groupsAPI.grant = async (caller, data) => { + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + await isOwner(caller, groupName); + + await groups.ownership.grant(data.uid, groupName); + logGroupEvent(caller, 'group-owner-grant', { + groupName: groupName, + targetUid: data.uid, + }); +}; + +groupsAPI.rescind = async (caller, data) => { + const groupName = await groups.getGroupNameByGroupSlug(data.slug); + await isOwner(caller, groupName); + + await groups.ownership.rescind(data.uid, groupName); + logGroupEvent(caller, 'group-owner-rescind', { + groupName: groupName, + targetUid: data.uid, + }); +}; + async function isOwner(caller, groupName) { if (typeof groupName !== 'string') { throw new Error('[[error:invalid-group-name]]'); diff --git a/src/api/posts.js b/src/api/posts.js index ca77e1ce33..c01b377340 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -15,6 +15,31 @@ const websockets = require('../socket.io'); const postsAPI = module.exports; +postsAPI.get = async function (caller, data) { + const [userPrivileges, post, voted] = await Promise.all([ + privileges.posts.get([data.pid], caller.uid), + posts.getPostData(data.pid), + posts.hasVoted(data.pid, caller.uid), + ]); + if (!post) { + return null; + } + Object.assign(post, voted); + + const userPrivilege = userPrivileges[0]; + if (!userPrivilege.read || !userPrivilege['topics:read']) { + return null; + } + + post.ip = userPrivilege.isAdminOrMod ? post.ip : undefined; + const selfPost = caller.uid && caller.uid === parseInt(post.uid, 10); + if (post.deleted && !(userPrivilege.isAdminOrMod || selfPost)) { + post.content = '[[topic:post_is_deleted]]'; + } + + return post; +}; + postsAPI.edit = async function (caller, data) { if (!data || !data.pid || (meta.config.minimumPostLength !== 0 && !data.content)) { throw new Error('[[error:invalid-data]]'); diff --git a/src/api/topics.js b/src/api/topics.js index f313321c8e..de62942370 100644 --- a/src/api/topics.js +++ b/src/api/topics.js @@ -4,6 +4,7 @@ const user = require('../user'); const topics = require('../topics'); const posts = require('../posts'); const meta = require('../meta'); +const privileges = require('../privileges'); const apiHelpers = require('./helpers'); const doTopicAction = apiHelpers.doTopicAction; @@ -13,6 +14,18 @@ const socketHelpers = require('../socket.io/helpers'); const topicsAPI = module.exports; +topicsAPI.get = async function (caller, data) { + const [userPrivileges, topic] = await Promise.all([ + privileges.topics.get(data.tid, caller.uid), + topics.getTopicData(data.tid), + ]); + if (!topic || !userPrivileges.read || !userPrivileges['topics:read'] || (topic.deleted && !userPrivileges.view_deleted)) { + return null; + } + + return topic; +}; + topicsAPI.create = async function (caller, data) { if (!data) { throw new Error('[[error:invalid-data]]'); diff --git a/src/cli/manage.js b/src/cli/manage.js index 2f541149c0..c986a2725f 100644 --- a/src/cli/manage.js +++ b/src/cli/manage.js @@ -77,7 +77,7 @@ async function listPlugins() { const active = await db.getSortedSetRange('plugins:active', 0, -1); // Merge the two sets, defer to plugins in `installed` if already present - let combined = installed.concat(active.reduce((memo, cur) => { + const combined = installed.concat(active.reduce((memo, cur) => { if (!installedList.includes(cur)) { memo.push({ id: cur, @@ -90,12 +90,12 @@ async function listPlugins() { }, [])); // Alphabetical sort - combined = combined.sort((a, b) => (a.id > b.id ? 1 : -1)); + combined.sort((a, b) => (a.id > b.id ? 1 : -1)); // Pretty output process.stdout.write('Active plugins:\n'); combined.forEach((plugin) => { - process.stdout.write('\t* ' + plugin.id + ' ('); + process.stdout.write('\t* ' + plugin.id + (plugin.version ? '@' + plugin.version : '') + ' ('); process.stdout.write(plugin.installed ? 'installed'.green : 'not installed'.red); process.stdout.write(', '); process.stdout.write(plugin.active ? 'enabled'.green : 'disabled'.yellow); diff --git a/src/controllers/admin.js b/src/controllers/admin.js index b6412828a5..d616a67a6a 100644 --- a/src/controllers/admin.js +++ b/src/controllers/admin.js @@ -42,6 +42,12 @@ adminController.routeIndex = async (req, res) => { return helpers.redirect(res, 'admin/manage/privileges'); } else if (privilegeSet['admin:users']) { return helpers.redirect(res, 'admin/manage/users'); + } else if (privilegeSet['admin:groups']) { + return helpers.redirect(res, 'admin/manage/groups'); + } else if (privilegeSet['admin:admins-mods']) { + return helpers.redirect(res, 'admin/manage/admins-mods'); + } else if (privilegeSet['admin:tags']) { + return helpers.redirect(res, 'admin/manage/tags'); } else if (privilegeSet['admin:settings']) { return helpers.redirect(res, 'admin/settings/general'); } diff --git a/src/controllers/admin/settings.js b/src/controllers/admin/settings.js index e49a4755ae..46bca36835 100644 --- a/src/controllers/admin/settings.js +++ b/src/controllers/admin/settings.js @@ -22,7 +22,7 @@ settingsController.email = async (req, res) => { res.render('admin/settings/email', { emails: emails, - sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')), + sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path), services: emailer.listServices(), }); }; diff --git a/src/controllers/api.js b/src/controllers/api.js index 57e98ed0b7..ae2af4fa76 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -2,16 +2,15 @@ const validator = require('validator'); const nconf = require('nconf'); +const winston = require('winston'); const meta = require('../meta'); const user = require('../user'); -const posts = require('../posts'); -const topics = require('../topics'); const categories = require('../categories'); -const privileges = require('../privileges'); const plugins = require('../plugins'); const translator = require('../translator'); const languages = require('../languages'); +const api = require('../api'); const apiController = module.exports; @@ -22,7 +21,7 @@ const socketioOrigins = nconf.get('socket.io:origins'); const websocketAddress = nconf.get('socket.io:address') || ''; apiController.loadConfig = async function (req) { - let config = { + const config = { relative_path, upload_url, assetBaseUrl: `${relative_path}/assets`, @@ -49,8 +48,8 @@ apiController.loadConfig = async function (req) { socketioTransports, socketioOrigins, websocketAddress, - maxReconnectionAttempts: meta.config.maxReconnectionAttempts || 5, - reconnectionDelay: meta.config.reconnectionDelay || 1500, + maxReconnectionAttempts: meta.config.maxReconnectionAttempts, + reconnectionDelay: meta.config.reconnectionDelay, topicsPerPage: meta.config.topicsPerPage || 20, postsPerPage: meta.config.postsPerPage || 20, maximumFileSize: meta.config.maximumFileSize, @@ -83,8 +82,12 @@ apiController.loadConfig = async function (req) { }; let settings = config; + let isAdminOrGlobalMod; if (req.loggedIn) { - settings = await user.getSettings(req.uid); + ([settings, isAdminOrGlobalMod] = await Promise.all([ + user.getSettings(req.uid), + user.isAdminOrGlobalMod(req.uid), + ])); } // Handle old skin configs @@ -101,8 +104,11 @@ apiController.loadConfig = async function (req) { config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; config.topicSearchEnabled = settings.topicSearchEnabled || false; config.bootswatchSkin = (meta.config.disableCustomUserSkins !== 1 && settings.bootswatchSkin && settings.bootswatchSkin !== '') ? settings.bootswatchSkin : ''; - config = await plugins.hooks.fire('filter:config.get', config); - return config; + + // Overrides based on privilege + config.disableChatMessageEditing = isAdminOrGlobalMod ? false : config.disableChatMessageEditing; + + return await plugins.hooks.fire('filter:config.get', config); }; apiController.getConfig = async function (req, res) { @@ -110,52 +116,10 @@ apiController.getConfig = async function (req, res) { res.json(config); }; -apiController.getPostData = async function (pid, uid) { - const [userPrivileges, post, voted] = await Promise.all([ - privileges.posts.get([pid], uid), - posts.getPostData(pid), - posts.hasVoted(pid, uid), - ]); - if (!post) { - return null; - } - Object.assign(post, voted); - - const userPrivilege = userPrivileges[0]; - if (!userPrivilege.read || !userPrivilege['topics:read']) { - return null; - } - - post.ip = userPrivilege.isAdminOrMod ? post.ip : undefined; - const selfPost = uid && uid === parseInt(post.uid, 10); - if (post.deleted && !(userPrivilege.isAdminOrMod || selfPost)) { - post.content = '[[topic:post_is_deleted]]'; - } - return post; -}; - -apiController.getTopicData = async function (tid, uid) { - const [userPrivileges, topic] = await Promise.all([ - privileges.topics.get(tid, uid), - topics.getTopicData(tid), - ]); - if (!topic || !userPrivileges.read || !userPrivileges['topics:read'] || (topic.deleted && !userPrivileges.view_deleted)) { - return null; - } - return topic; -}; - -apiController.getCategoryData = async function (cid, uid) { - const [userPrivileges, category] = await Promise.all([ - privileges.categories.get(cid, uid), - categories.getCategoryData(cid), - ]); - if (!category || !userPrivileges.read) { - return null; - } - return category; -}; - +// TODO: Deprecate these four controllers in 1.17.0 +apiController.getPostData = async (pid, uid) => api.posts.get({ uid }, { pid }); +apiController.getTopicData = async (tid, uid) => api.topics.get({ uid }, { tid }); +apiController.getCategoryData = async (cid, uid) => api.categories.get({ uid }, { cid }); apiController.getObject = async function (req, res, next) { const methods = { post: apiController.getPostData, @@ -166,6 +130,10 @@ apiController.getObject = async function (req, res, next) { if (!method) { return next(); } + + winston.warn('[api] This route has been deprecated and will likely be removed in v1.17.0'); + winston.warn('[api] Use GET /api/v3/(posts|topics|categories)/:id instead'); + try { const result = await method(req.params.id, req.uid); if (!result) { diff --git a/src/controllers/write/categories.js b/src/controllers/write/categories.js index d9bbd8ece6..17f56ab78b 100644 --- a/src/controllers/write/categories.js +++ b/src/controllers/write/categories.js @@ -15,6 +15,10 @@ const hasAdminPrivilege = async (uid) => { } }; +Categories.get = async (req, res) => { + helpers.formatApiResponse(200, res, await api.categories.get(req, req.params)); +}; + Categories.create = async (req, res) => { await hasAdminPrivilege(req.uid); diff --git a/src/controllers/write/groups.js b/src/controllers/write/groups.js index 243d5b0cf7..f4019cb075 100644 --- a/src/controllers/write/groups.js +++ b/src/controllers/write/groups.js @@ -15,6 +15,14 @@ Groups.create = async (req, res) => { helpers.formatApiResponse(200, res, groupObj); }; +Groups.update = async (req, res) => { + const groupObj = await api.groups.update(req, { + ...req.body, + slug: req.params.slug, + }); + helpers.formatApiResponse(200, res, groupObj); +}; + Groups.delete = async (req, res) => { await api.groups.delete(req, req.params); helpers.formatApiResponse(200, res); @@ -29,3 +37,13 @@ Groups.leave = async (req, res) => { await api.groups.leave(req, req.params); helpers.formatApiResponse(200, res); }; + +Groups.grant = async (req, res) => { + await api.groups.grant(req, req.params); + helpers.formatApiResponse(200, res); +}; + +Groups.rescind = async (req, res) => { + await api.groups.rescind(req, req.params); + helpers.formatApiResponse(200, res); +}; diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 1a73cb7b1b..7988439a1f 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -8,6 +8,10 @@ const apiHelpers = require('../../api/helpers'); const Posts = module.exports; +Posts.get = async (req, res) => { + helpers.formatApiResponse(200, res, await api.posts.get(req, { pid: req.params.pid })); +}; + Posts.edit = async (req, res) => { const editResult = await api.posts.edit(req, { ...req.body, diff --git a/src/controllers/write/topics.js b/src/controllers/write/topics.js index c91654530b..c20cc6a269 100644 --- a/src/controllers/write/topics.js +++ b/src/controllers/write/topics.js @@ -12,6 +12,10 @@ const uploadsController = require('../uploads'); const Topics = module.exports; +Topics.get = async (req, res) => { + helpers.formatApiResponse(200, res, await api.topics.get(req, req.params)); +}; + Topics.create = async (req, res) => { const payload = await api.topics.create(req, req.body); if (payload.queued) { diff --git a/src/emailer.js b/src/emailer.js index 83e16f5d97..7a2fcdc46d 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -5,7 +5,7 @@ const nconf = require('nconf'); const Benchpress = require('benchpressjs'); const nodemailer = require('nodemailer'); const wellKnownServices = require('nodemailer/lib/well-known/services'); -const htmlToText = require('html-to-text'); +const { htmlToText } = require('html-to-text'); const url = require('url'); const path = require('path'); const fs = require('fs'); @@ -37,12 +37,9 @@ Emailer.listServices = () => Object.keys(wellKnownServices); Emailer._defaultPayload = {}; const smtpSettingsChanged = (config) => { - if (!prevConfig) { - prevConfig = meta.config; - } - const settings = [ 'email:smtpTransport:enabled', + 'email:smtpTransport:pool', 'email:smtpTransport:user', 'email:smtpTransport:pass', 'email:smtpTransport:service', @@ -113,8 +110,8 @@ Emailer.getTemplates = async (config) => { }; Emailer.setupFallbackTransport = (config) => { - winston.verbose('[emailer] Setting up SMTP fallback transport'); - // Enable Gmail transport if enabled in ACP + winston.verbose('[emailer] Setting up fallback transport'); + // Enable SMTP transport if enabled in ACP if (parseInt(config['email:smtpTransport:enabled'], 10) === 1) { const smtpOptions = { pool: config['email:smtpTransport:pool'], @@ -177,6 +174,11 @@ Emailer.registerApp = (expressApp) => { Emailer.setupFallbackTransport(meta.config); buildCustomTemplates(meta.config); + // need to shallow clone the config object + // otherwise prevConfig holds reference to meta.config object, + // which is updated before the pubsub handler is called + prevConfig = { ...meta.config }; + // Update default payload if new logo is uploaded pubsub.on('config:update', (config) => { if (config) { @@ -204,8 +206,7 @@ Emailer.registerApp = (expressApp) => { Emailer.send = async (template, uid, params) => { if (!app) { - winston.warn('[emailer] App not ready!'); - return; + throw Error('[emailer] App not ready!'); } const [userData, userSettings] = await Promise.all([ @@ -214,13 +215,13 @@ Emailer.send = async (template, uid, params) => { ]); if (!userData || !userData.email) { - winston.warn('uid : ' + uid + ' has no email, not sending.'); + winston.warn(`uid : ${uid} has no email, not sending "${template}" email.`); return; } const allowedTpls = ['verify_email', 'welcome', 'registration_accepted']; if (meta.config.requireEmailConfirmation && !userData['email:confirmed'] && !allowedTpls.includes(template)) { - winston.warn('uid : ' + uid + ' has not confirmed email, not sending "' + template + '" email.'); + winston.warn(`uid : ${uid} (${userData.email}) has not confirmed email, not sending "${template}" email.`); return; } @@ -229,11 +230,7 @@ Emailer.send = async (template, uid, params) => { params.uid = uid; params.username = userData.username; params.rtl = await translator.translate('[[language:dir]]', userSettings.userLang) === 'rtl'; - try { - await Emailer.sendToEmail(template, userData.email, userSettings.userLang, params); - } catch (err) { - winston.error(err.stack); - } + await Emailer.sendToEmail(template, userData.email, userSettings.userLang, params); }; Emailer.sendToEmail = async (template, email, language, params) => { @@ -287,8 +284,8 @@ Emailer.sendToEmail = async (template, email, language, params) => { from_name: meta.config['email:from_name'] || 'NodeBB', subject: '[' + meta.config.title + '] ' + _.unescape(subject), html: html, - plaintext: htmlToText.fromString(html, { - ignoreImage: true, + plaintext: htmlToText(html, { + tags: { img: { format: 'skip' } }, }), template: template, uid: params.uid, @@ -313,7 +310,7 @@ Emailer.sendToEmail = async (template, email, language, params) => { } }; -Emailer.sendViaFallback = (data, callback) => { +Emailer.sendViaFallback = async (data) => { // Some minor alterations to the data to conform to nodemailer standard data.text = data.plaintext; delete data.plaintext; @@ -323,12 +320,7 @@ Emailer.sendViaFallback = (data, callback) => { delete data.from_name; winston.verbose('[emailer] Sending email to uid ' + data.uid + ' (' + data.to + ')'); - Emailer.fallbackTransport.sendMail(data, (err) => { - if (err) { - winston.error(err.stack); - } - callback(); - }); + await Emailer.fallbackTransport.sendMail(data); }; Emailer.renderAndTranslate = async (template, params, lang) => { diff --git a/src/groups/index.js b/src/groups/index.js index 9cb83e7ab4..f98ef0e7a8 100644 --- a/src/groups/index.js +++ b/src/groups/index.js @@ -39,7 +39,6 @@ Groups.getEphemeralGroup = function (groupName) { name: groupName, slug: slugify(groupName), description: '', - deleted: 0, hidden: 0, system: 1, }; @@ -55,7 +54,7 @@ Groups.removeEphemeralGroups = function (groups) { return groups; }; -var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/; +var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w\-:]+$/; Groups.isPrivilegeGroup = function (groupName) { return isPrivilegeGroupRegex.test(groupName); }; diff --git a/src/groups/ownership.js b/src/groups/ownership.js index 4296c307c0..9f0ce9a08f 100644 --- a/src/groups/ownership.js +++ b/src/groups/ownership.js @@ -22,16 +22,15 @@ module.exports = function (Groups) { }; Groups.ownership.grant = async function (toUid, groupName) { - // Note: No ownership checking is done here on purpose! await db.setAdd('group:' + groupName + ':owners', toUid); plugins.hooks.fire('action:group.grantOwnership', { uid: toUid, groupName: groupName }); }; Groups.ownership.rescind = async function (toUid, groupName) { - // Note: No ownership checking is done here on purpose! - // If the owners set only contains one member, error out! + // If the owners set only contains one member (and toUid is that member), error out! const numOwners = await db.setCount('group:' + groupName + ':owners'); - if (numOwners <= 1) { + const isOwner = await db.isSortedSetMember(`group:${groupName}:owners`); + if (numOwners <= 1 && isOwner) { throw new Error('[[error:group-needs-owner]]'); } await db.setRemove('group:' + groupName + ':owners', toUid); diff --git a/src/groups/update.js b/src/groups/update.js index 26d0cdad5c..0e34169660 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -24,6 +24,13 @@ module.exports = function (Groups) { values: values, })); + // Case some values as bool (if not boolean already) + ['userTitleEnabled', 'private', 'hidden', 'disableJoinRequests', 'disableLeave'].forEach((prop) => { + if (values.hasOwnProperty(prop) && typeof values[prop] !== 'boolean') { + values[prop] = !!parseInt(values[prop], 10); + } + }); + const payload = { description: values.description || '', icon: values.icon || '', diff --git a/src/messaging/edit.js b/src/messaging/edit.js index 3e94d55bbb..2ad14d2126 100644 --- a/src/messaging/edit.js +++ b/src/messaging/edit.js @@ -47,9 +47,11 @@ module.exports = function (Messaging) { durationConfig = 'chatDeleteDuration'; } + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(uid); + if (meta.config.disableChat) { throw new Error('[[error:chat-disabled]]'); - } else if (meta.config.disableChatMessageEditing) { + } else if (!isAdminOrGlobalMod && meta.config.disableChatMessageEditing) { throw new Error('[[error:chat-message-editing-disabled]]'); } @@ -57,19 +59,17 @@ module.exports = function (Messaging) { if (userData.banned) { throw new Error('[[error:user-banned]]'); } + const canChat = await privileges.global.can('chat', uid); if (!canChat) { throw new Error('[[error:no-privileges]]'); } - const [isAdmin, messageData] = await Promise.all([ - user.isAdministrator(uid), - Messaging.getMessageFields(messageId, ['fromuid', 'timestamp', 'system']), - ]); - - if (isAdmin && !messageData.system) { + const messageData = await Messaging.getMessageFields(messageId, ['fromuid', 'timestamp', 'system']); + if (isAdminOrGlobalMod && !messageData.system) { return; } + const chatConfigDuration = meta.config[durationConfig]; if (chatConfigDuration && Date.now() - messageData.timestamp > chatConfigDuration * 1000) { throw new Error('[[error:chat-' + type + '-duration-expired, ' + meta.config[durationConfig] + ']]'); diff --git a/src/middleware/admin.js b/src/middleware/admin.js index 021ccd2785..3c4cc9f9fc 100644 --- a/src/middleware/admin.js +++ b/src/middleware/admin.js @@ -73,11 +73,19 @@ middleware.renderHeader = async (req, res, data) => { version: version, latestVersion: results.latestVersion, upgradeAvailable: results.latestVersion && semver.gt(results.latestVersion, version), - showManageMenu: results.privileges.superadmin || ['categories', 'privileges', 'users', 'settings'].some(priv => results.privileges[`admin:${priv}`]), + showManageMenu: results.privileges.superadmin || ['categories', 'privileges', 'users', 'admins-mods', 'groups', 'tags', 'settings'].some(priv => results.privileges[`admin:${priv}`]), }; templateValues.template = { name: res.locals.template }; templateValues.template[res.locals.template] = true; + // remove @1.17.0 + ({ templateData: templateValues } = await plugins.hooks.fire('filter:admin/header.build', { req, res, templateData: templateValues })); + ({ templateData: templateValues } = await plugins.hooks.fire('filter:middleware.renderAdminHeader', { + req, + res, + templateData: templateValues, + data, + })); return await req.app.renderAsync('admin/header', templateValues); }; diff --git a/src/notifications.js b/src/notifications.js index df0429c654..b98a987cb8 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -181,8 +181,8 @@ async function pushToUids(uids, notification) { } body = posts.relativeToAbsolute(body, posts.urlRegex); body = posts.relativeToAbsolute(body, posts.imgRegex); - await async.eachLimit(uids, 3, function (uid, next) { - emailer.send('notification', uid, { + await async.eachLimit(uids, 3, async (uid) => { + await emailer.send('notification', uid, { path: notification.path, notification_url: notification.path.startsWith('http') ? notification.path : nconf.get('url') + notification.path, subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'), @@ -190,7 +190,7 @@ async function pushToUids(uids, notification) { body: body, notification: notification, showUnsubscribe: true, - }, next); + }).catch(err => winston.error('[emailer.send] ' + err.stack)); }); } diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js index a2509b35dc..0ed353e946 100644 --- a/src/plugins/hooks.js +++ b/src/plugins/hooks.js @@ -9,7 +9,8 @@ const Hooks = {}; module.exports = Hooks; Hooks.deprecatedHooks = { - 'filter:privileges:isUserAllowedTo': 'filter:privileges:isAllowedTo', // 👋 @ 1.16.0 + 'filter:privileges:isUserAllowedTo': 'filter:privileges:isAllowedTo', // 👋 @ 1.17.0 + 'filter:admin/header.build': 'filter:middleware.renderAdminHeader', // 👋 @ 1.17.0 'filter:router.page': 'response:router.page', // 👋 @ 2.0.0 }; diff --git a/src/privileges/admin.js b/src/privileges/admin.js index 31c0b3022d..004d3e862a 100644 --- a/src/privileges/admin.js +++ b/src/privileges/admin.js @@ -16,7 +16,10 @@ module.exports = function (privileges) { { name: '[[admin/manage/privileges:admin-dashboard]]' }, { name: '[[admin/manage/privileges:admin-categories]]' }, { name: '[[admin/manage/privileges:admin-privileges]]' }, + { name: '[[admin/manage/privileges:admin-admins-mods]]' }, { name: '[[admin/manage/privileges:admin-users]]' }, + { name: '[[admin/manage/privileges:admin-groups]]' }, + { name: '[[admin/manage/privileges:admin-tags]]' }, { name: '[[admin/manage/privileges:admin-settings]]' }, ]; @@ -24,7 +27,10 @@ module.exports = function (privileges) { 'admin:dashboard', 'admin:categories', 'admin:privileges', + 'admin:admins-mods', 'admin:users', + 'admin:groups', + 'admin:tags', 'admin:settings', ]; @@ -35,7 +41,11 @@ module.exports = function (privileges) { dashboard: 'admin:dashboard', 'manage/categories': 'admin:categories', 'manage/privileges': 'admin:privileges', + 'manage/admins-mods': 'admin:admins-mods', 'manage/users': 'admin:users', + 'manage/groups': 'admin:groups', + 'manage/tags': 'admin:tags', + 'settings/tags': 'admin:tags', 'extend/plugins': 'admin:settings', 'extend/widgets': 'admin:settings', 'extend/rewards': 'admin:settings', @@ -43,6 +53,7 @@ module.exports = function (privileges) { privileges.admin.routeRegexpMap = { '^manage/categories/\\d+': 'admin:categories', '^manage/privileges/(\\d+|admin)': 'admin:privileges', + '^manage/groups/.+$': 'admin:groups', '^settings/[\\w\\-]+$': 'admin:settings', '^appearance/[\\w]+$': 'admin:settings', '^plugins/[\\w\\-]+$': 'admin:settings', @@ -61,11 +72,14 @@ module.exports = function (privileges) { 'admin.categories.copySettingsFrom': 'admin:categories', 'admin.categories.getPrivilegeSettings': 'admin:privileges', - 'admin.categories.setPrivilege': 'admin:privileges', + 'admin.categories.setPrivilege': 'admin:privileges;admin:admins-mods', 'admin.categories.copyPrivilegesToChildren': 'admin:privileges', 'admin.categories.copyPrivilegesFrom': 'admin:privileges', 'admin.categories.copyPrivilegesToAllCategories': 'admin:privileges', + 'admin.user.makeAdmins': 'admin:admins-mods', + 'admin.user.removeAdmins': 'admin:admins-mods', + 'admin.user.loadGroups': 'admin:users', 'admin.groups.join': 'admin:users', 'admin.groups.leave': 'admin:users', @@ -79,6 +93,11 @@ module.exports = function (privileges) { 'admin.user.createUser': 'admin:users', 'admin.user.invite': 'admin:users', + 'admin.tags.create': 'admin:tags', + 'admin.tags.update': 'admin:tags', + 'admin.tags.rename': 'admin:tags', + 'admin.tags.deleteTags': 'admin:tags', + 'admin.getSearchDict': 'admin:settings', 'admin.config.setMultiple': 'admin:settings', 'admin.config.remove': 'admin:settings', @@ -86,6 +105,7 @@ module.exports = function (privileges) { 'admin.themes.set': 'admin:settings', 'admin.reloadAllSessions': 'admin:settings', 'admin.settings.get': 'admin:settings', + 'admin.settings.set': 'admin:settings', }; privileges.admin.resolve = (path) => { @@ -138,7 +158,7 @@ module.exports = function (privileges) { payload.keys = keys; // This is a hack because I can't do {labels.users.length} to echo the count in templates.js - payload.columnCount = payload.labels.users.length + 2; + payload.columnCount = payload.labels.users.length + 3; return payload; }; diff --git a/src/routes/api.js b/src/routes/api.js index 3280246626..3461a28550 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -19,6 +19,7 @@ module.exports = function (app, middleware, controllers) { router.get('/user/uid/:userslug/export/uploads', middleware.checkAccountPermissions, middleware.exposeUid, controllers.user.exportUploads); router.get('/user/uid/:userslug/export/profile', middleware.checkAccountPermissions, middleware.exposeUid, controllers.user.exportProfile); + // TODO: Deprecate in v1.17.0 router.get('/:type/pid/:id', middleware.authenticateOrGuest, controllers.api.getObject); router.get('/:type/tid/:id', middleware.authenticateOrGuest, controllers.api.getObject); router.get('/:type/cid/:id', middleware.authenticateOrGuest, controllers.api.getObject); diff --git a/src/routes/write/categories.js b/src/routes/write/categories.js index beb73b90b8..a87456a657 100644 --- a/src/routes/write/categories.js +++ b/src/routes/write/categories.js @@ -11,6 +11,7 @@ module.exports = function () { const middlewares = [middleware.authenticate]; setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.categories.create); + setupApiRoute(router, 'get', '/:cid', [middleware.authenticateOrGuest], controllers.write.categories.get); setupApiRoute(router, 'put', '/:cid', [...middlewares], controllers.write.categories.update); setupApiRoute(router, 'delete', '/:cid', [...middlewares], controllers.write.categories.delete); diff --git a/src/routes/write/groups.js b/src/routes/write/groups.js index 64af35bf70..e5adf28a40 100644 --- a/src/routes/write/groups.js +++ b/src/routes/write/groups.js @@ -12,9 +12,12 @@ module.exports = function () { setupApiRoute(router, 'post', '/', [...middlewares, middleware.checkRequired.bind(null, ['name'])], controllers.write.groups.create); setupApiRoute(router, 'head', '/:slug', [middleware.assert.group], controllers.write.groups.exists); + setupApiRoute(router, 'put', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.update); setupApiRoute(router, 'delete', '/:slug', [...middlewares, middleware.assert.group], controllers.write.groups.delete); setupApiRoute(router, 'put', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.join); setupApiRoute(router, 'delete', '/:slug/membership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.leave); + setupApiRoute(router, 'put', '/:slug/ownership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.grant); + setupApiRoute(router, 'delete', '/:slug/ownership/:uid', [...middlewares, middleware.assert.group], controllers.write.groups.rescind); return router; }; diff --git a/src/routes/write/index.js b/src/routes/write/index.js index 539668d508..6cf266acdb 100644 --- a/src/routes/write/index.js +++ b/src/routes/write/index.js @@ -48,6 +48,7 @@ Write.reload = async (params) => { router.post('/api/v3/ping', middleware.authenticate, function (req, res) { helpers.formatApiResponse(200, res, { uid: req.user.uid, + received: req.body, }); }); diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index f445873a1a..837f8def8c 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -10,6 +10,8 @@ const setupApiRoute = routeHelpers.setupApiRoute; module.exports = function () { const middlewares = [middleware.authenticate]; + setupApiRoute(router, 'get', '/:pid', [middleware.authenticateOrGuest], controllers.write.posts.get); + // There is no POST route because you POST to a topic to create a new post. Intuitive, no? setupApiRoute(router, 'put', '/:pid', [...middlewares, middleware.checkRequired.bind(null, ['content'])], controllers.write.posts.edit); setupApiRoute(router, 'delete', '/:pid', [...middlewares, middleware.assert.post], controllers.write.posts.purge); diff --git a/src/routes/write/topics.js b/src/routes/write/topics.js index 9203146588..d2c05a38bd 100644 --- a/src/routes/write/topics.js +++ b/src/routes/write/topics.js @@ -14,6 +14,7 @@ module.exports = function () { var multipartMiddleware = multipart(); setupApiRoute(router, 'post', '/', [middleware.authenticateOrGuest, middleware.checkRequired.bind(null, ['cid', 'title', 'content'])], controllers.write.topics.create); + setupApiRoute(router, 'get', '/:tid', [middleware.authenticateOrGuest], controllers.write.topics.get); setupApiRoute(router, 'post', '/:tid', [middleware.authenticateOrGuest, middleware.checkRequired.bind(null, ['content']), middleware.assert.topic], controllers.write.topics.reply); setupApiRoute(router, 'delete', '/:tid', [...middlewares], controllers.write.topics.purge); diff --git a/src/socket.io/admin.js b/src/socket.io/admin.js index 631914f05d..0cd8b3d452 100644 --- a/src/socket.io/admin.js +++ b/src/socket.io/admin.js @@ -41,8 +41,9 @@ SocketAdmin.before = async function (socket, method) { } // Check admin privileges mapping (if not in mapping, deny access) - const privilege = privileges.admin.socketMap[method]; - if (privilege && await privileges.admin.can(privilege, socket.uid)) { + const privilegeSet = privileges.admin.socketMap.hasOwnProperty(method) ? privileges.admin.socketMap[method].split(';') : []; + const hasPrivilege = (await Promise.all(privilegeSet.map(async privilege => privileges.admin.can(privilege, socket.uid)))).some(Boolean); + if (privilegeSet.length && hasPrivilege) { return; } diff --git a/src/socket.io/admin/email.js b/src/socket.io/admin/email.js index 04874675b9..584e5785e4 100644 --- a/src/socket.io/admin/email.js +++ b/src/socket.io/admin/email.js @@ -1,25 +1,24 @@ 'use strict'; -const async = require('async'); const userDigest = require('../../user/digest'); const userEmail = require('../../user/email'); const notifications = require('../../notifications'); const emailer = require('../../emailer'); -const utils = require('../../../public/src/utils'); +const utils = require('../../utils'); const Email = module.exports; -Email.test = function (socket, data, callback) { +Email.test = async function (socket, data) { const payload = { subject: '[[email:test-email.subject]]', }; switch (data.template) { case 'digest': - userDigest.execute({ + await userDigest.execute({ interval: 'alltime', subscribers: [socket.uid], - }, callback); + }); break; case 'banned': @@ -28,42 +27,36 @@ Email.test = function (socket, data, callback) { until: utils.toISOString(Date.now()), reason: 'Test Reason', }); - emailer.send(data.template, socket.uid, payload, callback); + await emailer.send(data.template, socket.uid, payload); break; case 'welcome': - userEmail.sendValidationEmail(socket.uid, { + await userEmail.sendValidationEmail(socket.uid, { force: 1, - }, callback); + }); break; - case 'notification': - async.waterfall([ - function (next) { - notifications.create({ - type: 'test', - bodyShort: '[[email:notif.test.short]]', - bodyLong: '[[email:notif.test.long]]', - nid: 'uid:' + socket.uid + ':test', - path: '/', - from: socket.uid, - }, next); - }, - function (notifObj, next) { - emailer.send('notification', socket.uid, { - path: notifObj.path, - subject: utils.stripHTMLTags(notifObj.subject || '[[notifications:new_notification]]'), - intro: utils.stripHTMLTags(notifObj.bodyShort), - body: notifObj.bodyLong || '', - notification: notifObj, - showUnsubscribe: true, - }, next); - }, - ], callback); - break; + case 'notification': { + const notification = await notifications.create({ + type: 'test', + bodyShort: '[[email:notif.test.short]]', + bodyLong: '[[email:notif.test.long]]', + nid: 'uid:' + socket.uid + ':test', + path: '/', + from: socket.uid, + }); + await emailer.send('notification', socket.uid, { + path: notification.path, + subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'), + intro: utils.stripHTMLTags(notification.bodyShort), + body: notification.bodyLong || '', + notification, + showUnsubscribe: true, + }); + } break; default: - emailer.send(data.template, socket.uid, payload, callback); + await emailer.send(data.template, socket.uid, payload); break; } }; diff --git a/src/socket.io/admin/groups.js b/src/socket.io/admin/groups.js index ef3cb3f878..6aa609288e 100644 --- a/src/socket.io/admin/groups.js +++ b/src/socket.io/admin/groups.js @@ -43,9 +43,12 @@ Groups.leave = async function (socket, data) { }; Groups.update = async function (socket, data) { + sockets.warnDeprecated(socket, 'PUT /api/v3/groups/:slug'); if (!data) { throw new Error('[[error:invalid-data]]'); } - await groups.update(data.groupName, data.values); + const slug = await groups.getGroupField(data.groupName, 'slug'); + await api.groups.update(socket, { slug, ...data.values }); + // await groups.update(data.groupName, data.values); }; diff --git a/src/socket.io/admin/rooms.js b/src/socket.io/admin/rooms.js index 1dc58f1c13..705d3d9e9d 100644 --- a/src/socket.io/admin/rooms.js +++ b/src/socket.io/admin/rooms.js @@ -131,10 +131,10 @@ SocketRooms.getLocalStats = function () { topics: {}, }; - if (io) { + if (io && io.sockets) { socketData.onlineGuestCount = Sockets.getCountInRoom('online_guests'); socketData.onlineRegisteredCount = SocketRooms.getOnlineUserCount(io); - socketData.socketCount = Object.keys(io.sockets.sockets).length; + socketData.socketCount = io.sockets.sockets.size; socketData.users.categories = Sockets.getCountInRoom('categories'); socketData.users.recent = Sockets.getCountInRoom('recent_topics'); socketData.users.unread = Sockets.getCountInRoom('unread_topics'); diff --git a/src/socket.io/admin/user.js b/src/socket.io/admin/user.js index a0070d7d37..8e1c91a2ab 100644 --- a/src/socket.io/admin/user.js +++ b/src/socket.io/admin/user.js @@ -87,9 +87,18 @@ User.sendValidationEmail = async function (socket, uids) { throw new Error('[[error:email-confirmations-are-disabled]]'); } + const failed = []; + await async.eachLimit(uids, 50, async function (uid) { - await user.email.sendValidationEmail(uid, { force: true }); + await user.email.sendValidationEmail(uid, { force: true }).catch((err) => { + winston.error('[user.create] Validation email failed to send\n[emailer.send] ' + err.stack); + failed.push(uid); + }); }); + + if (failed.length) { + throw Error(`Email sending failed for the following uids, check server logs for more info: ${failed.join(',')}`); + } }; User.sendPasswordResetEmail = async function (socket, uids) { diff --git a/src/socket.io/categories.js b/src/socket.io/categories.js index 7b2c35b4aa..b7aabb8174 100644 --- a/src/socket.io/categories.js +++ b/src/socket.io/categories.js @@ -4,7 +4,8 @@ const categories = require('../categories'); const privileges = require('../privileges'); const user = require('../user'); const topics = require('../topics'); -const apiController = require('../controllers/api'); +const api = require('../api'); +const sockets = require('.'); const SocketCategories = module.exports; @@ -147,7 +148,9 @@ SocketCategories.isModerator = async function (socket, cid) { }; SocketCategories.getCategory = async function (socket, cid) { - return await apiController.getCategoryData(cid, socket.uid); + sockets.warnDeprecated(socket, 'GET /api/v3/categories/:tid'); + return await api.categories.get(socket, { cid }); + // return await apiController.getCategoryData(cid, socket.uid); }; require('../promisify')(SocketCategories); diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index 4e1152b514..73f6af7d81 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -79,6 +79,8 @@ async function isInvited(socket, data) { } SocketGroups.grant = async (socket, data) => { + sockets.warnDeprecated(socket, 'PUT /api/v3/groups/:slug/ownership/:uid'); + await isOwner(socket, data); await groups.ownership.grant(data.toUid, data.groupName); logGroupEvent(socket, 'group-owner-grant', { @@ -88,6 +90,8 @@ SocketGroups.grant = async (socket, data) => { }; SocketGroups.rescind = async (socket, data) => { + sockets.warnDeprecated(socket, 'DELETE /api/v3/groups/:slug/ownership/:uid'); + await isOwner(socket, data); await groups.ownership.rescind(data.toUid, data.groupName); logGroupEvent(socket, 'group-owner-rescind', { @@ -186,8 +190,11 @@ SocketGroups.rejectInvite = async (socket, data) => { }; SocketGroups.update = async (socket, data) => { + sockets.warnDeprecated(socket, 'PUT /api/v3/groups/:slug'); await isOwner(socket, data); - await groups.update(data.groupName, data.values); + + const slug = await groups.getGroupField(data.groupName, 'slug'); + await api.groups.update(socket, { slug, ...data.values }); }; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 27930e3824..e9fc770338 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -10,8 +10,7 @@ const categories = require('../categories'); const user = require('../user'); const socketHelpers = require('./helpers'); const utils = require('../utils'); - -const apiController = require('../controllers/api'); +const api = require('../api'); const sockets = require('.'); const SocketPosts = module.exports; @@ -86,17 +85,19 @@ SocketPosts.getPostSummaryByIndex = async function (socket, data) { return 0; } - const canRead = await privileges.posts.can('topics:read', pid, socket.uid); - if (!canRead) { + const topicPrivileges = await privileges.topics.get(data.tid, socket.uid); + if (!topicPrivileges['topics:read']) { throw new Error('[[error:no-privileges]]'); } const postsData = await posts.getPostSummaryByPids([pid], socket.uid, { stripTags: false }); + posts.modifyPostByPrivilege(postsData[0], topicPrivileges); return postsData[0]; }; SocketPosts.getPost = async function (socket, pid) { - return await apiController.getPostData(pid, socket.uid); + sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid'); + return await api.posts.get(socket, { pid }); }; SocketPosts.loadMoreBookmarks = async function (socket, data) { diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index ecdc0e12cd..de4d356ad6 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -4,7 +4,6 @@ const api = require('../api'); const topics = require('../topics'); const user = require('../user'); const meta = require('../meta'); -const apiController = require('../controllers/api'); const privileges = require('../privileges'); const sockets = require('.'); @@ -96,7 +95,8 @@ SocketTopics.isModerator = async function (socket, tid) { }; SocketTopics.getTopic = async function (socket, tid) { - return await apiController.getTopicData(tid, socket.uid); + sockets.warnDeprecated(socket, 'GET /api/v3/topics/:tid'); + return await api.topics.get(socket, { tid }); }; require('../promisify')(SocketTopics); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index ed7466a377..6e7f03321f 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -1,6 +1,7 @@ 'use strict'; const util = require('util'); +const winston = require('winston'); const sleep = util.promisify(setTimeout); const api = require('../api'); @@ -117,7 +118,7 @@ SocketUser.reset.commit = async function (socket, data) { username: username, date: parsedDate, subject: '[[email:reset.notify.subject]]', - }); + }).catch(err => winston.error('[emailer.send] ' + err.stack)); }; SocketUser.isFollowing = async function (socket, data) { diff --git a/src/topics/create.js b/src/topics/create.js index aacee91432..c6daa389bd 100644 --- a/src/topics/create.js +++ b/src/topics/create.js @@ -35,9 +35,6 @@ module.exports = function (Topics) { postcount: 0, viewcount: 0, }; - if (data.thumb) { - topicData.thumb = data.thumb; - } const result = await plugins.hooks.fire('filter:topic.create', { topic: topicData, data: data }); topicData = result.topic; await db.setObject('topic:' + topicData.tid, topicData); diff --git a/src/topics/data.js b/src/topics/data.js index f518a5f5c0..430236b7d5 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -98,6 +98,7 @@ function modifyTopic(topic, fields) { escapeTitle(topic); + // TODO: Remove in v1.17.0 if (topic.hasOwnProperty('thumb')) { topic.thumb = validator.escape(String(topic.thumb)); } diff --git a/src/topics/thumbs.js b/src/topics/thumbs.js index e8709a46b4..d65b99b3c4 100644 --- a/src/topics/thumbs.js +++ b/src/topics/thumbs.js @@ -31,13 +31,17 @@ Thumbs.get = async function (tids) { return singular ? null : tids.map(() => []); } + const hasTimestampPrefix = /^\d+-/; const upload_url = nconf.get('upload_url'); const sets = tids.map(tid => `${validator.isUUID(String(tid)) ? 'draft' : 'topic'}:${tid}:thumbs`); const thumbs = await Promise.all(sets.map(set => getThumbs(set))); let response = thumbs.map((thumbSet, idx) => thumbSet.map(thumb => ({ id: tids[idx], - name: path.basename(thumb), - url: thumb.startsWith('http') ? thumb : path.join(upload_url, thumb), + name: (() => { + const name = path.basename(thumb); + return hasTimestampPrefix.test(name) ? name.slice(14) : name; + })(), + url: thumb.startsWith('http') ? thumb : path.posix.join(upload_url, thumb), }))); ({ thumbs: response } = await plugins.hooks.fire('filter:topics.getThumbs', { tids, thumbs: response })); diff --git a/src/upgrades/1.16.0/migrate_thumbs.js b/src/upgrades/1.16.0/migrate_thumbs.js index b5ebd4c5cc..34851568e7 100644 --- a/src/upgrades/1.16.0/migrate_thumbs.js +++ b/src/upgrades/1.16.0/migrate_thumbs.js @@ -17,16 +17,18 @@ module.exports = { if (parseInt(current, 10) === 120) { await meta.configs.set('topicThumbSize', 512); } - await meta.configs.set('allowTopicsThumbnail', 1); await batch.processSortedSet('topics:tid', async function (tids) { const keys = tids.map(tid => `topic:${tid}`); - const topicThumbs = (await db.getObjectsFields(keys, ['thumb'])).map(obj => (obj.thumb ? obj.thumb.replace(nconf.get('upload_url'), '') : null)); + const topicThumbs = (await db.getObjectsFields(keys, ['thumb'])) + .map(obj => (obj.thumb ? obj.thumb.replace(nconf.get('upload_url'), '') : null)); await Promise.all(tids.map(async (tid, idx) => { const path = topicThumbs[idx]; if (path) { - await topics.thumbs.associate({ id: tid, path }); + if (path.length < 255 && !path.startsWith('data:')) { + await topics.thumbs.associate({ id: tid, path }); + } await db.deleteObjectField(keys[idx], 'thumb'); } diff --git a/src/user/approval.js b/src/user/approval.js index 507aa7ef1b..94514963e4 100644 --- a/src/user/approval.js +++ b/src/user/approval.js @@ -1,6 +1,7 @@ 'use strict'; const validator = require('validator'); +const winston = require('winston'); const cronJob = require('cron').CronJob; const db = require('../database'); @@ -78,7 +79,7 @@ module.exports = function (User) { subject: '[[email:welcome-to, ' + (meta.config.title || meta.config.browserTitle || 'NodeBB') + ']]', template: 'registration_accepted', uid: uid, - }); + }).catch(err => winston.error('[emailer.send] ' + err.stack)); const total = await db.incrObjectField('registration:queue:approval:times', 'totalTime', Math.floor((Date.now() - creation_time) / 60000)); const counter = await db.incrObjectField('registration:queue:approval:times', 'counter', 1); await db.setObjectField('registration:queue:approval:times', 'average', total / counter); diff --git a/src/user/bans.js b/src/user/bans.js index d35370cb57..617ecce3bc 100644 --- a/src/user/bans.js +++ b/src/user/bans.js @@ -53,11 +53,7 @@ module.exports = function (User) { until: until ? (new Date(until)).toUTCString().replace(/,/g, '\\,') : false, reason: reason, }; - try { - await emailer.send('banned', uid, data); - } catch (err) { - winston.error('[emailer.send] ' + err.message); - } + await emailer.send('banned', uid, data).catch(err => winston.error('[emailer.send] ' + err.stack)); return banData; }; diff --git a/src/user/create.js b/src/user/create.js index b622a8b5a3..690f7b7846 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -1,6 +1,8 @@ 'use strict'; const zxcvbn = require('zxcvbn'); +const winston = require('winston'); + const db = require('../database'); const utils = require('../utils'); const slugify = require('../slugify'); @@ -116,7 +118,7 @@ module.exports = function (User) { if (userData.email && userData.uid > 1 && meta.config.requireEmailConfirmation) { User.email.sendValidationEmail(userData.uid, { email: userData.email, - }); + }).catch(err => winston.error('[user.create] Validation email failed to send\n[emailer.send] ' + err.stack)); } if (userNameChanged) { await User.notifications.sendNameChangeNotification(userData.uid, userData.username); diff --git a/src/user/digest.js b/src/user/digest.js index 689d0ff771..4ae34abd14 100644 --- a/src/user/digest.js +++ b/src/user/digest.js @@ -125,21 +125,17 @@ Digest.send = async function (data) { emailsSent += 1; const now = new Date(); - try { - await emailer.send('digest', userObj.uid, { - subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', - username: userObj.username, - userslug: userObj.userslug, - notifications: unreadNotifs, - recent: recentTopics, - topTopics: topTopics, - popularTopics: popularTopics, - interval: data.interval, - showUnsubscribe: true, - }); - } catch (err) { - winston.error('[user/jobs] Could not send digest email\n' + err.stack); - } + await emailer.send('digest', userObj.uid, { + subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', + username: userObj.username, + userslug: userObj.userslug, + notifications: unreadNotifs, + recent: recentTopics, + topTopics: topTopics, + popularTopics: popularTopics, + interval: data.interval, + showUnsubscribe: true, + }).catch(err => winston.error('[user/jobs] Could not send digest email\n[emailer.send] ' + err.stack)); if (data.interval !== 'alltime') { await db.sortedSetAdd('digest:delivery', now.getTime(), userObj.uid); diff --git a/src/user/invite.js b/src/user/invite.js index 4c20eab70a..8085ae4ece 100644 --- a/src/user/invite.js +++ b/src/user/invite.js @@ -49,7 +49,6 @@ module.exports = function (User) { } const data = await prepareInvitation(uid, email, groupsToJoin); - await emailer.sendToEmail('invitation', email, meta.config.defaultLang, data); }; diff --git a/src/user/profile.js b/src/user/profile.js index 2a571d4cfc..fee66f7fe2 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -3,6 +3,7 @@ const async = require('async'); const validator = require('validator'); +const winston = require('winston'); const utils = require('../utils'); const slugify = require('../slugify'); @@ -246,7 +247,7 @@ module.exports = function (User) { email: newEmail, subject: '[[email:email.verify-your-email.subject]]', template: 'verify_email', - }); + }).catch(err => winston.error('[user.create] Validation email failed to send\n[emailer.send] ' + err.stack)); } } diff --git a/src/user/reset.js b/src/user/reset.js index eabf826c5a..24a3869438 100644 --- a/src/user/reset.js +++ b/src/user/reset.js @@ -55,7 +55,7 @@ UserReset.send = async function (email) { subject: '[[email:password-reset-requested]]', template: 'reset', uid: uid, - }); + }).catch(err => winston.error('[emailer.send] ' + err.stack)); }; UserReset.commit = async function (code, password) { diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index 7192135e87..160c9ae69e 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -18,11 +18,11 @@ {{{ if user.privileges.admin:categories }}}
  • [[admin/menu:manage/categories]]
  • {{{ end }}} {{{ if user.privileges.admin:privileges }}}
  • [[admin/menu:manage/privileges]]
  • {{{ end }}} {{{ if user.privileges.admin:users }}}
  • [[admin/menu:manage/users]]
  • {{{ end }}} + {{{ if user.privileges.admin:groups }}}
  • [[admin/menu:manage/groups]]
  • {{{ end }}} + {{{ if user.privileges.admin:admins-mods }}}
  • [[admin/menu:manage/admins-mods]]
  • {{{ end }}} + {{{ if user.privileges.admin:tags }}}
  • [[admin/menu:manage/tags]]
  • {{{ end }}} {{{ if user.privileges.superadmin }}} -
  • [[admin/menu:manage/groups]]
  • -
  • [[admin/menu:manage/admins-mods]]
  • [[admin/menu:manage/registration]]
  • -
  • [[admin/menu:manage/tags]]
  • [[admin/menu:manage/uploads]]
  • [[admin/menu:manage/digest]]
  • @@ -189,11 +189,11 @@ {{{ if user.privileges.admin:categories }}}
  • [[admin/menu:manage/categories]]
  • {{{ end }}} {{{ if user.privileges.admin:privileges }}}
  • [[admin/menu:manage/privileges]]
  • {{{ end }}} {{{ if user.privileges.admin:users }}}
  • [[admin/menu:manage/users]]
  • {{{ end }}} + {{{ if user.privileges.admin:groups }}}
  • [[admin/menu:manage/groups]]
  • {{{ end }}} + {{{ if user.privileges.admin:admins-mods }}}
  • [[admin/menu:manage/admins-mods]]
  • {{{ end }}} + {{{ if user.privileges.admin:tags }}}
  • [[admin/menu:manage/tags]]
  • {{{ end }}} {{{ if user.privileges.superadmin }}} -
  • [[admin/menu:manage/groups]]
  • -
  • [[admin/menu:manage/admins-mods]]
  • [[admin/menu:manage/registration]]
  • -
  • [[admin/menu:manage/tags]]
  • [[admin/menu:manage/uploads]]
  • [[admin/menu:manage/digest]]
  • diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl index 3e54b08e1f..6b856526ef 100644 --- a/src/views/admin/settings/email.tpl +++ b/src/views/admin/settings/email.tpl @@ -121,7 +121,7 @@ diff --git a/src/views/modals/topic-thumbs.tpl b/src/views/modals/topic-thumbs.tpl index 7ee9773836..814ea86718 100644 --- a/src/views/modals/topic-thumbs.tpl +++ b/src/views/modals/topic-thumbs.tpl @@ -5,7 +5,7 @@ {{{ each thumbs }}}
    - +

    diff --git a/test/api.js b/test/api.js index 1f43e67e13..e4c28cfcef 100644 --- a/test/api.js +++ b/test/api.js @@ -1,11 +1,13 @@ 'use strict'; +const _ = require('lodash'); const assert = require('assert'); const path = require('path'); const fs = require('fs'); const SwaggerParser = require('@apidevtools/swagger-parser'); const request = require('request-promise-native'); const nconf = require('nconf'); +const jwt = require('jsonwebtoken'); const util = require('util'); const wait = util.promisify(setTimeout); @@ -33,7 +35,18 @@ describe('API', async () => { const mocks = { head: {}, - get: {}, + get: { + '/api/email/unsubscribe/{token}': [ + { + in: 'path', + name: 'token', + example: (() => jwt.sign({ + template: 'digest', + uid: 1, + }, nconf.get('secret')))(), + }, + ], + }, post: {}, put: {}, delete: { @@ -67,9 +80,13 @@ describe('API', async () => { async function dummySearchHook(data) { return [1]; } + async function dummyEmailerHook(data) { + // pretend to handle sending emails + } after(async function () { plugins.unregisterHook('core', 'filter:search.query', dummySearchHook); + plugins.unregisterHook('emailer-test', 'filter:email.send'); }); async function setupData() { @@ -100,6 +117,7 @@ describe('API', async () => { }], }); meta.config.allowTopicsThumbnail = 1; + meta.config.termsOfUse = 'I, for one, welcome our new test-driven overlords'; // Create a category const testCategory = await categories.create({ name: 'test' }); @@ -143,6 +161,11 @@ describe('API', async () => { hook: 'filter:search.query', method: dummySearchHook, }); + // Attach an emailer hook so related requests do not error + plugins.registerHook('emailer-test', { + hook: 'filter:email.send', + method: dummyEmailerHook, + }); jar = await helpers.loginUser('admin', '123456'); @@ -169,6 +192,68 @@ describe('API', async () => { readApi = await SwaggerParser.dereference(readApiPath); writeApi = await SwaggerParser.dereference(writeApiPath); + it('should grab all mounted routes and ensure a schema exists', async () => { + const webserver = require('../src/webserver'); + const buildPaths = function (stack, prefix) { + const paths = stack.map((dispatch) => { + if (dispatch.route && dispatch.route.path && typeof dispatch.route.path === 'string') { + if (!prefix && !dispatch.route.path.startsWith('/api/')) { + return null; + } + + if (prefix === nconf.get('relative_path')) { + prefix = ''; + } + + return { + method: Object.keys(dispatch.route.methods)[0], + path: (prefix || '') + dispatch.route.path, + }; + } else if (dispatch.name === 'router') { + const prefix = dispatch.regexp.toString().replace('/^', '').replace('\\/?(?=\\/|$)/i', '').replace(/\\\//g, '/'); + return buildPaths(dispatch.handle.stack, prefix); + } + + // Drop any that aren't actual routes (middlewares, error handlers, etc.) + return null; + }); + + return _.flatten(paths); + }; + + let paths = buildPaths(webserver.app._router.stack).filter(Boolean).map(function normalize(pathObj) { + pathObj.path = pathObj.path.replace(/\/:([^\\/]+)/g, '/{$1}'); + return pathObj; + }); + const exclusionPrefixes = ['/api/admin/plugins', '/api/compose', '/debug']; + paths = paths.filter(function filterExclusions(path) { + return path.method !== '_all' && !exclusionPrefixes.some(prefix => path.path.startsWith(prefix)); + }); + + + // For each express path, query for existence in read and write api schemas + paths.forEach((pathObj) => { + describe(`${pathObj.method.toUpperCase()} ${pathObj.path}`, () => { + it('should be defined in schema docs', () => { + let schema = readApi; + if (pathObj.path.startsWith('/api/v3')) { + schema = writeApi; + pathObj.path = pathObj.path.replace('/api/v3', ''); + } + + // Don't check non-GET routes in Read API + if (schema === readApi && pathObj.method !== 'get') { + return; + } + + const normalizedPath = pathObj.path.replace(/\/:([^\\/]+)/g, '/{$1}').replace(/\?/g, ''); + assert(schema.paths.hasOwnProperty(normalizedPath), `${pathObj.path} is not defined in schema docs`); + assert(schema.paths[normalizedPath].hasOwnProperty(pathObj.method), `${pathObj.path} was found in schema docs, but ${pathObj.method.toUpperCase()} method is not defined`); + }); + }); + }); + }); + generateTests(readApi, Object.keys(readApi.paths)); generateTests(writeApi, Object.keys(writeApi.paths), writeApi.servers[0].url); @@ -185,6 +270,7 @@ describe('API', async () => { const qs = {}; Object.keys(context).forEach((_method) => { + // Only test GET routes in the Read API if (api.info.title === 'NodeBB Read API' && _method !== 'get') { return; } @@ -195,8 +281,9 @@ describe('API', async () => { return; } - const names = (path.match(/{[\w\-_]+}?/g) || []).map(match => match.slice(1, -1)); - assert(context[method].parameters.map(param => (param.in === 'path' ? param.name : null)).filter(Boolean).every(name => names.includes(name)), `${method.toUpperCase()} ${path} has parameter(s) in path that are not defined in schema`); + const pathParams = (path.match(/{[\w\-_*]+}?/g) || []).map(match => match.slice(1, -1)); + const schemaParams = context[method].parameters.map(param => (param.in === 'path' ? param.name : null)).filter(Boolean); + assert(pathParams.every(param => schemaParams.includes(param)), `${method.toUpperCase()} ${path} has path parameters specified but not defined`); }); it('should have examples when parameters are present', () => { @@ -229,17 +316,18 @@ describe('API', async () => { it('should contain a valid request body (if present) with application/json or multipart/form-data type if POST/PUT/DELETE', () => { if (['post', 'put', 'delete'].includes(method) && context[method].hasOwnProperty('requestBody')) { - assert(context[method].requestBody); - assert(context[method].requestBody.content); + const failMessage = `${method.toUpperCase()} ${path} has a malformed request body`; + assert(context[method].requestBody, failMessage); + assert(context[method].requestBody.content, failMessage); if (context[method].requestBody.content.hasOwnProperty('application/json')) { - assert(context[method].requestBody.content['application/json']); - assert(context[method].requestBody.content['application/json'].schema); - assert(context[method].requestBody.content['application/json'].schema.properties); + assert(context[method].requestBody.content['application/json'], failMessage); + assert(context[method].requestBody.content['application/json'].schema, failMessage); + assert(context[method].requestBody.content['application/json'].schema.properties, failMessage); } else if (context[method].requestBody.content.hasOwnProperty('multipart/form-data')) { - assert(context[method].requestBody.content['multipart/form-data']); - assert(context[method].requestBody.content['multipart/form-data'].schema); - assert(context[method].requestBody.content['multipart/form-data'].schema.properties); + assert(context[method].requestBody.content['multipart/form-data'], failMessage); + assert(context[method].requestBody.content['multipart/form-data'].schema, failMessage); + assert(context[method].requestBody.content['multipart/form-data'].schema.properties, failMessage); } } }); @@ -266,17 +354,20 @@ describe('API', async () => { method: method, jar: !unauthenticatedRoutes.includes(path) ? jar : undefined, json: true, + followRedirect: false, // all responses are significant (e.g. 302) + simple: false, // don't throw on non-200 (e.g. 302) + resolveWithFullResponse: true, // send full request back (to check statusCode) headers: headers, qs: qs, body: body, }); } else if (type === 'form') { response = await new Promise((resolve, reject) => { - helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken, function (err, res, body) { + helpers.uploadFile(url, pathLib.join(__dirname, './files/test.png'), {}, jar, csrfToken, function (err, res) { if (err) { return reject(err); } - resolve(body); + resolve(res); }); }); } @@ -285,17 +376,40 @@ describe('API', async () => { } }); + it('response status code should match one of the schema defined responses', () => { + // HACK: allow HTTP 418 I am a teapot, for now 👇 + assert(context[method].responses.hasOwnProperty('418') || Object.keys(context[method].responses).includes(String(response.statusCode)), `${method.toUpperCase()} ${path} sent back unexpected HTTP status code: ${response.statusCode}`); + }); + // Recursively iterate through schema properties, comparing type - it('response should match schema definition', () => { - const has200 = context[method].responses['200']; - if (!has200) { + it('response body should match schema definition', () => { + const http302 = context[method].responses['302']; + if (http302 && response.statusCode === 302) { + // Compare headers instead + const expectedHeaders = Object.keys(http302.headers).reduce((memo, name) => { + const value = http302.headers[name].schema.example; + memo[name] = value.startsWith(nconf.get('relative_path')) ? value : nconf.get('relative_path') + value; + return memo; + }, {}); + + for (const header in expectedHeaders) { + if (expectedHeaders.hasOwnProperty(header)) { + assert(response.headers[header.toLowerCase()]); + assert.strictEqual(response.headers[header.toLowerCase()], expectedHeaders[header]); + } + } return; } - const hasJSON = has200.content && has200.content['application/json']; + const http200 = context[method].responses['200']; + if (!http200) { + return; + } + + const hasJSON = http200.content && http200.content['application/json']; if (hasJSON) { schema = context[method].responses['200'].content['application/json'].schema; - compare(schema, response, method.toUpperCase(), path, 'root'); + compare(schema, response.body, method.toUpperCase(), path, 'root'); } // TODO someday: text/csv, binary file type checking? @@ -332,12 +446,20 @@ describe('API', async () => { let required = []; const additionalProperties = schema.hasOwnProperty('additionalProperties'); - if (schema.allOf) { - schema = schema.allOf.reduce((memo, obj) => { - required = required.concat(obj.required ? obj.required : Object.keys(obj.properties)); - memo = { ...memo, ...obj.properties }; - return memo; + function flattenAllOf(obj) { + return obj.reduce((memo, obj) => { + if (obj.allOf) { + obj = { properties: flattenAllOf(obj.allOf) }; + } else { + required = required.concat(obj.required ? obj.required : Object.keys(obj.properties)); + } + + return { ...memo, ...obj.properties }; }, {}); + } + + if (schema.allOf) { + schema = flattenAllOf(schema.allOf); } else if (schema.properties) { required = schema.required || Object.keys(schema.properties); schema = schema.properties; diff --git a/test/database/sorted.js b/test/database/sorted.js index c63c653433..2670e7e791 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -334,6 +334,7 @@ describe('Sorted Set methods', function () { }); it('should work with big arrays (length > 100) ', async function () { + this.timeout(50000); const keys = []; for (let i = 0; i < 400; i++) { /* eslint-disable no-await-in-loop */ diff --git a/test/groups.js b/test/groups.js index 643eff6ee0..3543499b97 100644 --- a/test/groups.js +++ b/test/groups.js @@ -1189,7 +1189,7 @@ describe('Groups', function () { }, }; socketGroups.update({ uid: adminUid }, data, function (err) { - assert.equal(err.message, '[[error:no-group]]'); + assert.equal(err.message, '[[error:invalid-group-name]]'); done(); }); }); diff --git a/test/messaging.js b/test/messaging.js index ae6f3f164c..3ebe798a98 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -597,12 +597,15 @@ describe('Messaging Library', function () { describe('edit/delete', function () { var socketModules = require('../src/socket.io/modules'); var mid; - before(function (done) { - socketModules.chats.send({ uid: fooUid }, { roomId: roomId, message: 'first chat message' }, function (err, messageData) { - assert.ifError(err); - mid = messageData.mid; - done(); - }); + let mid2; + before(async function () { + await socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'baz' }); + mid = (await socketModules.chats.send({ uid: fooUid }, { roomId: roomId, message: 'first chat message' })).mid; + mid2 = (await socketModules.chats.send({ uid: bazUid }, { roomId: roomId, message: 'second chat message' })).mid; + }); + + after(async () => { + await socketModules.chats.leave({ uid: bazUid }, roomId); }); it('should fail to edit message with invalid data', function (done) { @@ -723,6 +726,38 @@ describe('Messaging Library', function () { done(); }); }); + + describe('disabled via ACP', () => { + before(async () => { + meta.config.disableChatMessageEditing = true; + }); + + after(async () => { + meta.config.disableChatMessageEditing = false; + }); + + it('should error out for regular users', async () => { + try { + await socketModules.chats.delete({ uid: bazUid }, { messageId: mid2, roomId: roomId }); + } catch (err) { + assert.strictEqual('[[error:chat-message-editing-disabled]]', err.message); + } + }); + + it('should succeed for administrators', async () => { + await socketModules.chats.delete({ uid: fooUid }, { messageId: mid2, roomId: roomId }); + await socketModules.chats.restore({ uid: fooUid }, { messageId: mid2, roomId: roomId }); + }); + + it('should succeed for global moderators', async () => { + await Groups.join(['Global Moderators'], bazUid); + + await socketModules.chats.delete({ uid: fooUid }, { messageId: mid2, roomId: roomId }); + await socketModules.chats.restore({ uid: fooUid }, { messageId: mid2, roomId: roomId }); + + await Groups.leave(['Global Moderators'], bazUid); + }); + }); }); describe('controller', function () { diff --git a/test/socket.io.js b/test/socket.io.js index f041037d1d..8a4e7fa432 100644 --- a/test/socket.io.js +++ b/test/socket.io.js @@ -240,6 +240,21 @@ describe('socket.io', function () { describe('validation emails', function () { var meta = require('../src/meta'); + var plugins = require('../src/plugins'); + + async function dummyEmailerHook(data) { + // pretend to handle sending emails + } + before(function () { + // Attach an emailer hook so related requests do not error + plugins.registerHook('emailer-test', { + hook: 'filter:email.send', + method: dummyEmailerHook, + }); + }); + after(function () { + plugins.unregisterHook('emailer-test', 'filter:email.send'); + }); it('should validate emails', function (done) { socketAdmin.user.validateEmail({ uid: adminUid }, [regularUid], function (err) { diff --git a/test/user.js b/test/user.js index 253657afa2..405ba3adc0 100644 --- a/test/user.js +++ b/test/user.js @@ -25,7 +25,18 @@ describe('User', function () { var testUid; var testCid; + var plugins = require('../src/plugins'); + + async function dummyEmailerHook(data) { + // pretend to handle sending emails + } before(function (done) { + // Attach an emailer hook so related requests do not error + plugins.registerHook('emailer-test', { + hook: 'filter:email.send', + method: dummyEmailerHook, + }); + Categories.create({ name: 'Test Category', description: 'A test', @@ -39,6 +50,9 @@ describe('User', function () { done(); }); }); + after(function () { + plugins.unregisterHook('emailer-test', 'filter:email.send'); + }); beforeEach(function () { userData = {