diff --git a/CHANGELOG.md b/CHANGELOG.md index f59f675ee1..3be6f7a448 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,74 @@ +#### 1.13.1 (2019-12-19) + +##### Chores + +* incrementing version number - v1.13.1 (d1e0672f) +* incrementing version number - v1.13.0 (c38b2d23) +* **deps:** + * update dependency husky to v3.1.0 (#8046) (c3418c26) + * update dependency coveralls to v3.0.9 (#8067) (0aeee144) + * update dependency eslint to v6.7.0 (32cfe96f) + * update dependency coveralls to v3.0.8 (#8054) (8ba26104) + +##### Documentation Changes + +* updated changelog (94499da3) + +##### New Features + +* better output for cli plugins list, closes #8075 (4fc69443) +* #5272, allow changing user groups from manage users page (05c9fe27) +* merge social authentication into plugins menu in ACP (f9a8ebfc) +* convert middleware.isAdmin to async/await (efd1e88b) + +##### Bug Fixes + +* #8085, fix cookie name (dec157d6) +* #8058, fix incorrect digest setting display in ACP (1b992d82) +* remove select version (6a17e32d) +* travis config (3ae98300) +* travis :dog: (3731dc4e) +* #8078, dont mark notifications read without a mergeId (a8df6d62) +* #8077, show continue chat on all profile pages (7af1c873) +* profile showing posts from deleted topics (2679f37d) +* #8073, configurable necroThreshold (4d669783) +* allow members to search as well (b323df2f) +* #8069, dont show hidden groups in search (c2cd7de8) +* missing await (33fd4a1c) +* #8064, break-word on post-queue (1bda92e3) +* #6711 (7ed002a1) +* #8061, don't crash if there is a network problem (de404102) +* #8059, properly mark topic unread when using mark unread for all (a688aaae) +* #8042, dont show errors after clearing form (3811e0a3) +* unhandled promise rejection error on reset error (51073772) +* #8050, fix redirect after registration (366ad5cd) +* make _csrf a secure cookie if the website is using https (#8045) (0efe27b1) +* #8034 (0a96c923) +* serialize (a2545204) +* show login fields if user has local password (1eca5b3d) +* use the correct attribute name for widgets (6c404b81) +* **deps:** + * update dependency semver to v7 (483d7535) + * update dependency nodebb-theme-vanilla to v11.1.12 (610ecf35) + * update dependency sharp to v0.23.4 (#8076) (eb18c182) + * update dependency nodebb-theme-persona to v10.1.30 (0514383a) + * update dependency nodebb-plugin-markdown to v8.11.0 (702ca164) + * update dependency connect-mongo to v3.2.0 (2aef7a5b) + * update dependency mongodb to v3.3.5 (#8065) (68118e43) + * update dependency nodebb-theme-persona to v10.1.29 (#8057) (34933091) + * update dependency sharp to v0.23.3 (#8044) (6fa88823) + * update dependency validator to v12.1.0 (#8055) (488ea394) + * update dependency nodebb-theme-slick to v1.2.28 (#8041) (b3511f71) + * update dependency nodebb-theme-vanilla to v11.1.11 (#8040) (d567c4ae) + * update dependency nodebb-theme-persona to v10.1.28 (#8039) (6c87bed5) + * update dependency nodebb-plugin-dbsearch to v4.0.7 (#8038) (1e2e16b4) + +##### Refactors + +* async/await middleware (a227cbe3) +* change to const/let (3454a24b) +* shorter returns (cec00795) + ### 1.13.0 (2019-11-13) ##### Chores diff --git a/install/package.json b/install/package.json index 37448bb06c..f6507c2f2f 100644 --- a/install/package.json +++ b/install/package.json @@ -49,7 +49,7 @@ "connect-mongo": "3.2.0", "connect-multiparty": "^2.1.0", "connect-pg-simple": "^6.0.0", - "connect-redis": "4.0.3", + "connect-redis": "4.0.4", "cookie-parser": "^1.4.3", "cron": "^1.3.0", "cropperjs": "^1.2.2", @@ -73,43 +73,43 @@ "lru-cache": "5.1.1", "material-design-lite": "^1.3.0", "mime": "^2.2.0", - "mkdirp": "^0.5.1", - "mongodb": "3.4.0", + "mkdirp": "^1.0.3", + "mongodb": "3.5.2", "morgan": "^1.9.1", "mousetrap": "^1.6.1", "mubsub-nbb": "^1.5.1", "nconf": "^0.10.0", - "nodebb-plugin-composer-default": "6.3.20", + "nodebb-plugin-composer-default": "6.3.21", "nodebb-plugin-dbsearch": "4.0.7", "nodebb-plugin-emoji": "^3.0.0", "nodebb-plugin-emoji-android": "2.0.0", "nodebb-plugin-markdown": "8.11.0", - "nodebb-plugin-mentions": "2.7.3", + "nodebb-plugin-mentions": "2.7.4", "nodebb-plugin-soundpack-default": "1.0.0", "nodebb-plugin-spam-be-gone": "0.6.7", "nodebb-rewards-essentials": "0.1.2", "nodebb-theme-lavender": "5.0.11", - "nodebb-theme-persona": "10.1.30", + "nodebb-theme-persona": "10.1.34", "nodebb-theme-slick": "1.2.28", - "nodebb-theme-vanilla": "11.1.12", - "nodebb-widget-essentials": "4.0.17", + "nodebb-theme-vanilla": "11.1.15", + "nodebb-widget-essentials": "4.0.18", "nodemailer": "^6.0.0", "passport": "^0.4.0", "passport-local": "1.0.0", "pg": "^7.4.0", "pg-cursor": "^2.0.0", - "postcss": "7.0.21", + "postcss": "7.0.26", "postcss-clean": "1.1.0", "promise-polyfill": "^8.0.0", "prompt": "^1.0.0", "redis": "2.8.0", "request": "2.88.0", - "rimraf": "3.0.0", + "rimraf": "3.0.1", "rss": "^1.2.2", "sanitize-html": "^1.16.3", "semver": "^7.0.0", "serve-favicon": "^2.4.5", - "sharp": "0.23.4", + "sharp": "0.24.0", "sitemap": "^5.0.0", "socket.io": "2.3.0", "socket.io-adapter-cluster": "^1.0.1", @@ -124,27 +124,27 @@ "textcomplete.contenteditable": "^0.1.1", "toobusy-js": "^0.5.1", "uglify-es": "^3.3.9", - "validator": "12.1.0", + "validator": "12.2.0", "winston": "3.2.1", "xml": "^1.0.1", "xregexp": "^4.1.1", "zxcvbn": "^4.4.2" }, "devDependencies": { - "@commitlint/cli": "8.2.0", - "@commitlint/config-angular": "8.2.0", + "@commitlint/cli": "8.3.5", + "@commitlint/config-angular": "8.3.4", "coveralls": "3.0.9", - "eslint": "6.7.0", + "eslint": "6.8.0", "eslint-config-airbnb-base": "14.0.0", "eslint-plugin-import": "2.18.2", "grunt": "1.0.4", "grunt-contrib-watch": "1.1.0", - "husky": "3.1.0", + "husky": "4.2.1", "jsdom": "15.2.1", - "lint-staged": "9.4.2", - "mocha": "6.2.2", + "lint-staged": "10.0.7", + "mocha": "7.0.1", "mocha-lcov-reporter": "1.3.0", - "nyc": "14.1.1", + "nyc": "15.0.0", "smtp-server": "3.5.0" }, "bugs": { @@ -170,4 +170,4 @@ "url": "https://github.com/barisusakli" } ] -} \ No newline at end of file +} diff --git a/public/language/de/admin/menu.json b/public/language/de/admin/menu.json index 3772752480..3f80014f20 100644 --- a/public/language/de/admin/menu.json +++ b/public/language/de/admin/menu.json @@ -18,7 +18,7 @@ "manage/groups": "Gruppen", "manage/ip-blacklist": "IP Blacklist", "manage/uploads": "Uploads", - "manage/digest": "Digests", + "manage/digest": "Zusammenfassungen", "section-settings": "Einstellungen", "settings/general": "Allgemein", diff --git a/public/language/de/admin/settings/email.json b/public/language/de/admin/settings/email.json index 69e54c409f..a780354a76 100644 --- a/public/language/de/admin/settings/email.json +++ b/public/language/de/admin/settings/email.json @@ -33,8 +33,8 @@ "testing.select": "Wählen Sie die E-Mail Vorlage", "testing.send": "Test-E-Mail versenden", "testing.send-help": "Die Test-E-Mail wird an die E-Mail Adresse des momentan eingeloggten Nutzers geschickt.", - "subscriptions": "Email Digests", - "subscriptions.disable": "Disable email digests", + "subscriptions": "Email Zusammenfassungen", + "subscriptions.disable": "Deaktivierung der Email Zusammenfassungen", "subscriptions.hour": "Sende Zeit", "subscriptions.hour-help": "Bitte geben Sie eine Nummer ein, welche die Stunde repräsentiert zu welcher geplante Emails versandt werden sollen (z.B. 0 für Mitternacht, 17 für 5 Uhr Nachmittags). Beachten Sie, dass die Zeit auf der Serverzeit basiert und daher nicht umbedingt mit ihrer Systemzeit übereinstimmen muss.
Die ungefähre Serverzeit ist:
Die nächste tägliche Sendung ist um geplant" } \ No newline at end of file diff --git a/public/language/de/email.json b/public/language/de/email.json index 4cf29674cf..efafcf791b 100644 --- a/public/language/de/email.json +++ b/public/language/de/email.json @@ -27,9 +27,9 @@ "digest.week": "der letzten Woche", "digest.month": "des letzen Monats", "digest.subject": "Zusammenfassung für %1", - "digest.title.day": "Your Daily Digest", - "digest.title.week": "Your Weekly Digest", - "digest.title.month": "Your Monthly Digest", + "digest.title.day": "Deine tägliche Zusammenfassung", + "digest.title.week": "Deine wöchentliche Zusammenfassung", + "digest.title.month": "Deine monatliche Zusammenfassung", "notif.chat.subject": "Neue Chatnachricht von %1 erhalten", "notif.chat.cta": "Klicke hier, um die Unterhaltung fortzusetzen", "notif.chat.unsub.info": "Diese Chat-Benachrichtigung wurde dir aufgrund deiner Abonnement-Einstellungen gesendet.", diff --git a/public/language/de/topic.json b/public/language/de/topic.json index bd0196ade0..7b651fd2d8 100644 --- a/public/language/de/topic.json +++ b/public/language/de/topic.json @@ -134,6 +134,6 @@ "diffs.no-revisions-description": "Dieser Beitrag ha %1 Revisionen.", "diffs.current-revision": "Aktuelle Revision", "diffs.original-revision": "Ursprüngliche Revision", - "timeago_later": "%1 later", + "timeago_later": "%1 später", "timeago_earlier": "%1 earlier" } \ No newline at end of file diff --git a/public/language/he/admin/advanced/events.json b/public/language/he/admin/advanced/events.json index aae407a7ee..0d01c47b2c 100644 --- a/public/language/he/admin/advanced/events.json +++ b/public/language/he/admin/advanced/events.json @@ -1,11 +1,11 @@ { "events": "ארועים", "no-events": "אין ארועים", - "control-panel": "בקרת ארועים", - "filters": "Filters", - "filters-apply": "Apply Filters", - "filter-type": "Event Type", - "filter-start": "Start Date", - "filter-end": "End Date", - "filter-perPage": "Per Page" + "control-panel": "בקרת ארועים\n ", + "filters": "מסננים", + "filters-apply": "החל מסננים", + "filter-type": "סוג אירוע", + "filter-start": "מתאריך", + "filter-end": "עד תאריך", + "filter-perPage": "פריטים בכל דף" } \ No newline at end of file diff --git a/public/language/he/topic.json b/public/language/he/topic.json index 39f2d0bc80..c8d9acbe8d 100644 --- a/public/language/he/topic.json +++ b/public/language/he/topic.json @@ -18,13 +18,13 @@ "last_reply_time": "תגובה אחרונה", "reply-as-topic": "הגב כנושא", "guest-login-reply": "התחבר כדי לפרסם תגובה", - "login-to-view": "🔒 Log in to view", + "login-to-view": "🔒 התחבר כדי לצפות", "edit": "עריכה", "delete": "מחק", "purge": "מחק לצמיתות", "restore": "שחזר", "move": "הזז", - "change-owner": "Change Owner", + "change-owner": "שנה מחבר הודעה", "fork": "פורק", "link": "לינק", "share": "שתף", @@ -56,7 +56,7 @@ "ignoring": "מתעלם", "watching.description": "הודע לי על תגובות חדשות.
הצג נושא חדש ברשימת הלא נקראו.", "not-watching.description": "אל תיידע אותי על תגובות חדשות.
הצג נושא חדש ברשימת הלא נקראו במידה ובחרתי לא להתעלם מקבוצת הדיון", - "ignoring.description": "Do not notify me of new replies.
Do not show topic in unread.", + "ignoring.description": "אל תתריע לי על תגובות חדשות.
אל תראה את הנושא בנושאים שלא נקראו ", "thread_tools.title": "כלי נושא", "thread_tools.markAsUnreadForAll": "סמן לא נקרא לכולם", "thread_tools.pin": "נעץ נושא", @@ -66,7 +66,7 @@ "thread_tools.move": "הזז נושא", "thread_tools.move-posts": "הזז פוסטים", "thread_tools.move_all": "הזז הכל", - "thread_tools.change_owner": "Change Owner", + "thread_tools.change_owner": "שנה את כותב ההודעה", "thread_tools.select_category": "בחר קטגוריה", "thread_tools.fork": "שכפל נושא", "thread_tools.delete": "מחק נושא", @@ -101,7 +101,7 @@ "delete_posts_instruction": "לחץ על הפוסטים שברצונך למחוק", "merge_topics_instruction": "לחץ על הנושאים שתרצה למזג", "move_posts_instruction": "לחץ על הפוסטים שאתה רוצה להזיז", - "change_owner_instruction": "Click the posts you want to assign to another user", + "change_owner_instruction": "לחץ על ההודעה שהנך רוצה לשנות את בעל ההודעה", "composer.title_placeholder": "הכנס את כותרת הנושא כאן...", "composer.handle_placeholder": "שם", "composer.discard": "ביטול", @@ -130,10 +130,10 @@ "stale.reply_anyway": "הגב לנושא זה בכל מקרה", "link_back": "תגובה: [%1](%2)", "diffs.title": "היסטוריית עריכת הפוסט", - "diffs.description": "This post has %1 revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.description": "להודעה זו יש %1 עריכות. לחץ על אחת מהעריכות להלן כדי לראות את תוכן ההודעה בנקודת זמן זו.", "diffs.no-revisions-description": "לפוסט זה יש %1גרסאות", - "diffs.current-revision": "current revision", - "diffs.original-revision": "original revision", - "timeago_later": "%1 later", - "timeago_earlier": "%1 earlier" + "diffs.current-revision": "גירסה נוכחית", + "diffs.original-revision": "גירסה מקורית", + "timeago_later": "אחרי %1:", + "timeago_earlier": "לפני %1 " } \ No newline at end of file diff --git a/public/language/it/admin/manage/categories.json b/public/language/it/admin/manage/categories.json index 606fde9118..9a0407d8f2 100644 --- a/public/language/it/admin/manage/categories.json +++ b/public/language/it/admin/manage/categories.json @@ -30,16 +30,16 @@ "select-category": "Seleziona Categoria", "set-parent-category": "Imposta la Categoria Padre", - "privileges.description": "You can configure the access control privileges for portions of the site in this section. Privileges can be granted on a per-user or a per-group basis. Select the domain of effect from the dropdown below.", + "privileges.description": "In questa sezione è possibile configurare i privilegi di controllo dell'accesso per parti del sito. I privilegi possono essere concessi per utente o per gruppo. Seleziona il dominio dell'effetto dal menu a discesa in basso.", "privileges.category-selector": "Configura privilegi per", - "privileges.warning": "Note: Privilege settings take effect immediately. It is not necessary to save the category after adjusting these settings.", + "privileges.warning": "Nota: Le impostazioni dei privilegi hanno effetto immediato. Non è necessario salvare la categoria dopo aver regolato queste impostazioni.", "privileges.section-viewing": "Visualizzazione dei Privilegi", - "privileges.section-posting": "Posting Privileges", - "privileges.section-moderation": "Moderation Privileges", + "privileges.section-posting": "Privilegi di pubblicazione", + "privileges.section-moderation": "Privilegi di Moderazione", "privileges.section-other": "Altro", "privileges.section-user": "Utente", "privileges.search-user": "Aggiungi Utente", - "privileges.no-users": "No user-specific privileges in this category.", + "privileges.no-users": "Nessun privilegio specifico dell'utente in questa categoria.", "privileges.section-group": "Gruppo", "privileges.group-private": "Questo gruppo è privato", "privileges.search-group": "Aggiungi gruppo", @@ -49,27 +49,27 @@ "privileges.copy-group-privileges-to-children": "Copia i privilegi di questo gruppo dai figli di questa categoria.", "privileges.copy-group-privileges-to-all-categories": "Copia i privilegi di questo gruppo in tutte le categorie.", "privileges.copy-group-privileges-from": "Copia questo gruppo di privilegi da un altra categoria.", - "privileges.inherit": "If the registered-users group is granted a specific privilege, all other groups receive an implicit privilege, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the registered-users user group, and so, privileges for additional groups need not be explicitly granted.", + "privileges.inherit": "Se l' utente registrato al gruppo viene concesso un privilegio specifico, tutti gli altri gruppi ricevono unprivilegio implicito,anche se non sono esplicitamente definiti / controllati. Questo privilegio implicito ti viene mostrato perché tutti gli utenti fanno parte digruppo di utenti registrati e quindi i privilegi per gruppi aggiuntivi non devono essere esplicitamente concessi.", "privileges.copy-success": "Privilegi copiati!", - "analytics.back": "Back to Categories List", + "analytics.back": "Torna all'Elenco delle Categorie", "analytics.title": "Statistiche per la categoria \"%1\"", "analytics.pageviews-hourly": "Figura 1 – Vista delle visualizzazioni orarie per questa categoria", "analytics.pageviews-daily": "Figura 2 – Vista delle visualizzazioni giornaliere per questa categoria", "analytics.topics-daily": "Figura 3 – Argomenti giornalieri creati in questa categoria", - "analytics.posts-daily": "Figure 4 – Daily posts made in this category", + "analytics.posts-daily": "Figura 4dash; Post giornalieri pubblicati in questa categoria", "alert.created": "Creato", "alert.create-success": "Categoria creata con successo!", "alert.none-active": "Hai una categoria non attiva.", "alert.create": "Crea una Categoria", - "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-moderate": "Sei sicuro di voler concedere il privilegio di moderazione a questo gruppo di utenti? Questo gruppo è pubblico e tutti gli utenti possono aderire a piacimento.", "alert.confirm-purge": "

Vuoi davvero eliminare definitivamente questa categoria \"%1\"?

Attenzione!Tutte le discussioni e i posti in questa categoria saranno eliminati definitivamente!

Eliminare definitivamente una categoria rimuoverà tutte le discussioni e i post ed eliminerà la categoria dal database. Se vuoi rimuovere una categoria temporaneamente, puoi invece \"disabilitare\" la categoria.", "alert.purge-success": "Categoria eliminata definitivamente!", - "alert.copy-success": "Settings Copied!", - "alert.set-parent-category": "Set Parent Category", - "alert.updated": "Updated Categories", - "alert.updated-success": "Category IDs %1 successfully updated.", + "alert.copy-success": "Impostazioni copiate!", + "alert.set-parent-category": "Imposta la Categoria Genitore", + "alert.updated": "Categorie aggiornate", + "alert.updated-success": "ID categoria %1 aggiornati correttamente.", "alert.upload-image": "Carica immagine categoria", "alert.find-user": "Trova un Utente", "alert.user-search": "Cerca un utente qui...", diff --git a/public/language/it/admin/manage/digest.json b/public/language/it/admin/manage/digest.json index 075b8e1aff..fc02b3725b 100644 --- a/public/language/it/admin/manage/digest.json +++ b/public/language/it/admin/manage/digest.json @@ -1,21 +1,21 @@ { - "lead": "A listing of digest delivery stats and times is displayed below.", - "disclaimer": "Please be advised that email delivery is not guaranteed, due to the nature of email technology. Many variables factor into whether an email sent to the recipient server is ultimately delivered into the user's inbox, including server reputation, blacklisted IP addresses, and whether DKIM/SPF/DMARC is configured.", - "disclaimer-continued": "A successful delivery means the message was sent successfully by NodeBB and acknowledged by the recipient server. It does not mean the email landed in the inbox. For best results, we recommend using a third-party email delivery service such as SendGrid.", + "lead": "Di seguito viene visualizzato un elenco di statistiche e tempi di consegna del digest.", + "disclaimer": "Si informa che la consegna della posta non è garantita a causa della natura della tecnologia di posta elettronica. Molte variabili determinano se una e-mail inviata al server del destinatario viene infine recapitata nella posta in arrivo dell'utente, inclusa la reputazione del server, gli indirizzi IP nella lista nera e se DKIM/SPF/DMARC è configurato.", + "disclaimer-continued": "Una consegna corretta indica che il messaggio è stato inviato correttamente da NodeBB e riconosciuto dal server destinatario. Non significa che l'email è arrivata nella posta in arrivo. Per risultati ottimali, si consiglia di utilizzare un servizio di consegna e-mail di terze parti come SendGrid.", - "user": "User", - "subscription": "Subscription Type", - "last-delivery": "Last successful delivery", - "default": "System default", - "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", - "resend": "Resend Digest", - "resend-all-confirm": "Are you sure you wish to mnually execute this digest run?", - "resent-single": "Manual digest resend completed", - "resent-day": "Daily digest resent", - "resent-week": "Weekly digest resent", - "resent-month": "Monthly digest resent", - "null": "Never", - "manual-run": "Manual digest run:", + "user": "Utente", + "subscription": "Tipo di Abbonamento", + "last-delivery": "Ultima consegna riuscita", + "default": "Sistema predefinito", + "default-help": "Sistema predefinito significa che l'utente non ha esplicitamente sovrascritto l'impostazione del forum globale per i digest, che attualmente è: & quot;%1"", + "resend": "Rinvia Digest", + "resend-all-confirm": "Sei sicuro di voler eseguire manualmente questa corsa digest?", + "resent-single": "Invio del digest manuale completato", + "resent-day": "Rinvio digest giornaliero", + "resent-week": "Rinvio del digest settimanale", + "resent-month": "Rinvio del digest mensile", + "null": "Mai", + "manual-run": "Esecuzione digest manuale:", - "no-delivery-data": "No delivery data found" + "no-delivery-data": "Nessun dato di consegna trovato" } \ No newline at end of file diff --git a/public/language/it/admin/manage/users.json b/public/language/it/admin/manage/users.json index cf19a7307c..a45b72dfc1 100644 --- a/public/language/it/admin/manage/users.json +++ b/public/language/it/admin/manage/users.json @@ -15,8 +15,8 @@ "delete": "Rimuovi Utente(i)", "purge": "Rimuovi Utente(i) e Contenuto", "download-csv": "Scarica CSV", - "manage-groups": "Manage Groups", - "add-group": "Add Group", + "manage-groups": "Gestisci Gruppi", + "add-group": "Aggiungi Gruppo", "invite": "Invito", "new": "Nuovo utente", diff --git a/public/language/it/admin/settings/post.json b/public/language/it/admin/settings/post.json index 3e1d873ddf..a0c9aa3c66 100644 --- a/public/language/it/admin/settings/post.json +++ b/public/language/it/admin/settings/post.json @@ -27,33 +27,33 @@ "restrictions.max-title-length": "Lunghezza Massima Titolo", "restrictions.min-post-length": "Lunghezza Minima Post", "restrictions.max-post-length": "Lunghezza Massima Post", - "restrictions.days-until-stale": "Days until topic is considered stale", - "restrictions.stale-help": "If a topic is considered \"stale\", then a warning will be shown to users who attempt to reply to that topic.", - "timestamp": "Timestamp", - "timestamp.cut-off": "Date cut-off (in days)", - "timestamp.cut-off-help": "Dates & times will be shown in a relative manner (e.g. \"3 hours ago\" / \"5 days ago\"), and localised into various\n\t\t\t\t\tlanguages. After a certain point, this text can be switched to display the localised date itself\n\t\t\t\t\t(e.g. 5 Nov 2016 15:30).
(Default: 30, or one month). Set to 0 to always display dates, leave blank to always display relative times.", - "timestamp.necro-threshold": "Necro Threshold (in days)", - "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", - "teaser": "Teaser Post", - "teaser.last-post": "Last – Show the latest post, including the original post, if no replies", - "teaser.last-reply": "Last – Show the latest reply, or a \"No replies\" placeholder if no replies", + "restrictions.days-until-stale": "Giorni prima che l'argomento sia considerato vecchio", + "restrictions.stale-help": "Se un argomento è considerato \"non aggiornato\", verrà mostrato un avviso agli utenti che tentano di rispondere a tale argomento.", + "timestamp": "Data e Ora", + "timestamp.cut-off": "Data di interruzione (in giorni)", + "timestamp.cut-off-help": "I tempi delle date verranno visualizzati in modo relativo (ad es. \"3 ore fa\" / \"5 giorni fa\") e localizzati in varie\n\t\t\t\t\tlingue. Dopo un certo punto, questo testo può essere cambiato per visualizzare la data localizzata\n\t\t\t\t\t(es. 5 Nov 2016 15:30).
(Predefinito: 30, o un mese).Impostare su 0 per visualizzare sempre le date, lasciare vuoto per visualizzare sempre i tempi relativi.", + "timestamp.necro-threshold": "Necro Threshold (in giorni)", + "timestamp.necro-threshold-help": "Un messaggio verrà mostrato tra i post se il tempo tra loro è più lungo della soglia necro. (Predefinito: 7, o una settimana). Impostare su 0 per disabilitare.", + "teaser": "Post Inopportuno", + "teaser.last-post": "Ultimo – Mostra l'ultimo post, incluso il post originale, se non ci sono risposte", + "teaser.last-reply": "Ultimo – Mostra l'ultima risposta o un segnaposto \"Nessuna risposta\" se non risposto", "teaser.first": "Primo", - "unread": "Unread Settings", - "unread.cutoff": "Unread cutoff days", - "unread.min-track-last": "Minimum posts in topic before tracking last read", + "unread": "Impostazioni non Lette", + "unread.cutoff": "Giorni di interruzione non letti", + "unread.min-track-last": "Post minimi nell'argomento prima del monitoraggio dell'ultima lettura", "recent": "Impostazioni Recenti", - "recent.categoryFilter.disable": "Disable filtering of topics in ignored categories on the /recent page", - "signature": "Signature Settings", - "signature.disable": "Disable signatures", - "signature.no-links": "Disable links in signatures", - "signature.no-images": "Disable images in signatures", - "signature.max-length": "Maximum Signature Length", - "composer": "Composer Settings", - "composer-help": "The following settings govern the functionality and/or appearance of the post composer shown\n\t\t\t\tto users when they create new topics, or reply to existing topics.", - "composer.show-help": "Show \"Help\" tab", - "composer.enable-plugin-help": "Allow plugins to add content to the help tab", - "composer.custom-help": "Custom Help Text", - "ip-tracking": "IP Tracking", - "ip-tracking.each-post": "Track IP Address for each post", - "enable-post-history": "Enable Post History" + "recent.categoryFilter.disable": "Disabilita il filtro degli argomenti nelle categorie ignorate nella pagina/recente", + "signature": "Impostazioni della Firma", + "signature.disable": "Disabilita le firme", + "signature.no-links": "Disabilita i collegamenti nelle firme", + "signature.no-images": "Disabilita le immagini nelle firme", + "signature.max-length": "Lunghezza massima della firma", + "composer": "Impostazioni del compositore", + "composer-help": "Le seguenti impostazioni regolano la funzionalità e/o l'aspetto del post compositore mostrato\n\t\t\t\tagli utenti quando creano nuovi argomenti o rispondono ad argomenti esistenti.", + "composer.show-help": "Mostra la scheda \"Aiuto\"", + "composer.enable-plugin-help": "Consenti ai plug-in di aggiungere contenuti alla scheda Guida", + "composer.custom-help": "Testo di aiuto personalizzato", + "ip-tracking": "Monitoraggio IP", + "ip-tracking.each-post": "Traccia l'indirizzo IP per ogni post", + "enable-post-history": "Abilita Cronologia post" } \ No newline at end of file diff --git a/public/language/it/admin/settings/user.json b/public/language/it/admin/settings/user.json index 3c72014667..3c151bf968 100644 --- a/public/language/it/admin/settings/user.json +++ b/public/language/it/admin/settings/user.json @@ -41,11 +41,11 @@ "registration-type.invite-only": "Solo Invito", "registration-type.admin-invite-only": "Solo invito per Amministratori", "registration-type.disabled": "Niente registrazione", - "registration-type.help": "Normal - Users can register from the /register page.
\nInvite Only - Users can invite others from the users page.
\nAdmin Invite Only - Only administrators can invite others from users and admin/manage/users pages.
\nNo registration - No user registration.
", - "registration-approval-type.help": "Normal - Users are registered immediately.
\nAdmin Approval - User registrations are placed in an approval queue for administrators.
\nAdmin Approval for IPs - Normal for new users, Admin Approval for IP addresses that already have an account.
", + "registration-type.help": "Normale: gli utenti possono registrarsi dalla pagina/di registrazione.
\nSolo invito: gli utenti possono invitare altri dalla pagina utenti.
\nSolo su invito amministratore: solo gli amministratori possono invitare altri utenti edalle pagine amministratore/gestione/utenti.
\nNessuna registrazione - Nessuna registrazione dell'utente.
", + "registration-approval-type.help": "Normale: gli utenti vengono registrati immediatamente.
\nApprovazione amministratore - Le registrazioni degli utenti vengono inserite in una coda di approvazione per amministratori.
\nApprovazione amministratore per IP - Normale per i nuovi utenti, Approvazione amministratore per indirizzi IP che dispongono già di un account.
", "registration.max-invites": "Numero massimo di inviti per Utente", "max-invites": "Numero massimo di inviti per Utente", - "max-invites-help": "0 for no restriction. Admins get infinite invitations
Only applicable for \"Invite Only\"", + "max-invites-help": "0 per nessuna restrizione. Gli amministratori ricevono infiniti inviti
Applicabile solo per \"Solo invito\"", "invite-expiration": "Invito scaduto", "invite-expiration-help": "Il tuo invito scadrà tra %1 giorni.", "min-username-length": "Lunghezza Minima Username", @@ -63,7 +63,7 @@ "outgoing-new-tab": "Apri link esterni in una nuova scheda", "topic-search": "Abilita ricerca nella Discussione", "digest-freq": "Iscriviti al Riepilogo", - "digest-freq.off": "Off", + "digest-freq.off": "Spento", "digest-freq.daily": "Quotidiano", "digest-freq.weekly": "Settimanale", "digest-freq.monthly": "Mensile", @@ -72,8 +72,8 @@ "follow-created-topics": "Segui le discussioni che tu crei", "follow-replied-topics": "Segui discussioni a cui rispondi tu", "default-notification-settings": "Impostazioni di notifica predefinite", - "categoryWatchState": "Default category watch state", - "categoryWatchState.watching": "Watching", - "categoryWatchState.notwatching": "Not Watching", + "categoryWatchState": "Stato predefinito della categoria di controllo", + "categoryWatchState.watching": "Guardare", + "categoryWatchState.notwatching": "Non Guardare", "categoryWatchState.ignoring": "Ignorato" } \ No newline at end of file diff --git a/public/language/it/email.json b/public/language/it/email.json index 7abdd4bb4d..7db90c1bee 100644 --- a/public/language/it/email.json +++ b/public/language/it/email.json @@ -34,7 +34,7 @@ "notif.chat.cta": "Clicca qui per continuare la conversazione", "notif.chat.unsub.info": "Questa notifica di chat ti è stata inviata perché l'hai sottoscritta nelle impostazioni.", "notif.post.unsub.info": "Questa notifica di discussione ti è stata inviata perché l'hai sottoscritta nelle impostazioni.", - "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.post.unsub.one-click": "In alternativa, annulla l'iscrizione a future email come questa, facendo clic", "notif.cta": "Al forum", "notif.cta-new-reply": "Visualizza Post", "notif.cta-new-chat": "Visualizza Chat", @@ -42,8 +42,8 @@ "notif.test.long": "Questo è un test delle notifiche email. Invia aiuto!", "test.text1": "Questa è una email di prova per verificare che il servizio di invio email è configurato correttamente sul tuo NodeBB.", "unsub.cta": "Clicca qui per modificare queste impostazioni", - "unsubscribe": "unsubscribe", - "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsubscribe": "Annulla l'iscrizione", + "unsub.success": "Non riceverai più email dalla %1 mailing list", "banned.subject": "Sei stato bannato da %1", "banned.text1": "L'utente %1 è stato bannato da %2", "banned.text2": "Questo ban durerà fino a %1.", diff --git a/public/language/it/error.json b/public/language/it/error.json index e42e2f7c56..04f0c79870 100644 --- a/public/language/it/error.json +++ b/public/language/it/error.json @@ -26,14 +26,14 @@ "invalid-pagination-value": "Valore di impaginazione non valido, deve essere almeno %1 ed al massimo %2", "username-taken": "Nome utente già esistente", "email-taken": "Email già esistente", - "email-not-confirmed": "You are unable to post until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed": "Non puoi pubblicare post finché la tua email non è confermata, fai clic qui per confermare la tua email", "email-not-confirmed-chat": "Non puoi chattare finché non confermi la tua email, per favore clicca qui per confermare la tua email.", - "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You won't be able to post or chat until your email is confirmed.", + "email-not-confirmed-email-sent": "La tua email non è stata ancora confermata, controlla la tua casella di posta per l'email di conferma. Non potrai pubblicare post o chattare fino a quando la tua email non sarà confermata.", "no-email-to-confirm": "Questo forum richiede una conferma via email, clicca qui per inserire un'email", "email-confirm-failed": "Non abbiamo potuto confermare la tua email, per favore riprovaci più tardi.", "confirm-email-already-sent": "Email di conferma già inviata, per favore attendere %1 minuto(i) per inviarne un'altra.", "sendmail-not-found": "Impossibile trovare l'eseguibile di sendmail, per favore assicurati che sia installato ed eseguibile dall'utente che esegue NodeBB.", - "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "digest-not-enabled": "Questo utente non ha digest attivi o l'impostazione predefinita del sistema non è configurata per l'invio di digest", "username-too-short": "Nome utente troppo corto", "username-too-long": "Nome utente troppo lungo", "password-too-long": "Password troppo lunga", diff --git a/public/language/it/user.json b/public/language/it/user.json index b0238a3858..e57a828965 100644 --- a/public/language/it/user.json +++ b/public/language/it/user.json @@ -159,17 +159,17 @@ "consent.email_intro": "Occasionalmente, potremmo inviare email al tuo indirizzo email registrato per fornirti aggiornamenti e/o per informarti di nuove attività che ti riguardano. Puoi personalizzare la frequenza del riepilogo della comunità (compresa la disabilitazione definitiva), così come selezionare quali tipi di notifiche ricevere via email, tramite la pagina delle impostazioni utente.", "consent.digest_frequency": "A meno che non sia stato modificato esplicitamente nelle impostazioni utente, questa comunità fornisce email riepilogative ogni %1.", "consent.digest_off": "A meno che non sia stato modificato esplicitamente nelle impostazioni utente, questa comunità non invia email riepilogative", - "consent.received": "You have provided consent for this website to collect and process your information. No additional action is required.", - "consent.not_received": "You have not provided consent for data collection and processing. At any time this website's administration may elect to delete your account in order to become compliant with the General Data Protection Regulation.", + "consent.received": "Hai fornito il consenso a questo sito Web per raccogliere ed elaborare le tue informazioni. Non è richiesta alcuna azione aggiuntiva.", + "consent.not_received": "Non hai fornito il consenso per la raccolta e l'elaborazione dei dati. In qualsiasi momento l'amministrazione di questo sito Web può decidere di eliminare il tuo account per renderlo conforme al regolamento generale sulla protezione dei dati.", "consent.give": "Consenti", "consent.right_of_access": "Hai i privilegi di accesso", - "consent.right_of_access_description": "You have the right to access any data collected by this website upon request. You can retrieve a copy of this data by clicking the appropriate button below.", + "consent.right_of_access_description": "Hai il diritto di accedere a tutti i dati raccolti da questo sito Web su richiesta. È possibile recuperare una copia di questi dati facendo clic sul pulsante appropriato di seguito.", "consent.right_to_rectification": "Hai i privilegi alla rettifica", - "consent.right_to_rectification_description": "You have the right to change or update any inaccurate data provided to us. Your profile can be updated by editing your profile, and post content can always be edited. If this is not the case, please contact this site's administrative team.", + "consent.right_to_rectification_description": "Hai il diritto di modificare o aggiornare i dati inesatti forniti a noi. Il tuo profilo può essere aggiornato modificando il tuo profilo e il contenuto dei post può sempre essere modificato. In caso contrario, contattare questo team amministrativo del sito.", "consent.right_to_erasure": "Hai i privilegi per cancellare", - "consent.right_to_erasure_description": "At any time, you are able to revoke your consent to data collection and/or processing by deleting your account. Your individual profile can be deleted, although your posted content will remain. If you wish to delete both your account and your content, please contact the administrative team for this website.", + "consent.right_to_erasure_description": "In qualsiasi momento, puoi revocare il tuo consenso alla raccolta e / o al trattamento dei dati eliminando il tuo account. Il tuo profilo individuale può essere eliminato, anche se i contenuti pubblicati rimarranno. Se desideri eliminare entrambi i tuoi account e i tuoi contenuti, contatta il team amministrativo per questo sito Web.", "consent.right_to_data_portability": "Hai i privilegi alla portabilità dei dati", - "consent.right_to_data_portability_description": "You may request from us a machine-readable export of any collected data about you and your account. You can do so by clicking the appropriate button below.", + "consent.right_to_data_portability_description": "Puoi richiedere da noi un'esportazione leggibile meccanicamente di tutti i dati raccolti su di te e sul tuo account. Puoi farlo facendo clic sul pulsante appropriato in basso.", "consent.export_profile": "Esporta i profili (.csv)", "consent.export_uploads": "Esporta i contenuti caricati (.zip)", "consent.export_posts": "Esporta i post (.csv)" diff --git a/public/language/pl/admin/extend/widgets.json b/public/language/pl/admin/extend/widgets.json index 43b8bacd49..d2b4531f1b 100644 --- a/public/language/pl/admin/extend/widgets.json +++ b/public/language/pl/admin/extend/widgets.json @@ -20,11 +20,11 @@ "error.select-clone": "Proszę wybrać stronę do sklonowania", - "title": "Title", - "title.placeholder": "Title (only shown on some containers)", - "container": "Container", - "container.placeholder": "Drag and drop a container or enter HTML here.", - "show-to-groups": "Show to groups", - "hide-from-groups": "Hide from groups", - "hide-on-mobile": "Hide on mobile" + "title": "Tytuł", + "title.placeholder": "Tytuł (wyświetlany tylko w niektórych kontenerach)", + "container": "Kontener", + "container.placeholder": "Przeciągnij i upuść kontener lub wpisz tutaj HTML.", + "show-to-groups": "Pokaż dla grup", + "hide-from-groups": "Ukryj dla grup", + "hide-on-mobile": "Ukraj na urządzeniach mobilnych" } \ No newline at end of file diff --git a/public/language/pl/admin/manage/digest.json b/public/language/pl/admin/manage/digest.json index 075b8e1aff..40437bb572 100644 --- a/public/language/pl/admin/manage/digest.json +++ b/public/language/pl/admin/manage/digest.json @@ -3,19 +3,19 @@ "disclaimer": "Please be advised that email delivery is not guaranteed, due to the nature of email technology. Many variables factor into whether an email sent to the recipient server is ultimately delivered into the user's inbox, including server reputation, blacklisted IP addresses, and whether DKIM/SPF/DMARC is configured.", "disclaimer-continued": "A successful delivery means the message was sent successfully by NodeBB and acknowledged by the recipient server. It does not mean the email landed in the inbox. For best results, we recommend using a third-party email delivery service such as SendGrid.", - "user": "User", - "subscription": "Subscription Type", + "user": "Użytkownik", + "subscription": "Typ subskrypcji", "last-delivery": "Last successful delivery", - "default": "System default", + "default": "Domyślne ustawienie systemowe", "default-help": "System default means the user has not explicitly overridden the global forum setting for digests, which is currently: "%1"", - "resend": "Resend Digest", - "resend-all-confirm": "Are you sure you wish to mnually execute this digest run?", - "resent-single": "Manual digest resend completed", - "resent-day": "Daily digest resent", - "resent-week": "Weekly digest resent", - "resent-month": "Monthly digest resent", - "null": "Never", - "manual-run": "Manual digest run:", + "resend": "Wyślij ponownie podsumowanie", + "resend-all-confirm": "Czy na pewno chcesz ręcznie wykonać włącznie podsumowania?", + "resent-single": "Ręczne wysyłanie podsumowania zakończone", + "resent-day": "Codzienne podsumowanie", + "resent-week": "Tygodniowe podsumowanie", + "resent-month": "Miesięczne podsumowanie", + "null": "Nigdy", + "manual-run": "Włącz ręcznie podsumowania", - "no-delivery-data": "No delivery data found" + "no-delivery-data": "Nie znaleziono danych" } \ 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 47ecddf320..31b6092230 100644 --- a/public/language/pl/admin/manage/privileges.json +++ b/public/language/pl/admin/manage/privileges.json @@ -1,8 +1,8 @@ { "global": "Globalny", "global.no-users": "Brak globalnych uprawnień zdefiniowanych dla użytkownika", - "group-privileges": "Group Privileges", - "user-privileges": "User Privileges", + "group-privileges": "Uprawnienia grup", + "user-privileges": "Uprawnienia użytkownika", "chat": "Dostęp do czatu", "upload-images": "Przesyłanie zdjęć", "upload-files": "Przesyłanie plików", @@ -16,7 +16,7 @@ "view-groups": "Wyświetlanie grup", "allow-local-login": "Logowanie lokalne", "allow-group-creation": "Tworzenie grup", - "view-users-info": "View Users Info", + "view-users-info": "Pokaż dane użytkownika", "find-category": "Szukanie kategorii", "access-category": "Dostęp do kategorii", "access-topics": "Dostęp do tematów", diff --git a/public/language/pl/admin/manage/users.json b/public/language/pl/admin/manage/users.json index 7b218170b4..84f2bac975 100644 --- a/public/language/pl/admin/manage/users.json +++ b/public/language/pl/admin/manage/users.json @@ -15,8 +15,8 @@ "delete": "Usuń użytkownika(-ów)", "purge": "Usuń użytkownika(-ów) oraz zawartość", "download-csv": "Pobierz CSV", - "manage-groups": "Manage Groups", - "add-group": "Add Group", + "manage-groups": "Zarządzaj grupami", + "add-group": "Dodaj grupę", "invite": "Zaproś", "new": "Nowy użytkownik", diff --git a/public/language/pl/admin/menu.json b/public/language/pl/admin/menu.json index f4e2bdb31a..a6380b8808 100644 --- a/public/language/pl/admin/menu.json +++ b/public/language/pl/admin/menu.json @@ -18,7 +18,7 @@ "manage/groups": "Grupy", "manage/ip-blacklist": "Czarna lista IP", "manage/uploads": "Przesłane pliki", - "manage/digest": "Digests", + "manage/digest": "Podsumownia", "section-settings": "Ustawienia", "settings/general": "Ogólne", diff --git a/public/language/pl/admin/settings/email.json b/public/language/pl/admin/settings/email.json index 1839cdaf85..de1d0e85d5 100644 --- a/public/language/pl/admin/settings/email.json +++ b/public/language/pl/admin/settings/email.json @@ -33,8 +33,8 @@ "testing.select": "Wybierz szablon e-maila", "testing.send": "Wyślij testowy e-mail", "testing.send-help": "Testowy e-mail zostanie wysłany na adres aktualnie zalogowanego użytkownika.", - "subscriptions": "Email Digests", - "subscriptions.disable": "Disable email digests", + "subscriptions": "Podsumowania e-mail", + "subscriptions.disable": "Wyłącz podsumowania e-maili", "subscriptions.hour": "Godzina podsumowania", "subscriptions.hour-help": "Wprowadź liczbę odpowiadającą godzinie, o której mają być wysyłane regularne e-maile z podsumowaniem (np. 0 dla północy lub 17 dla 17:00). Pamiętaj, że godzina jest godziną serwera i nie musi zgadzać się z czasem lokalnym administratora. Przybliżony czas serwera to:
Wysłanie kolejnego e-maila z podsumowaniem zaplanowano na " } \ No newline at end of file diff --git a/public/language/pl/admin/settings/post.json b/public/language/pl/admin/settings/post.json index fac43cb28d..8468917386 100644 --- a/public/language/pl/admin/settings/post.json +++ b/public/language/pl/admin/settings/post.json @@ -7,12 +7,12 @@ "sorting.most-posts": "Najwięcej postów", "sorting.topic-default": "Domyślne sortowanie tematów", "length": "Długość postu", - "post-queue": "Post Queue", + "post-queue": "Kolejka postów", "restrictions": "Restrykcje postowania", "restrictions-new": "Restrykcje dla nowych użytkowników", "restrictions.post-queue": "Włącz kolejkę postów", - "restrictions.post-queue-rep-threshold": "Reputation required to bypass post queue", - "restrictions.groups-exempt-from-post-queue": "Select groups that should be exempt from the post queue", + "restrictions.post-queue-rep-threshold": "Reputacja wymagana do ominięcia kolejki postów", + "restrictions.groups-exempt-from-post-queue": "Wybierz grupy, które powinny być zwolnione z kolejki postów", "restrictions-new.post-queue": "Włącz restrykcje dla nowych użytkowników", "restrictions.post-queue-help": "Włączenie kolejki postów spowoduje umieszczenie postów nowych użytkowników w kolejce do zatwierdzenia", "restrictions-new.post-queue-help": "Włączenie restrykcji dla nowych użytkowników ustawi restrykcje na ich wpisy.", @@ -32,8 +32,8 @@ "timestamp": "Znacznik czasowy", "timestamp.cut-off": "Termin odcięcia (w dniach)", "timestamp.cut-off-help": "Daty oraz godziny będą wyświetlane w sposób relatywny (np. \"3 godziny temu\" / \"5 dni temu\"), oraz przetłumaczone na różne\n\t\t\t\t\tjęzyki. Po określonym czasie, ten tekst może zostać zmieniony, aby wyświetlać sformatowane daty.\n\t\t\t\t\t(np. 4 Lut 2017 12:45).
(domyślnie: 30, lub jeden miesiąc). Ustaw 0, aby zawsze wyświetlać daty; pozostaw puste, aby korzystać z tylko z relatywnych opisów.", - "timestamp.necro-threshold": "Necro Threshold (in days)", - "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.necro-threshold": "Próg nekro (w dniach)", + "timestamp.necro-threshold-help": "Komunikat zostanie wyświetlony między postami, jeśli czas między nimi jest dłuższy niż próg nekro. (Domyślnie: 7 lub jeden tydzień). Ustaw na 0, aby wyłączyć.", "teaser": "Zwiastun postu", "teaser.last-post": "Ostatni – Pokaż ostatni post, włączając pierwszy post, w razie braku odpowiedzi", "teaser.last-reply": "Ostatni – Pokaż ostatnią odpowiedź lub komunikat „Brak odpowiedzi” w razie ich braku", diff --git a/public/language/pl/email.json b/public/language/pl/email.json index 3498550c40..7956ca203a 100644 --- a/public/language/pl/email.json +++ b/public/language/pl/email.json @@ -34,7 +34,7 @@ "notif.chat.cta": "Kliknij tutaj, aby kontynuować rozmowę", "notif.chat.unsub.info": "To powiadomienie o czacie zostało wysłane zgodnie z Twoimi ustawieniami.", "notif.post.unsub.info": "To powiadomienie o poście zostało wysłane zgodnie z Twoimi ustawieniami.", - "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", + "notif.post.unsub.one-click": "Możesz zrezygnować z otrzymywania takich e-maili w przyszłości, klikając", "notif.cta": "Na forum", "notif.cta-new-reply": "Pokaż wpisy", "notif.cta-new-chat": "Pokaż czat", @@ -42,8 +42,8 @@ "notif.test.long": "To jest email testowy z powiadomieniami. Wyślij pomoc!", "test.text1": "To jest e-mail testowy wysyłany w celu sprawdzenia konfiguracji e-mailera w NodeBB.", "unsub.cta": "Kliknij tutaj, aby zmienić te ustawienia", - "unsubscribe": "unsubscribe", - "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsubscribe": "Wypisz się", + "unsub.success": "Nie będziesz już otrzymywać wiadomości e-mail z %1", "banned.subject": "Zostałeś zbanowany na %1", "banned.text1": "Użytkownik %1 został zbanowany na %2.", "banned.text2": "Ban potrwa do %1", diff --git a/public/language/pl/error.json b/public/language/pl/error.json index 9dd4e7d239..5126800b6b 100644 --- a/public/language/pl/error.json +++ b/public/language/pl/error.json @@ -5,15 +5,15 @@ "account-locked": "Twoje konto zostało tymczasowo zablokowane", "search-requires-login": "Wyszukiwanie wymaga konta - zaloguj się lub zarejestruj.", "goback": "Wciśnij wstecz, aby powrócić do poprzedniej strony", - "invalid-cid": "Błędne ID kategorii", - "invalid-tid": "Błędne ID tematu", - "invalid-pid": "Błędne ID posta", - "invalid-uid": "Błędne ID użytkownika", - "invalid-username": "Błędny login", - "invalid-email": "Błędny e-mail", - "invalid-fullname": "Invalid Fullname", - "invalid-location": "Invalid Location", - "invalid-birthday": "Invalid Birthday", + "invalid-cid": "Nieprawidłowy ID kategorii", + "invalid-tid": "Nieprawidłowy ID tematu", + "invalid-pid": "Nieprawidłowy ID posta", + "invalid-uid": "Nieprawidłowy ID użytkownika", + "invalid-username": "Nieprawidłowy login", + "invalid-email": "Nieprawidłowy adres e-mail", + "invalid-fullname": "Nieprawidłowa nazwa", + "invalid-location": "Nieprawidłowa lokalizacja", + "invalid-birthday": "Nieprawidłowa data urodzenia", "invalid-title": "Błędna nazwa", "invalid-user-data": "Błędne dane użytkownika", "invalid-password": "Błędne hasło", @@ -26,14 +26,14 @@ "invalid-pagination-value": "Błędna wartość paginacji, zakres od %1 do %2", "username-taken": "Login zajęty", "email-taken": "Email zajęty", - "email-not-confirmed": "You are unable to post until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed": "Nie możesz opublikować, dopóki Twój adres e-mail nie zostanie potwierdzony, kliknij tutaj, aby potwierdzić swój adres e-mail.", "email-not-confirmed-chat": "Nie możesz prowadzić rozmów, dopóki twój email nie zostanie potwierdzony. Kliknij tutaj, aby potwierdzić swój email.", - "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You won't be able to post or chat until your email is confirmed.", + "email-not-confirmed-email-sent": "Twój e-mail nie został jeszcze potwierdzony, sprawdź swoją skrzynkę pocztową, aby znaleźć e-mail z potwierdzeniem. Nie będziesz mógł dodawać postów ani czatować, dopóki Twój adres e-mail nie zostanie potwierdzony.", "no-email-to-confirm": "To forum wymaga weryfikacji przez email. Proszę kliknąć tutaj, aby wprowadzić adres.", "email-confirm-failed": "Nie byliśmy w stanie potwierdzić Twojego adresu e-mail. Spróbuj później.", "confirm-email-already-sent": "Email potwierdzający został już wysłany, proszę odczekaj jeszcze %1 minut(y), aby wysłać kolejny.", "sendmail-not-found": "Program sendmail nie został znaleziony, proszę upewnij się, że jest zainstalowany i możliwy do uruchomienia przez użytkownika uruchamiającego NodeBB.", - "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "digest-not-enabled": "Ten użytkownik nie ma włączonych skrótów lub system nie jest skonfigurowany do wysyłania skrótów", "username-too-short": "Nazwa użytkownika za krótka", "username-too-long": "Zbyt długa nazwa użytkownika", "password-too-long": "Hasło jest za długie", @@ -103,8 +103,8 @@ "group-needs-owner": "Ta grupa musi mieć przynajmniej jednego właściciela", "group-already-invited": "Ten użytkownik został już zaproszony", "group-already-requested": "Twoje podanie o członkostwo zostało już wysłane", - "group-join-disabled": "You are not able to join this group at this time", - "group-leave-disabled": "You are not able to leave this group at this time", + "group-join-disabled": "Nie możesz teraz dołączyć do tej grupy", + "group-leave-disabled": "Obecnie nie możesz opuścić tej grupy", "post-already-deleted": "Ten post został już skasowany", "post-already-restored": "Ten post został już przywrócony", "topic-already-deleted": "Ten temat został już skasowany", diff --git a/public/language/pl/groups.json b/public/language/pl/groups.json index e983d428c2..e0569382ae 100644 --- a/public/language/pl/groups.json +++ b/public/language/pl/groups.json @@ -25,7 +25,7 @@ "details.latest_posts": "Ostatnie posty", "details.private": "Prywatna", "details.disableJoinRequests": "Wyłączono prośbę o dołączenie", - "details.disableLeave": "Disallow users from leaving the group", + "details.disableLeave": "Wyłącz możliwość opuszczania użytkowników z grupy", "details.grant": "Nadaj/Cofnij prawa Właściciela", "details.kick": "Wykop", "details.kick_confirm": "Jesteś pewny, że chcesz wyrzucić tego użytkownika z grupy?", @@ -49,11 +49,11 @@ "event.updated": "Dane grupy zostały zaktualizowane", "event.deleted": "Grupa \"%1\" została usunięta", "membership.accept-invitation": "Przyjmij zaproszenie", - "membership.accept.notification_title": "You are now a member of %1", + "membership.accept.notification_title": "Jesteś teraz członkiem %1", "membership.invitation-pending": "Oczekujące zaproszenie", "membership.join-group": "Dołącz do grupy", "membership.leave-group": "Opuść grupę", - "membership.leave.notification_title": "%1 has left group %2", + "membership.leave.notification_title": "%1 opuścił grupę %2", "membership.reject": "Odrzuć", "new-group.group_name": "Nazwa grupy:", "upload-group-cover": "Prześlij zdjęcie tła grupy", diff --git a/public/language/pl/reset_password.json b/public/language/pl/reset_password.json index 6e36eac103..69f90297eb 100644 --- a/public/language/pl/reset_password.json +++ b/public/language/pl/reset_password.json @@ -1,6 +1,6 @@ { "reset_password": "Zresetuj hasło", - "update_password": "Zmień hasło", + "update_password": "Zaktualizuj hasło", "password_changed.title": "Hasło zmienione", "password_changed.message": "

Hasło zostało zmienione. Zaloguj się ponownie.", "wrong_reset_code.title": "Nieprawidłowy kod resetujący", @@ -9,7 +9,7 @@ "repeat_password": "Powtórz hasło", "enter_email": "Podaj swój adres e-mail, by otrzymać wiadomość z instrukcjami, jak zresetować hasło.", "enter_email_address": "Wpisz swój adres e-mail", - "password_reset_sent": "If the specified address corresponds to an existing user account, a password reset email was sent. Please note that only one email will be sent per minute.", + "password_reset_sent": "Jeśli podany adres odpowiada istniejącemu kontu użytkownika, to zostanie wysłana wiadomość e-mail dotyczącą resetowania hasła. Pamiętaj, że na minutę zostanie wysłany tylko jeden e-mail.", "invalid_email": "Nieprawidłowy adres e-mail.", "password_too_short": "Wprowadzone hasło jest zbyt krótkie, wybierz inne hasło.", "passwords_do_not_match": "Wprowadzone hasła nie pasują do siebie", diff --git a/public/language/pl/topic.json b/public/language/pl/topic.json index 01beb27cc7..45f744e20d 100644 --- a/public/language/pl/topic.json +++ b/public/language/pl/topic.json @@ -34,7 +34,7 @@ "moved": "Przeniesiony", "copy-ip": "Kopiuj IP", "ban-ip": "Blokuj IP", - "view-history": "Edytuj historię", + "view-history": "Historia edycji", "bookmark_instructions": "Kliknij tutaj, by powrócić do ostatniego przeczytanego postu w tym temacie.", "flag_title": "Zgłoś post do moderacji", "merged_message": "Ten temat został połączony z %2", diff --git a/public/language/pl/user.json b/public/language/pl/user.json index b116718938..d5346c094c 100644 --- a/public/language/pl/user.json +++ b/public/language/pl/user.json @@ -26,7 +26,7 @@ "reputation": "Reputacja", "bookmarks": "Zakładki", "watched_categories": "Obserwowane kategorie", - "change_all": "Change All", + "change_all": "Zmień wszystko", "watched": "Obserwowane", "ignored": "Zignorowane", "default-category-watch-state": "Domyślny stan oglądania kategorii", @@ -125,7 +125,7 @@ "follow_topics_you_reply_to": "Obserwuj tematy, w których uczestniczysz", "follow_topics_you_create": "Obserwuj tematy, które utworzyłeś", "grouptitle": "Nazwa grupy", - "group-order-help": "Select a group and use the arrows to order titles", + "group-order-help": "Wybierz grupę i użyj strzałek, aby zamówić tytuł", "no-group-title": "Brak nazwy grupy", "select-skin": "Wybierz skórkę", "select-homepage": "Wybierz stronę startową", diff --git a/public/language/ru/admin/extend/plugins.json b/public/language/ru/admin/extend/plugins.json index a497727e3f..604fc95c41 100644 --- a/public/language/ru/admin/extend/plugins.json +++ b/public/language/ru/admin/extend/plugins.json @@ -24,8 +24,8 @@ "plugin-item.install": "Установить", "plugin-item.uninstall": "Удалить", "plugin-item.settings": "Настройки", - "plugin-item.installed": "Установленные", - "plugin-item.latest": "Недавние", + "plugin-item.installed": "Установленная версия", + "plugin-item.latest": "Последняя версия", "plugin-item.upgrade": "Обновить", "plugin-item.more-info": "Дополнительная информация:", "plugin-item.unknown": "Неизвестно", diff --git a/public/language/uk/category.json b/public/language/uk/category.json index 0ebb159ac6..63be07c88a 100644 --- a/public/language/uk/category.json +++ b/public/language/uk/category.json @@ -10,13 +10,13 @@ "watch": "Стежити", "ignore": "Ігнорувати", "watching": "Відстежується", - "not-watching": "Not Watching", + "not-watching": "Не спостерігається", "ignoring": "Ігнорувати", - "watching.description": "Show topics in unread and recent", - "not-watching.description": "Do not show topics in unread, show in recent", - "ignoring.description": "Do not show topics in unread and recent", - "watching.message": "You are now watching updates from this category and all subcategories", - "notwatching.message": "You are not watching updates from this category and all subcategories", - "ignoring.message": "You are now ignoring updates from this category and all subcategories", + "watching.description": "Показати теми в непрочитаних та останніх", + "not-watching.description": "Не показувати теми в непрочитаних, показувати в останніх", + "ignoring.description": "Не показувати теми в непрочитаних і останніх", + "watching.message": "Ви зараз спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій", + "notwatching.message": "Зараз ви не спостерігаєте за оновленнями з цієї категорії та всіх її підкатегорій", + "ignoring.message": "Зараз ви ігноруєте оновлення з цієї категорії та всіх її підкатегорій", "watched-categories": "Переглянуті категорії" } \ No newline at end of file diff --git a/public/language/uk/email.json b/public/language/uk/email.json index 839b8f5b37..fc78b875d0 100644 --- a/public/language/uk/email.json +++ b/public/language/uk/email.json @@ -1,19 +1,19 @@ { - "test-email.subject": "Test Email", - "password-reset-requested": "Password Reset Requested!", + "test-email.subject": "Тестове поштове повідомлення", + "password-reset-requested": "Отримано запит на скидання пароля!", "welcome-to": "Ласкаво просимо до %1", "invite": "Запрошення від %1", "greeting_no_name": "Привіт", "greeting_with_name": "Привіт %1", - "email.verify-your-email.subject": "Please verify your email", - "email.verify.text1": "Your email address has changed!", + "email.verify-your-email.subject": "Будь-ласка перевірте вашу електронну адресу", + "email.verify.text1": "Ваша електронна адреса змінилась!", "welcome.text1": "Дякуємо за реєстрацію з %1!", "welcome.text2": "Щоб повністю активувати ваш акаунт, нам потрібно перевірити, що вам належить електронна адреса, яку ви вказали при реєстрації ", "welcome.text3": "Адміністратор схвалив ваш запит на реєстрацію. Ви можете залогінитись, використовуючи свій пароль та назву акаунту", "welcome.cta": "Натисніть тут, щоб підтвердити вашу електронну адресу", "invitation.text1": "%1 запросив вас приєднатися до %2", "invitation.text2": "Термін дії вашого запрошення закінчиться за %1 днів.", - "invitation.cta": "Click here to create your account.", + "invitation.cta": "Натисніть тут щоб створити акаунт.", "reset.text1": "Ми отримали запит на відновлення вашого паролю, можливо тому, что ви його забули. Якщо вам це не потрібно - проігноруйте цей лист", "reset.text2": "Щоб продовжити відновлення паролю, будь ласка, перейдіть за посиланням", "reset.cta": "Натисніть тут щоб скинути Ваш пароль", @@ -27,23 +27,23 @@ "digest.week": "тиждень", "digest.month": "місяць", "digest.subject": "Дайджест для %1", - "digest.title.day": "Your Daily Digest", - "digest.title.week": "Your Weekly Digest", - "digest.title.month": "Your Monthly Digest", + "digest.title.day": "Ваш щоденний дайджест", + "digest.title.week": "Ваш тижневий дайджест", + "digest.title.month": "Ваш місячний дайджест", "notif.chat.subject": "Отримане нове повідомлення чату від %1", "notif.chat.cta": "Натисніть тут, щоб продовжити розмову", "notif.chat.unsub.info": "Це повідомлення чату було вислано вам, згідно ваших налаштувань підписки", "notif.post.unsub.info": "Це поштове повідомлення було вислано вам, згідно ваших налаштувань підписки", - "notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking", - "notif.cta": "To the forum", - "notif.cta-new-reply": "View Post", - "notif.cta-new-chat": "View Chat", - "notif.test.short": "Testing Notifications", - "notif.test.long": "This is a test of the notifications email. Send help!", + "notif.post.unsub.one-click": "Ви також можете відписатись від схожих майбутніх повідомлень, натиснувши тут", + "notif.cta": "На форум", + "notif.cta-new-reply": "Переглянути допис", + "notif.cta-new-chat": "Переглянути чат", + "notif.test.short": "Перевірка сповіщень", + "notif.test.long": "Це перевірка повідомлення про сповіщення.", "test.text1": "Це пробний лист для верифікації поштової служби. Всі налаштування вірні для NodeBB.", "unsub.cta": "Натисніть тут, щоб змінити ці налаштування", - "unsubscribe": "unsubscribe", - "unsub.success": "You will no longer receive emails from the %1 mailing list", + "unsubscribe": "відписатись", + "unsub.success": "Ви більше не будете отримувати повідомлення з %1 поштової розсилки", "banned.subject": "Ви були забанені на %1", "banned.text1": "Користувач %1 був забанений на %2.", "banned.text2": "Тривалість бану - до %1.", diff --git a/public/language/uk/error.json b/public/language/uk/error.json index 0d7ea48f09..c2728ccf5b 100644 --- a/public/language/uk/error.json +++ b/public/language/uk/error.json @@ -11,9 +11,9 @@ "invalid-uid": "Невірний ID користувача", "invalid-username": "Невірне ім'я користувача", "invalid-email": "Невірна електронна адреса", - "invalid-fullname": "Invalid Fullname", - "invalid-location": "Invalid Location", - "invalid-birthday": "Invalid Birthday", + "invalid-fullname": "Невірне повне ім'я", + "invalid-location": "Невірне місцезнаходження", + "invalid-birthday": "Невірна дата народження", "invalid-title": "Невірний заголовок", "invalid-user-data": "Невірні користувацькі дані", "invalid-password": "Невірний пароль", @@ -26,18 +26,18 @@ "invalid-pagination-value": "Невірне значення сторінки, має бути щонайменше %1 та щонайбільше %2", "username-taken": "Це ім'я зайняте", "email-taken": "Ця електронна пошта зайнята", - "email-not-confirmed": "You are unable to post until your email is confirmed, please click here to confirm your email.", + "email-not-confirmed": "Ви не зможете постити до підтвердження вашої електронної адреси, будь-ласка натисніть тут щоб підтвердити свій емейл.", "email-not-confirmed-chat": "Ви не можете користуватися чатом поки ваша електронна пошта не буде підтверджена, натисніть тут, щоб це зробити.", - "email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email. You won't be able to post or chat until your email is confirmed.", + "email-not-confirmed-email-sent": "Ваша електронна адреса ще не була підтверджена, будь-ласка перевірте свою поштову скриньку. Ви не зможете постити або чатитись до того як ваш емейл підтверджено.", "no-email-to-confirm": "Цей форум вимагає підтвердження електронної пошти, будь-ласка, натисніть тут, щоб його ввести.", "email-confirm-failed": "Ми не можемо підтвердити вашу електронну пошту, будь ласка, спробуйте пізніше.", "confirm-email-already-sent": "Підтвердження по електронній пошті вже було надіслано, зачекайте, будь ласка, %1 хвилин(и), щоб відправити ще одне. ", "sendmail-not-found": "Виконуваний файл sendmail не знайдено, переконайтесь, будь ласка, що його встановлено та що він виконується власником процесу NodeBB.", - "digest-not-enabled": "This user does not have digests enabled, or the system default is not configured to send digests", + "digest-not-enabled": "Цей користувач не має активних дайджестів, або налаштування по замовчанню не включають надсилання дайджестів.", "username-too-short": "Ім'я користувача закоротке", "username-too-long": "Ім'я користувача задовге", "password-too-long": "Пароль задовгий", - "reset-rate-limited": "Too many password reset requests (rate limited)", + "reset-rate-limited": "Занадто багато запитів на скидання паролю (кількість за період часу обмежена)", "user-banned": "Користувача забанено", "user-banned-reason": "Вибачте, але цей акаунт було забанено (Причина: %1)", "user-banned-reason-until": "Вибачте, цей акаунт забанений до %1 (Причина: %2)", @@ -83,7 +83,7 @@ "still-uploading": "Зачекайте, будь ласка, доки завантаження завершиться.", "file-too-big": "Максимальний розмір файлу %1 кБ — завантажте менший файл, будь ласка.", "guest-upload-disabled": "Гостьове завантаження вимкнено.", - "cors-error": "Unable to upload image due to misconfigured CORS", + "cors-error": "Неможливо завантажити зображення через неправильно налаштований CORS", "already-bookmarked": "Ви вже додали цей пост собі в закладки", "already-unbookmarked": "Ви вже видалили цей пост із закладок", "cant-ban-other-admins": "Ви не можете банити інших адмінів!", @@ -93,7 +93,7 @@ "invalid-image-type": "Невірний тип зображення. Дозволені типи: %1", "invalid-image-extension": "Невірне розширення зображення", "invalid-file-type": "Невірний тип файлу. Дозволені типи: %1", - "invalid-image-dimensions": "Image dimensions are too big", + "invalid-image-dimensions": "Зображення занадто велике", "group-name-too-short": "Ім'я групи занадто коротке", "group-name-too-long": "Ім'я групи занадто довге", "group-already-exists": "Група вже існує", @@ -103,8 +103,8 @@ "group-needs-owner": "Ця група потребує щонайменше одного власника", "group-already-invited": "Користувача вже було запрошено", "group-already-requested": "Ваша заявка на вступ вже подана", - "group-join-disabled": "You are not able to join this group at this time", - "group-leave-disabled": "You are not able to leave this group at this time", + "group-join-disabled": "Ви не можете приєднатись до цієї групи зараз", + "group-leave-disabled": "Ви не можете покинути цю групу зараз", "post-already-deleted": "Цей пост вже видалено", "post-already-restored": "Цей пост вже відновлено", "topic-already-deleted": "Ця тема вже була видалена", @@ -127,7 +127,7 @@ "chat-edit-duration-expired": "Ви можете редагувати повідомлення чату лише через %1 секунд після публікації", "chat-delete-duration-expired": "Ви можете видаляти повідомлення чату лише через %1 секунд після публікації", "chat-deleted-already": "Це повідомлення чату вже було видалено.", - "chat-restored-already": "This chat message has already been restored.", + "chat-restored-already": "Це чат повідомлення вже було відновлене", "already-voting-for-this-post": "Ви вже проголосували за цей пост.", "reputation-system-disabled": "Система репутацій вимкнена.", "downvoting-disabled": "Голосування проти вимкнено", @@ -159,8 +159,8 @@ "cant-move-to-same-topic": "Ви не можете перемістити пост до тієї ж самої теми!", "cannot-block-self": "Ви не можете заблокувати самого себе!", "cannot-block-privileged": "Ви не можете заблокувати адміністраторів або глобальних модераторів", - "cannot-block-guest": "Guest are not able to block other users", - "already-blocked": "This user is already blocked", - "already-unblocked": "This user is already unblocked", + "cannot-block-guest": "Гості не можуть блокувати інших користувачів", + "already-blocked": "Цей користувач вже заблокований", + "already-unblocked": "Цей користувач вже розблокований", "no-connection": "Схоже, виникла проблема з вашим Інтернет-з'єднанням" } \ No newline at end of file diff --git a/public/language/uk/global.json b/public/language/uk/global.json index 3a3a6e6928..1d0447038a 100644 --- a/public/language/uk/global.json +++ b/public/language/uk/global.json @@ -59,8 +59,8 @@ "downvoted": "Проти", "views": "Перегляди", "reputation": "Репутація", - "lastpost": "Last post", - "firstpost": "First post", + "lastpost": "Останній допис", + "firstpost": "Перший допис", "read_more": "читати далі", "more": "Більше", "posted_ago_by_guest": "запостив Гість %1", @@ -87,7 +87,7 @@ "language": "Мова", "guest": "Гість", "guests": "Гості", - "former_user": "A Former User", + "former_user": "Колишній користувач", "updated.title": "Форум оновлено", "updated.message": "Форум було щойно оновлено до останньої версії. Клікніть тут, щоб оновити сторінку.", "privacy": "Приватність", diff --git a/public/language/uk/groups.json b/public/language/uk/groups.json index a3f5cd8bdb..2254fbac27 100644 --- a/public/language/uk/groups.json +++ b/public/language/uk/groups.json @@ -25,11 +25,11 @@ "details.latest_posts": "Останні пости", "details.private": "Приватна", "details.disableJoinRequests": "Вимкнути запити на приєднання", - "details.disableLeave": "Disallow users from leaving the group", + "details.disableLeave": "Забороніть користувачам покидати групу", "details.grant": "Надати/забрати права адміністратора", "details.kick": "Вигнати", "details.kick_confirm": "Ви впевнені, що бажаєте видалити цього користувача з групи?", - "details.add-member": "Add Member", + "details.add-member": "Додати члена групи", "details.owner_options": "Адміністрація групи", "details.group_name": "Назва групи", "details.member_count": "Кількість учасників", @@ -37,8 +37,8 @@ "details.description": "Опис", "details.badge_preview": "Попередній перегляд бейджа", "details.change_icon": "Змінити іконку", - "details.change_label_colour": "Change Label Colour", - "details.change_text_colour": "Change Text Colour", + "details.change_label_colour": "Змінити колір позначки", + "details.change_text_colour": "Змінити колір тексту", "details.badge_text": "Текст бейджа", "details.userTitleEnabled": "Показати бейдж", "details.private_help": "Якщо увімкнено, приєднання до групи вимагає підтвердження власника.", @@ -49,11 +49,11 @@ "event.updated": "Деталі групи оновлено", "event.deleted": "Група \"%1\" видалена", "membership.accept-invitation": "Прийняти запрошення", - "membership.accept.notification_title": "You are now a member of %1", + "membership.accept.notification_title": "Тепер ви є членом %1", "membership.invitation-pending": "Запрошення в черзі", "membership.join-group": "Приєднатися до групи", "membership.leave-group": "Покинути групу", - "membership.leave.notification_title": "%1 has left group %2", + "membership.leave.notification_title": "%1 покинув групу %2", "membership.reject": "Відхилити", "new-group.group_name": "Назва групи:", "upload-group-cover": "Завантажити обкладинку групи", diff --git a/public/language/uk/modules.json b/public/language/uk/modules.json index 132ede2891..a074c3fd82 100644 --- a/public/language/uk/modules.json +++ b/public/language/uk/modules.json @@ -22,7 +22,7 @@ "chat.delete_message_confirm": "Ви впевнені, що хочете видалити це повідомлення?", "chat.retrieving-users": "Отримання користувачів...", "chat.manage-room": "Управління чат кімнатами", - "chat.add-user-help": "Search for users here. When selected, the user will be added to the chat. The new user will not be able to see chat messages written before they were added to the conversation. Only room owners () may remove users from chat rooms.", + "chat.add-user-help": "Шукайте користувачів тут. Користувача можна додати до чату, обравши його. Нові користувачі не можуть бачити повідомлення, написані до того, як їх додали до розмови. Тільки власники кімнат можуть видаляти користувачів з кімнат.", "chat.confirm-chat-with-dnd-user": "Користувач змінив свій статус на DnD (Не турбувати). Ви дійсно бажаєте надіслати йому повідомлення в чат?", "chat.rename-room": "Перейменувати Кімнату", "chat.rename-placeholder": "Введіть назву своєї кімнати тут", @@ -33,10 +33,10 @@ "chat.in-room": "У цій кімнаті", "chat.kick": "Штурхнути", "chat.show-ip": "Показати IP", - "chat.owner": "Room Owner", - "chat.system.user-join": "%1 has joined the room", - "chat.system.user-leave": "%1 has left the room", - "chat.system.room-rename": "%2 has renamed this room: %1", + "chat.owner": "Власник кімнати", + "chat.system.user-join": "%1 зайшов в кімнату", + "chat.system.user-leave": "%1 покинув кімнату", + "chat.system.room-rename": "%2 перейменував кімнату на: %1", "composer.compose": "Редактор повідомлень", "composer.show_preview": "Показати попередній перегляд", "composer.hide_preview": "Сховати попередній перегляд", @@ -50,7 +50,7 @@ "composer.formatting.italic": "Курсив", "composer.formatting.list": "Список", "composer.formatting.strikethrough": "Закреслений", - "composer.formatting.code": "Code", + "composer.formatting.code": "Код", "composer.formatting.link": "Посилання", "composer.formatting.picture": "Зображення", "composer.upload-picture": "Завантажити зображення", diff --git a/public/language/uk/notifications.json b/public/language/uk/notifications.json index 0015cd1c7f..e0c4ab4f3e 100644 --- a/public/language/uk/notifications.json +++ b/public/language/uk/notifications.json @@ -8,7 +8,7 @@ "outgoing_link_message": "Ви залишаєте %1", "continue_to": "Перейти до %1", "return_to": "Повернутись до %1", - "new_notification": "You have a new notification", + "new_notification": "У вас нове сповіщення", "you_have_unread_notifications": "У вас немає непрочитаних сповіщень", "all": "Всі", "topics": "Теми", @@ -56,7 +56,7 @@ "notificationType_follow": "Коли хтось починає слідкувати за вами", "notificationType_new-chat": "Коли ви отримуєте повідомлення чату", "notificationType_group-invite": "Коли ви отримуєте запрошення до групи", - "notificationType_group-request-membership": "When someone requests to join a group you own", + "notificationType_group-request-membership": "Коли хтось подає запит на приєднання до групи, якою ви володієте", "notificationType_new-register": "Коли когось додано до черги на реєстрацію", "notificationType_post-queue": "Коли новий пост знаходиться в черзі", "notificationType_new-post-flag": "Коли повідомлення позначено", diff --git a/public/language/uk/pages.json b/public/language/uk/pages.json index e41ef13136..88bae4fd9f 100644 --- a/public/language/uk/pages.json +++ b/public/language/uk/pages.json @@ -6,10 +6,10 @@ "popular-month": "Популярні теми цього місяця", "popular-alltime": "Популярні теми за весь час", "recent": "Свіжі теми", - "top-day": "Top voted topics today", - "top-week": "Top voted topics this week", - "top-month": "Top voted topics this month", - "top-alltime": "Top Voted Topics", + "top-day": "Найрейтинговіші теми сьогодні", + "top-week": "Найрейтинговіші теми цього тижня", + "top-month": "Найрейтинговіші теми цього місяця", + "top-alltime": "Найрейтинговіші теми", "moderator-tools": "Інструменти Модератора", "flagged-content": "Оскаржений вміст", "ip-blacklist": "Чорний список IP адрес", @@ -43,10 +43,10 @@ "account/following": "Люди за котрими стежить %1", "account/followers": "Люди котрі стежать за %1", "account/posts": "Пости написані %1", - "account/latest-posts": "Latest posts made by %1", + "account/latest-posts": "Останні дописи від %1", "account/topics": "Теми створені %1", "account/groups": "Групи %1", - "account/watched_categories": "%1's Watched Categories", + "account/watched_categories": "Категорії, за якими спостерігає %1", "account/bookmarks": "Закладки %1", "account/settings": "Налаштування користувача", "account/watched": "Теми за якими стежить %1", @@ -56,7 +56,7 @@ "account/best": "Найкращі пости %1", "account/blocks": "Заблоковані користувачі для %1", "account/uploads": "Завантаження від %1", - "account/sessions": "Login Sessions", + "account/sessions": "Логін-сесії", "confirm": "Електронну пошту підтверджено", "maintenance.text": "%1 в данний час на технічному обслуговувані. Завітайте, будь ласка, пізніше.", "maintenance.messageIntro": "Крім того, адміністратор залишив це повідомлення:", diff --git a/public/language/uk/reset_password.json b/public/language/uk/reset_password.json index 68d32de939..6de4009fd3 100644 --- a/public/language/uk/reset_password.json +++ b/public/language/uk/reset_password.json @@ -9,7 +9,7 @@ "repeat_password": "Підтвердіть пароль", "enter_email": "Будь ласка, введіть свою електронну пошту і ми надішлемо вам листа с інструкцією як скинути ваш обліковий запис.", "enter_email_address": "Введіть електронну пошту", - "password_reset_sent": "If the specified address corresponds to an existing user account, a password reset email was sent. Please note that only one email will be sent per minute.", + "password_reset_sent": "Якщо зазначена електронна адреса належить існуючому користувачеві, повідомлення для скидання паролю було надіслане на цю адресу. Майте на увазі, що тільки одне повідомлення може бути надіслане за хвилину.", "invalid_email": "Невірна або неіснуюча електронна пошта!", "password_too_short": "Уведений пароль закороткий, оберіть, будь ласка, інший.", "passwords_do_not_match": "Паролі що ви ввели не співпадають.", diff --git a/public/language/uk/search.json b/public/language/uk/search.json index 029e8707f5..9bcc52226d 100644 --- a/public/language/uk/search.json +++ b/public/language/uk/search.json @@ -17,7 +17,7 @@ "at-most": "Щонайбільше", "relevance": "Релевантність", "post-time": "Час посту", - "votes": "Votes", + "votes": "Голоси", "newer-than": "Новіші за", "older-than": "Старіші за", "any-date": "Будь-яка дата", @@ -31,7 +31,7 @@ "sort-by": "Сортувати за", "last-reply-time": "Час останньої відповіді", "topic-title": "Заголовок теми", - "topic-votes": "Topic votes", + "topic-votes": "Голоси за тему", "number-of-replies": "Кількість відповідей", "number-of-views": "Кількість переглядів", "topic-start-date": "Час початку теми", @@ -44,5 +44,5 @@ "search-preferences-saved": "Налаштування пошуку збережено", "search-preferences-cleared": "Налаштування пошуку очищені", "show-results-as": "Показати результати як", - "see-more-results": "See more results (%1)" + "see-more-results": "Дивитись більше результатів (%1)" } \ No newline at end of file diff --git a/public/language/uk/topic.json b/public/language/uk/topic.json index 762604361e..c3e7484c52 100644 --- a/public/language/uk/topic.json +++ b/public/language/uk/topic.json @@ -18,13 +18,13 @@ "last_reply_time": "Остання відповідь", "reply-as-topic": "Відповісти темою", "guest-login-reply": "Увійти для відповіді", - "login-to-view": "🔒 Log in to view", + "login-to-view": "🔒 Увійдіть щоб переглянути", "edit": "Редагувати", "delete": "Видалити", "purge": "Стерти", "restore": "Відновити", "move": "Перемістити", - "change-owner": "Change Owner", + "change-owner": "Змінити Власника", "fork": "Відгалужити", "link": "Зв'язати", "share": "Поширити", @@ -66,7 +66,7 @@ "thread_tools.move": "Перемістити тему", "thread_tools.move-posts": "Перемістити Пости", "thread_tools.move_all": "Перемістити всі", - "thread_tools.change_owner": "Change Owner", + "thread_tools.change_owner": "Змінити Власника", "thread_tools.select_category": "Обрати Категорію", "thread_tools.fork": "Відгалужити тему", "thread_tools.delete": "Видалити тему", @@ -101,7 +101,7 @@ "delete_posts_instruction": "Тисніть пости які ви бажаєте видалити/стерти", "merge_topics_instruction": "Натисніть на теми, які потрібно об'єднати", "move_posts_instruction": "Натисніть на пости, які ви хочете перемістити", - "change_owner_instruction": "Click the posts you want to assign to another user", + "change_owner_instruction": "Клікніть на дописи які ви хочете призначити іншому користувачу", "composer.title_placeholder": "Уведіть заголовок теми...", "composer.handle_placeholder": "Ім'я", "composer.discard": "Скасувати", @@ -134,6 +134,6 @@ "diffs.no-revisions-description": "Цей пост має %1 версій.", "diffs.current-revision": "поточна ревізія", "diffs.original-revision": "початкова ревізія", - "timeago_later": "%1 later", - "timeago_earlier": "%1 earlier" + "timeago_later": "%1 пізніше", + "timeago_earlier": "%1 раніше" } \ No newline at end of file diff --git a/public/language/uk/user.json b/public/language/uk/user.json index 6186e36c2d..961edbaa4a 100644 --- a/public/language/uk/user.json +++ b/public/language/uk/user.json @@ -25,17 +25,17 @@ "profile_views": "Переглядів профілю", "reputation": "Репутація", "bookmarks": "Закладки", - "watched_categories": "Watched categories", - "change_all": "Change All", + "watched_categories": "Категорії, за якими ви спостерігаєте", + "change_all": "Змінити Всі", "watched": "Переглянуті", "ignored": "Ігнорується", - "default-category-watch-state": "Default category watch state", + "default-category-watch-state": "Спостереження за категоріями за замовчанням", "followers": "Відстежувачі", "following": "Відстежувані", "blocks": "Блокування", "block_toggle": "Увімкнути Блокування", - "block_user": "Block User", - "unblock_user": "Unblock User", + "block_user": "Заблокувати Користувача", + "unblock_user": "Розблокувати Користувача", "aboutme": "Про мене", "signature": "Підпис", "birthday": "День народження", @@ -50,7 +50,7 @@ "change_picture": "Змінити зображення", "change_username": "Змінити ім'я користувача", "change_email": "Змінити електронну пошту", - "email_same_as_password": "Please enter your current password to continue – you've entered your new email again", + "email_same_as_password": "Будь-ласка введіть ваш поточний пароль щоб продовжити – ви ввели ваш новий емейл знову", "edit": "Редагувати", "edit-profile": "Редагувати профіль", "default_picture": "Стандартна іконка", @@ -112,9 +112,9 @@ "no-sound": "Без звуку", "upvote-notif-freq": "Частота сповіщень позитивних відгуків", "upvote-notif-freq.all": "Всі позитивні відгуки", - "upvote-notif-freq.first": "First Per Post", + "upvote-notif-freq.first": "Перше в дописі", "upvote-notif-freq.everyTen": "Кожні 10 позитивних відгуків", - "upvote-notif-freq.threshold": "On 1, 5, 10, 25, 50, 100, 150, 200...", + "upvote-notif-freq.threshold": "На 1, 5, 10, 25, 50, 100, 150, 200...", "upvote-notif-freq.logarithmic": "На 10, 100, 1000...", "upvote-notif-freq.disabled": "Вимкнено", "browsing": "Налаштування перегляду", @@ -125,7 +125,7 @@ "follow_topics_you_reply_to": "Підписуватися на теми в котрих ви відповідаєте", "follow_topics_you_create": "Підписуватися на теми які ви створюєте", "grouptitle": "Заголовок групи", - "group-order-help": "Select a group and use the arrows to order titles", + "group-order-help": "Оберіть групу і використовуйте стрілки для зміни порядку заголовків", "no-group-title": "Немає заголовка групи", "select-skin": "Обрати стиль сайту", "select-homepage": "Обрати домашню сторінку", @@ -152,7 +152,7 @@ "info.moderation-note": "Коментар модератора", "info.moderation-note.success": "Коментар модератора збережено", "info.moderation-note.add": "Додати коментар", - "sessions.description": "This page allows you to view any active sessions on this forum and revoke them if necessary. You can revoke your own session by logging out of your account.", + "sessions.description": "Ця сторінка дозволяє вам переглядати будь-які активні сесії на цьому форумі та видаляти їх якщо потрібно. Ви можете видалити вашу власну сесію, якщо вийдете зі свого акаунта.", "consent.title": "Ваші Права & Згода", "consent.lead": "Цей форум збирає та обробляє вашу особисту інформацію.", "consent.intro": "Ми використовуємо цю інформацію виключно з метою персоналізації вашої активності у цій спільноті, а також для з'єднання ваших постів з вашим особистим акаунтом. На етапі реєстрації ми просили вас надати ім'я користувача та електронну пошту, також ви можете (необов'язково) надати нам додаткову інформацію, щоб завершити створення свого користувацького профілю на цьому сайті.

Ми зберігаємо цю інформацію протягом всього періоду життя вашого акаунту, і ви можете відкликати свою згоду у будь-який час, якщо видалите акаунт. У будь-який час ви можете отримати копію ваших особистих даних та внеску на цьому сайті через свою сторінку Права & Згода.

Якщо у вас виникли будь-які питання або зауваження, ми заохочуємо вас звернутись до команди Адміністраторів цього форуму.", diff --git a/public/language/uk/users.json b/public/language/uk/users.json index 6960968de0..c87acf2d44 100644 --- a/public/language/uk/users.json +++ b/public/language/uk/users.json @@ -10,7 +10,7 @@ "filter-by": "Фільтрувати за", "online-only": "Лише в мережі", "invite": "Запросити", - "prompt-email": "Emails:", + "prompt-email": "Емейли:", "invitation-email-sent": "Лист із запрошенням відправлено %1", "user_list": "Список користувачів", "recent_topics": "Нещодавні теми", diff --git a/public/language/vi/admin/general/dashboard.json b/public/language/vi/admin/general/dashboard.json index 42f034232e..0cbc260f5e 100644 --- a/public/language/vi/admin/general/dashboard.json +++ b/public/language/vi/admin/general/dashboard.json @@ -1,32 +1,32 @@ { - "forum-traffic": "Forum Traffic", + "forum-traffic": "Lưu lượng truy cập", "page-views": "Lượt xem trang", "unique-visitors": "Khách truy cập duy nhất", - "new-users": "New Users", + "new-users": "Người dùng mới", "posts": "Bài viết", "topics": "Chủ đề", "page-views-seven": "7 ngày trước", "page-views-thirty": "30 ngày trước", "page-views-last-day": "24 giờ trước", - "page-views-custom": "Custom Date Range", - "page-views-custom-start": "Range Start", - "page-views-custom-end": "Range End", - "page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is YYYY-MM-DD", - "page-views-custom-error": "Please enter a valid date range in the format YYYY-MM-DD", + "page-views-custom": "Tùy chỉnh phạm vi ngày", + "page-views-custom-start": "Phạm vi bắt đầu", + "page-views-custom-end": "Phạm vi kết thúc", + "page-views-custom-help": "Nhập phạm vi ngày của lượt xem trang bạn muốn xem. Nếu không có bộ chọn ngày, định dạng được chấp nhận là YYYY-MM-DD", + "page-views-custom-error": "Vui lòng nhập một phạm vi ngày hợp lệ trong định dạng YYYY-MM-DD", - "stats.yesterday": "Yesterday", - "stats.today": "Today", - "stats.last-week": "Last Week", - "stats.this-week": "This Week", - "stats.last-month": "Last Month", - "stats.this-month": "This Month", - "stats.all": "All Time", + "stats.yesterday": "Hôm qua", + "stats.today": "Hôm nay", + "stats.last-week": "Tuần trước", + "stats.this-week": "Tuần này", + "stats.last-month": "Tháng trước", + "stats.this-month": "Tháng này", + "stats.all": "Mọi lúc", - "updates": "Updates", - "running-version": "You are running NodeBB v%1.", - "keep-updated": "Always make sure that your NodeBB is up to date for the latest security patches and bug fixes.", - "up-to-date": "

You are up-to-date

", - "upgrade-available": "

A new version (v%1) has been released. Consider upgrading your NodeBB.

", + "updates": "Cập nhật", + "running-version": "Bạn đang chạy NodeBB v%1.", + "keep-updated": "Luôn đảm bảo rằng NodeBB của bạn được cập nhật cho các bản vá bảo mật và sửa lỗi mới nhất.", + "up-to-date": "

Bạn đang bản mới nhất

", + "upgrade-available": "

Phiên bản mới (v%1) đã được phát hành. Xem xét nâng cấp NodeBB của bạn.

", "prerelease-upgrade-available": "

This is an outdated pre-release version of NodeBB. A new version (v%1) has been released. Consider upgrading your NodeBB.

", "prerelease-warning": "

This is a pre-release version of NodeBB. Unintended bugs may occur.

", "running-in-development": "Forum is running in development mode. The forum may be open to potential vulnerabilities; please contact your system administrator.", @@ -39,7 +39,7 @@ "search-plugin-not-installed": "Search Plugin not installed", "search-plugin-tooltip": "Install a search plugin from the plugin page in order to activate search functionality", - "control-panel": "System Control", + "control-panel": "Điều khiển hệ thống", "rebuild-and-restart": "Rebuild & Restart", "restart": "Restart", "restart-warning": "Rebuilding or Restarting your NodeBB will drop all existing connections for a few seconds.", diff --git a/public/language/zh-CN/admin/extend/plugins.json b/public/language/zh-CN/admin/extend/plugins.json index 933e82c4b7..f5cdf59dbc 100644 --- a/public/language/zh-CN/admin/extend/plugins.json +++ b/public/language/zh-CN/admin/extend/plugins.json @@ -13,7 +13,7 @@ "reorder-plugins": "重新排序插件", "order-active": "排序生效插件", "dev-interested": "有兴趣为NodeBB开发插件?", - "docs-info": "有关插件创作的完整文档可以在 NodeBB 文档中找到。", + "docs-info": "有关插件创作的完整文档可以在 NodeBB 文档中找到。", "order.description": "部分插件需要在其它插件启用之后才能完美运作。", "order.explanation": "插件将按照以下顺序载入,从上至下。", diff --git a/public/language/zh-CN/admin/manage/users.json b/public/language/zh-CN/admin/manage/users.json index ad1cf75ff0..e84fc1f6ed 100644 --- a/public/language/zh-CN/admin/manage/users.json +++ b/public/language/zh-CN/admin/manage/users.json @@ -15,8 +15,8 @@ "delete": "删除用户", "purge": "删除用户和内容", "download-csv": "下载CSV", - "manage-groups": "Manage Groups", - "add-group": "Add Group", + "manage-groups": "管理用户组", + "add-group": "添加至群组", "invite": "邀请", "new": "新建用户", diff --git a/public/language/zh-CN/admin/settings/post.json b/public/language/zh-CN/admin/settings/post.json index b79f69e561..0b04390725 100644 --- a/public/language/zh-CN/admin/settings/post.json +++ b/public/language/zh-CN/admin/settings/post.json @@ -32,8 +32,8 @@ "timestamp": "时间戳", "timestamp.cut-off": "截止日期(天)", "timestamp.cut-off-help": "日期&时间将以相对方式 (例如,“3小时前” / “5天前”) 显示,并且会依照访客语言时区转换。在某一时刻之后,可以切换该文本以显示本地化日期本身 (例如2016年11月5日15:30) 。
(默认值: 30 或一个月) 。 设置为0可始终显示日期,留空以始终显示相对时间。", - "timestamp.necro-threshold": "Necro Threshold (in days)", - "timestamp.necro-threshold-help": "A message will be shown between posts if the time between them is longer than the necro threshold. (Default: 7, or one week). Set to 0 to disable.", + "timestamp.necro-threshold": "挖坟警告(单位:天)", + "timestamp.necro-threshold-help": "若进行回复的帖子最后回复的时间早于挖坟警告设定的天数,则在尝试回复前显示挖坟警告(默认:7天)。可以设置为 0 来禁用。", "teaser": "预览帖子", "teaser.last-post": "最后– 显示最新的帖子,包括原帖,如果没有回复", "teaser.last-reply": "最后– 显示最新回复,如果没有回复,则显示“无回复”占位符", diff --git a/public/language/zh-CN/category.json b/public/language/zh-CN/category.json index 5af7756f19..92032a39c2 100644 --- a/public/language/zh-CN/category.json +++ b/public/language/zh-CN/category.json @@ -1,7 +1,7 @@ { "category": "版块", "subcategories": "子版块", - "new_topic_button": "新主题", + "new_topic_button": "发表主题", "guest-login-post": "登录以发表", "no_topics": "此版块还没有任何内容。
赶紧来发帖吧!", "browsing": "正在浏览", diff --git a/public/language/zh-CN/email.json b/public/language/zh-CN/email.json index dede6a60e9..604ed32ab8 100644 --- a/public/language/zh-CN/email.json +++ b/public/language/zh-CN/email.json @@ -6,15 +6,15 @@ "greeting_no_name": "您好", "greeting_with_name": "%1,您好", "email.verify-your-email.subject": "请验证你的电子邮箱", - "email.verify.text1": "你的电子邮箱地址已经成功更改!", + "email.verify.text1": "你的电子邮箱地址已成功更改!", "welcome.text1": "感谢您注册 %1 帐户!", - "welcome.text2": "我们需要在校验您注册时填写的电子邮箱地址后,才能激活您的帐户。", - "welcome.text3": "管理员接受了您的注册请求,请用您的用户名和密码登陆。", + "welcome.text2": "在您验证您绑定的邮箱地址之后,您的账户才能激活。", + "welcome.text3": "管理员批准了您的注册申请,现在您可以登录您的账户了。", "welcome.cta": "点击这里确认您的电子邮箱地址", "invitation.text1": "%1 邀请您加入 %2", "invitation.text2": "您的邀请将在 %1 天后过期。", "invitation.cta": "点击这里新建账号", - "reset.text1": "可能由于您忘记了密码,我们收到了重置您帐户密码的申请。 如果您没有提交密码重置的请求,请忽略这封邮件。", + "reset.text1": "很可能是您忘记了密码,我们收到了重置您帐户密码的申请。 如果您没有申请密码重置,请忽略这封邮件。", "reset.text2": "如需继续重置密码,请点击下面的链接:", "reset.cta": "点击这里重置您的密码", "reset.notify.subject": "更改密码成功", @@ -43,10 +43,10 @@ "test.text1": "这是一封测试邮件,用来验证 NodeBB 的邮件配置是否设置正确。", "unsub.cta": "点击这里修改这些设置", "unsubscribe": "退订", - "unsub.success": "你将不再从%1邮寄名单接受邮件", - "banned.subject": "您已被封禁从 %1", - "banned.text1": "用户 %1 已被封禁从 %2.", - "banned.text2": "封禁将持续到 %1.", + "unsub.success": "您将不再收到来自%1邮寄名单的邮件", + "banned.subject": "您在 %1 的账户已被封禁", + "banned.text1": "您在 %2 的账户 %1 已被封禁。", + "banned.text2": "本次封禁将在 %1 结束。", "banned.text3": "这是您被封禁的原因:", "closing": "谢谢!" } \ No newline at end of file diff --git a/public/language/zh-CN/login.json b/public/language/zh-CN/login.json index e627a3ca1b..618b91327b 100644 --- a/public/language/zh-CN/login.json +++ b/public/language/zh-CN/login.json @@ -2,11 +2,11 @@ "username-email": "用户名 / 邮箱", "username": "用户名", "email": "邮件", - "remember_me": "记住我?", + "remember_me": "保持登录信息?", "forgot_password": "忘记密码?", "alternative_logins": "使用合作网站帐号登录", "failed_login_attempt": "登录失败", - "login_successful": "您已经成功登录!", + "login_successful": "您已成功登录!", "dont_have_account": "没有帐号?", "logged-out-due-to-inactivity": "由于长时间不活动,您的账号已被管理员从控制面板中注销" } \ No newline at end of file diff --git a/public/src/admin/general/dashboard.js b/public/src/admin/general/dashboard.js index a838397f11..4d617a7348 100644 --- a/public/src/admin/general/dashboard.js +++ b/public/src/admin/general/dashboard.js @@ -465,7 +465,7 @@ define('admin/general/dashboard', ['semver', 'Chart', 'translator', 'benchpress' // Update the View as JSON button url var apiEl = $('#view-as-json'); var newHref = $.param({ - units: units, + units: units || 'hours', until: until, count: amount, }); diff --git a/public/src/admin/manage/category.js b/public/src/admin/manage/category.js index 05ee5daef9..882a860683 100644 --- a/public/src/admin/manage/category.js +++ b/public/src/admin/manage/category.js @@ -227,6 +227,22 @@ define('admin/manage/category', [ $('button[data-action="setParent"]').removeClass('hide'); }); }); + $('button[data-action="toggle"]').on('click', function () { + var payload = {}; + var $this = $(this); + var disabled = $this.attr('data-disabled') === '1'; + payload[ajaxify.data.category.cid] = { + disabled: disabled ? 0 : 1, + }; + socket.emit('admin.categories.update', payload, function (err) { + if (err) { + return app.alertError(err.message); + } + $this.translateText(!disabled ? '[[admin/manage/categories:enable]]' : '[[admin/manage/categories:disable]]'); + $this.toggleClass('btn-primary', !disabled).toggleClass('btn-danger', disabled); + $this.attr('data-disabled', disabled ? 0 : 1); + }); + }); }; function modified(el) { diff --git a/public/src/admin/manage/group.js b/public/src/admin/manage/group.js index 17a0eaa99a..5d0b925bec 100644 --- a/public/src/admin/manage/group.js +++ b/public/src/admin/manage/group.js @@ -110,6 +110,7 @@ define('admin/manage/group', [ private: $('#group-private').is(':checked'), hidden: $('#group-hidden').is(':checked'), disableJoinRequests: $('#group-disableJoinRequests').is(':checked'), + disableLeave: $('#group-disableLeave').is(':checked'), }, }, function (err) { if (err) { diff --git a/public/src/admin/manage/users.js b/public/src/admin/manage/users.js index dd3554d11b..eef65cccab 100644 --- a/public/src/admin/manage/users.js +++ b/public/src/admin/manage/users.js @@ -308,6 +308,7 @@ define('admin/manage/users', ['translator', 'benchpress', 'autocomplete'], funct }, }); }); + return false; }); } diff --git a/public/src/app.js b/public/src/app.js index 82a2a8c478..f8a3a643ef 100644 --- a/public/src/app.js +++ b/public/src/app.js @@ -398,7 +398,7 @@ app.cacheBuster = null; } if (registerMessage) { $(document).ready(function () { - showAlert('register', decodeURIComponent(registerMessage)); + showAlert('register', utils.escapeHTML(decodeURIComponent(registerMessage))); registerMessage = false; }); } diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 4b1515f998..46ad454585 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -70,13 +70,10 @@ define('chat', [ roomData.silent = true; roomData.uid = app.user.uid; roomData.isSelf = isSelf; - module.createModal(roomData, function (modal) { + module.createModal(roomData, function () { if (!isSelf) { updateTitleAndPlaySound(data.message.mid, username); } - if (!modal) { - addMessageToModal(data); - } }); }); } @@ -87,7 +84,10 @@ define('chat', [ var username = data.message.fromUser.username; var isSelf = data.self === 1; require(['forum/chats/messages'], function (ChatsMessages) { - ChatsMessages.appendChatMessage(modal.find('.chat-content'), data.message); + // don't add if already added + if (!modal.find('[data-mid="' + data.message.messageId + '"]').length) { + ChatsMessages.appendChatMessage(modal.find('.chat-content'), data.message); + } if (modal.is(':visible')) { taskbar.updateActive(modal.attr('data-uuid')); @@ -145,7 +145,7 @@ define('chat', [ require(['scrollStop', 'forum/chats', 'forum/chats/messages'], function (scrollStop, Chats, ChatsMessages) { app.parseAndTranslate('chat', data, function (chatModal) { if (module.modalExists(data.roomId)) { - return callback(null); + return callback(module.getModal(data.roomId)); } var uuid = utils.generateUUID(); var dragged = false; diff --git a/public/src/modules/taskbar.js b/public/src/modules/taskbar.js index 701b14665d..ad4a31d4ce 100644 --- a/public/src/modules/taskbar.js +++ b/public/src/modules/taskbar.js @@ -155,7 +155,7 @@ define('taskbar', ['benchpress', 'translator'], function (Benchpress, translator var taskbarEl = $('
  • ') .addClass(data.options.className) - .html('' + + .html('' + (data.options.icon ? ' ' : '') + '' + title + '' + '') diff --git a/public/src/modules/translator.js b/public/src/modules/translator.js index 74d6b867c2..5c7b3366e2 100644 --- a/public/src/modules/translator.js +++ b/public/src/modules/translator.js @@ -361,7 +361,7 @@ var nodes = descendantTextNodes(element); var text = nodes.map(function (node) { - return node.nodeValue; + return utils.escapeHTML(node.nodeValue); }).join(' || '); var attrNodes = attributes.reduce(function (prev, attr) { diff --git a/public/src/overrides.js b/public/src/overrides.js index a0ae5af1ad..95288cf038 100644 --- a/public/src/overrides.js +++ b/public/src/overrides.js @@ -123,14 +123,16 @@ if (typeof window !== 'undefined') { $.timeago.settings.allowFuture = true; var userLang = config.userLang.replace('_', '-'); var options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; - var formatFn; - if (typeof Intl === 'undefined') { - formatFn = function (date) { - return date.toLocaleString(userLang, options); - }; - } else { - var dtFormat = new Intl.DateTimeFormat(userLang, options); - formatFn = dtFormat.format; + var formatFn = function (date) { + return date.toLocaleString(userLang, options); + }; + try { + if (typeof Intl !== 'undefined') { + var dtFormat = new Intl.DateTimeFormat(userLang, options); + formatFn = dtFormat.format; + } + } catch (err) { + console.error(err); } var iso; diff --git a/src/controllers/admin/dashboard.js b/src/controllers/admin/dashboard.js index b8d025c6ad..539147d2fb 100644 --- a/src/controllers/admin/dashboard.js +++ b/src/controllers/admin/dashboard.js @@ -8,7 +8,7 @@ const _ = require('lodash'); const versions = require('../../admin/versions'); const db = require('../../database'); const meta = require('../../meta'); -const analytics = require('../../analytics').async; +const analytics = require('../../analytics'); const plugins = require('../../plugins'); const user = require('../../user'); const utils = require('../../utils'); @@ -93,7 +93,7 @@ dashboardController.getAnalytics = async (req, res, next) => { } const method = req.query.units === 'days' ? analytics.getDailyStatsForSet : analytics.getHourlyStatsForSet; - let payload = await Promise.all(sets.map(async set => method('analytics:' + set, until, count))); + let payload = await Promise.all(sets.map(set => method('analytics:' + set, until, count))); payload = _.zipObject(sets, payload); res.json({ diff --git a/src/controllers/api.js b/src/controllers/api.js index 8bb0a2f7a7..7b9b404de1 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -86,8 +86,8 @@ apiController.loadConfig = async function (req) { config.usePagination = settings.usePagination; config.topicsPerPage = settings.topicsPerPage; config.postsPerPage = settings.postsPerPage; - config.userLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.userLang || config.defaultLang; - config.acpLang = (req.query.lang ? validator.escape(String(req.query.lang)) : null) || settings.acpLang; + config.userLang = validator.escape(String((req.query.lang ? req.query.lang : null) || settings.userLang || config.defaultLang)); + config.acpLang = validator.escape(String((req.query.lang ? req.query.lang : null) || settings.acpLang)); config.openOutgoingLinksInNewTab = settings.openOutgoingLinksInNewTab; config.topicPostSort = settings.topicPostSort || config.topicPostSort; config.categoryTopicSort = settings.categoryTopicSort || config.categoryTopicSort; diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index 56a0a11f20..bb3768169f 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -316,8 +316,12 @@ authenticationController.doLogin = async function (req, uid) { }; authenticationController.onSuccessfulLogin = async function (req, uid) { - // If already called once, return prematurely - if (req.res.locals.user) { + /* + * Older code required that this method be called from within the SSO plugin. + * That behaviour is no longer required, onSuccessfulLogin is now automatically + * called in NodeBB core. However, if already called, return prematurely + */ + if (req.loggedIn && !req.session.forceLogin) { return true; } diff --git a/src/controllers/composer.js b/src/controllers/composer.js index baec8b62f6..36dd1fac40 100644 --- a/src/controllers/composer.js +++ b/src/controllers/composer.js @@ -22,6 +22,10 @@ exports.get = async function (req, res, callback) { templateData: {}, }); + if (!data || !data.templateData) { + return callback(new Error('[[error:invalid-data]]')); + } + if (data.templateData.disabled) { res.render('', { title: '[[modules:composer.compose]]', diff --git a/src/controllers/topics.js b/src/controllers/topics.js index a42747c0d4..06957d1fae 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -166,9 +166,9 @@ async function buildBreadcrumbs(topicData) { } async function addTags(topicData, req, res) { - var postAtIndex = topicData.posts.find(p => parseInt(p.index, 10) === parseInt(Math.max(0, req.params.post_index - 1), 10)); - - var description = ''; + const postIndex = parseInt(req.params.post_index, 10) || 0; + const postAtIndex = topicData.posts.find(p => parseInt(p.index, 10) === parseInt(Math.max(0, postIndex - 1), 10)); + let description = ''; if (postAtIndex && postAtIndex.content) { description = utils.stripHTMLTags(utils.decodeHTMLEntities(postAtIndex.content)); } @@ -329,10 +329,10 @@ topicsController.pagination = async function (req, res, callback) { return helpers.notAllowed(req, res); } - var postCount = topic.postcount; - var pageCount = Math.max(1, Math.ceil(postCount / settings.postsPerPage)); + const postCount = topic.postcount; + const pageCount = Math.max(1, Math.ceil(postCount / settings.postsPerPage)); - var paginationData = pagination.create(currentPage, pageCount); + const paginationData = pagination.create(currentPage, pageCount); paginationData.rel.forEach(function (rel) { rel.href = nconf.get('url') + '/topic/' + topic.slug + rel.href; }); diff --git a/src/events.js b/src/events.js index 2a05ede75c..2217ca3b51 100644 --- a/src/events.js +++ b/src/events.js @@ -25,6 +25,7 @@ events.types = [ 'post-delete', 'post-restore', 'post-purge', + 'post-change-owner', 'topic-delete', 'topic-restore', 'topic-purge', diff --git a/src/file.js b/src/file.js index c8ccedb030..565f7b7f27 100644 --- a/src/file.js +++ b/src/file.js @@ -10,7 +10,6 @@ const graceful = require('graceful-fs'); const util = require('util'); const readdirAsync = util.promisify(fs.readdir); -const mkdirpAsync = util.promisify(mkdirp); const copyFileAsync = util.promisify(fs.copyFile); const writeFleAsync = util.promisify(fs.writeFile); const statAsync = util.promisify(fs.stat); @@ -33,7 +32,7 @@ file.saveFileToLocal = async function (filename, folder, tempPath) { const uploadPath = path.join(nconf.get('upload_path'), folder, filename); winston.verbose('Saving file ' + filename + ' to : ' + uploadPath); - await mkdirpAsync(path.dirname(uploadPath)); + await mkdirp(path.dirname(uploadPath)); await copyFileAsync(tempPath, uploadPath); return { url: '/assets/uploads/' + (folder ? folder + '/' : '') + filename, diff --git a/src/flags.js b/src/flags.js index 489ebbf9d9..6f698f6511 100644 --- a/src/flags.js +++ b/src/flags.js @@ -19,6 +19,16 @@ const utils = require('../public/src/utils'); const Flags = module.exports; +Flags._constants = { + states: ['open', 'wip', 'resolved', 'rejected'], + state_class: { + open: 'info', + wip: 'warning', + resolved: 'success', + rejected: 'danger', + }, +}; + Flags.init = async function () { // Query plugins for custom filter strategies and merge into core filter strategies function prepareSets(sets, orSets, prefix, value) { @@ -162,13 +172,7 @@ Flags.list = async function (filters, uid) { 'icon:text': userObj['icon:text'], }, }; - const stateToLabel = { - open: 'info', - wip: 'warning', - resolved: 'success', - rejected: 'danger', - }; - flagObj.labelClass = stateToLabel[flagObj.state]; + flagObj.labelClass = Flags._constants.state_class[flagObj.state]; return Object.assign(flagObj, { description: validator.escape(String(flagObj.description)), @@ -247,18 +251,21 @@ Flags.create = async function (type, id, uid, reason, timestamp) { timestamp = Date.now(); doHistoryAppend = true; } - const [exists, targetExists, targetUid, targetCid] = await Promise.all([ + const [flagExists, targetExists, canFlag, targetUid, targetCid] = await Promise.all([ // Sanity checks Flags.exists(type, id, uid), Flags.targetExists(type, id), + Flags.canFlag(type, id, uid), // Extra data for zset insertion Flags.getTargetUid(type, id), Flags.getTargetCid(type, id), ]); - if (exists) { + if (flagExists) { throw new Error('[[error:already-flagged]]'); } else if (!targetExists) { throw new Error('[[error:invalid-data]]'); + } else if (!canFlag) { + throw new Error('[[error:no-privileges]]'); } const flagId = await db.incrObjectField('global', 'nextFlagId'); @@ -303,6 +310,16 @@ Flags.exists = async function (type, id, uid) { return await db.isSortedSetMember('flags:hash', [type, id, uid].join(':')); }; +Flags.canFlag = async function (type, id, uid) { + if (type === 'user') { + return true; + } + if (type === 'post') { + return await privileges.posts.can('topics:read', id, uid); + } + throw new Error('[[error:invalid-data]]'); +}; + Flags.getTarget = async function (type, id, uid) { if (type === 'user') { const userData = await user.getUserData(id); @@ -344,6 +361,7 @@ Flags.getTargetCid = async function (type, id) { }; Flags.update = async function (flagId, uid, changeset) { + const current = await db.getObjectFields('flag:' + flagId, ['state', 'assignee', 'type', 'targetId']); const now = changeset.datetime || Date.now(); const notifyAssignee = async function (assigneeId) { if (assigneeId === '' || parseInt(uid, 10) === parseInt(assigneeId, 10)) { @@ -359,20 +377,40 @@ Flags.update = async function (flagId, uid, changeset) { }); await notifications.push(notifObj, [assigneeId]); }; + const isAssignable = async function (assigneeId) { + let allowed = false; + allowed = await user.isAdminOrGlobalMod(assigneeId); - // Retrieve existing flag data to compare for history-saving purposes - const current = await db.getObjectFields('flag:' + flagId, ['state', 'assignee']); + // Mods are also allowed to be assigned, if flag target is post in uid's moderated cid + if (!allowed && current.type === 'post') { + const cid = await posts.getCidByPid(current.targetId); + allowed = await user.isModerator(assigneeId, cid); + } + + return allowed; + }; + + // Retrieve existing flag data to compare for history-saving/reference purposes const tasks = []; for (var prop in changeset) { if (changeset.hasOwnProperty(prop)) { if (current[prop] === changeset[prop]) { delete changeset[prop]; } else if (prop === 'state') { - tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId)); - tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId)); + if (!Flags._constants.states.includes(changeset[prop])) { + delete changeset[prop]; + } else { + tasks.push(db.sortedSetAdd('flags:byState:' + changeset[prop], now, flagId)); + tasks.push(db.sortedSetRemove('flags:byState:' + current[prop], flagId)); + } } else if (prop === 'assignee') { - tasks.push(db.sortedSetAdd('flags:byAssignee:' + changeset[prop], now, flagId)); - tasks.push(notifyAssignee(changeset[prop])); + /* eslint-disable-next-line */ + if (!await isAssignable(parseInt(changeset[prop], 10))) { + delete changeset[prop]; + } else { + tasks.push(db.sortedSetAdd('flags:byAssignee:' + changeset[prop], now, flagId)); + tasks.push(notifyAssignee(changeset[prop])); + } } } } diff --git a/src/groups/create.js b/src/groups/create.js index 9a1f9c3954..76aec0afe0 100644 --- a/src/groups/create.js +++ b/src/groups/create.js @@ -16,7 +16,7 @@ module.exports = function (Groups) { const disableLeave = parseInt(data.disableLeave, 10) === 1 ? 1 : 0; const isHidden = parseInt(data.hidden, 10) === 1; - validateGroupName(data.name); + Groups.validateGroupName(data.name); const exists = await meta.userOrGroupExists(data.name); if (exists) { @@ -72,11 +72,15 @@ module.exports = function (Groups) { Groups.isPrivilegeGroup(data.name); } - function validateGroupName(name) { + Groups.validateGroupName = function (name) { if (!name) { throw new Error('[[error:group-name-too-short]]'); } + if (typeof name !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } + if (!Groups.isPrivilegeGroup(name) && name.length > meta.config.maximumGroupNameLength) { throw new Error('[[error:group-name-too-long]]'); } @@ -88,5 +92,5 @@ module.exports = function (Groups) { if (name.includes('/') || !utils.slugify(name)) { throw new Error('[[error:invalid-group-name]]'); } - } + }; }; diff --git a/src/groups/data.js b/src/groups/data.js index 21a5db4b56..19a1119b8e 100644 --- a/src/groups/data.js +++ b/src/groups/data.js @@ -53,6 +53,11 @@ module.exports = function (Groups) { return Array.isArray(groupsData) && groupsData[0] ? groupsData[0] : null; }; + Groups.getGroupField = async function (groupName, field) { + const groupData = await Groups.getGroupFields(groupName, [field]); + return groupData ? groupData[field] : null; + }; + Groups.getGroupFields = async function (groupName, fields) { const groups = await Groups.getGroupsFields([groupName], fields); return groups ? groups[0] : null; diff --git a/src/groups/index.js b/src/groups/index.js index c1422da25a..00abdf94df 100644 --- a/src/groups/index.js +++ b/src/groups/index.js @@ -78,7 +78,8 @@ Groups.getGroupsBySort = async function (sort, start, stop) { Groups.getNonPrivilegeGroups = async function (set, start, stop) { let groupNames = await db.getSortedSetRevRange(set, start, stop); groupNames = groupNames.concat(Groups.ephemeralGroups).filter(groupName => !Groups.isPrivilegeGroup(groupName)); - return await Groups.getGroupsData(groupNames); + const groupsData = await Groups.getGroupsData(groupNames); + return groupsData.filter(Boolean); }; Groups.getGroups = async function (set, start, stop) { diff --git a/src/groups/update.js b/src/groups/update.js index cc30cb8775..c7c0996557 100644 --- a/src/groups/update.js +++ b/src/groups/update.js @@ -54,7 +54,10 @@ module.exports = function (Groups) { payload.disableLeave = values.disableLeave ? '1' : '0'; } - await checkNameChange(groupName, values.name); + if (values.hasOwnProperty('name')) { + await checkNameChange(groupName, values.name); + } + if (values.hasOwnProperty('private')) { await updatePrivacy(groupName, values.private); } @@ -125,6 +128,10 @@ module.exports = function (Groups) { } async function checkNameChange(currentName, newName) { + Groups.validateGroupName(newName); + if (Groups.isPrivilegeGroup(newName)) { + throw new Error('[[error:invalid-group-name]]'); + } const currentSlug = utils.slugify(currentName); const newSlug = utils.slugify(newName); if (currentName === newName || currentSlug === newSlug) { diff --git a/src/messaging/create.js b/src/messaging/create.js index 4acd05a1b6..239aa54cfe 100644 --- a/src/messaging/create.js +++ b/src/messaging/create.js @@ -1,9 +1,9 @@ 'use strict'; -var meta = require('../meta'); -var plugins = require('../plugins'); -var db = require('../database'); -var user = require('../user'); +const meta = require('../meta'); +const plugins = require('../plugins'); +const db = require('../database'); +const user = require('../user'); module.exports = function (Messaging) { Messaging.sendMessage = async (data) => { @@ -21,7 +21,7 @@ module.exports = function (Messaging) { throw new Error('[[error:invalid-chat-message]]'); } - const maximumChatMessageLength = (meta.config.maximumChatMessageLength || 1000); + const maximumChatMessageLength = meta.config.maximumChatMessageLength || 1000; const data = await plugins.fireHook('filter:messaging.checkContent', { content: content }); content = String(data.content).trim(); if (!content) { diff --git a/src/messaging/data.js b/src/messaging/data.js index 982e51b746..21ea1e7d17 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -1,9 +1,11 @@ 'use strict'; -var db = require('../database'); -var user = require('../user'); -var utils = require('../utils'); -var plugins = require('../plugins'); +const validator = require('validator'); + +const db = require('../database'); +const user = require('../user'); +const utils = require('../utils'); +const plugins = require('../plugins'); const intFields = ['timestamp', 'edited', 'fromuid', 'roomId', 'deleted', 'system']; @@ -79,6 +81,7 @@ module.exports = function (Messaging) { messages = await Promise.all(messages.map(async (message) => { if (message.system) { + message.content = validator.escape(String(message.content)); return message; } diff --git a/src/messaging/delete.js b/src/messaging/delete.js index b7c1bbbd1b..9a4c551d2f 100644 --- a/src/messaging/delete.js +++ b/src/messaging/delete.js @@ -11,6 +11,6 @@ module.exports = function (Messaging) { throw new Error('[[error:chat-' + field + '-already]]'); } - return await Messaging.setMessageField(mid, 'deleted', state); + await Messaging.setMessageField(mid, 'deleted', state); } }; diff --git a/src/messaging/edit.js b/src/messaging/edit.js index 50af113f43..add728c416 100644 --- a/src/messaging/edit.js +++ b/src/messaging/edit.js @@ -1,9 +1,9 @@ 'use strict'; -var meta = require('../meta'); -var user = require('../user'); +const meta = require('../meta'); +const user = require('../user'); -var sockets = require('../socket.io'); +const sockets = require('../socket.io'); module.exports = function (Messaging) { @@ -57,18 +57,18 @@ module.exports = function (Messaging) { const [isAdmin, messageData] = await Promise.all([ user.isAdministrator(uid), - Messaging.getMessageFields(messageId, ['fromuid', 'timestamp']), + Messaging.getMessageFields(messageId, ['fromuid', 'timestamp', 'system']), ]); - if (isAdmin) { + if (isAdmin && !messageData.system) { return; } - var chatConfigDuration = meta.config[durationConfig]; + const chatConfigDuration = meta.config[durationConfig]; if (chatConfigDuration && Date.now() - messageData.timestamp > chatConfigDuration * 1000) { throw new Error('[[error:chat-' + type + '-duration-expired, ' + meta.config[durationConfig] + ']]'); } - if (messageData.fromuid === parseInt(uid, 10)) { + if (messageData.fromuid === parseInt(uid, 10) && !messageData.system) { return; } diff --git a/src/messaging/index.js b/src/messaging/index.js index 2bcad1445b..a29abfe733 100644 --- a/src/messaging/index.js +++ b/src/messaging/index.js @@ -139,14 +139,8 @@ Messaging.getRecentChats = async (callerUid, uid, start, stop) => { }); }; -Messaging.generateUsernames = (users, excludeUid) => { - users = users.filter(function (user) { - return user && parseInt(user.uid, 10) !== excludeUid; - }); - return users.map(function (user) { - return user.username; - }).join(', '); -}; +Messaging.generateUsernames = (users, excludeUid) => users.filter(user => user && parseInt(user.uid, 10) !== excludeUid) + .map(user => user.username).join(', '); Messaging.getTeaser = async (uid, roomId) => { const mid = await Messaging.getLatestUndeletedMessage(uid, roomId); diff --git a/src/messaging/rooms.js b/src/messaging/rooms.js index f2c93b18bc..5dbd834f03 100644 --- a/src/messaging/rooms.js +++ b/src/messaging/rooms.js @@ -20,10 +20,7 @@ module.exports = function (Messaging) { }; Messaging.getRoomsData = async (roomIds) => { - const roomData = await db.getObjects(roomIds.map(function (roomId) { - return 'chat:room:' + roomId; - })); - + const roomData = await db.getObjects(roomIds.map(roomId => 'chat:room:' + roomId)); modifyRoomData(roomData); return roomData; }; @@ -53,6 +50,7 @@ module.exports = function (Messaging) { db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid), ]); await Promise.all([ + Messaging.addSystemMessage('user-join', uid, roomId), // chat owner should also get the user-join system message Messaging.addUsersToRoom(uid, toUids, roomId), Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now), ]); @@ -61,7 +59,7 @@ module.exports = function (Messaging) { }; Messaging.isUserInRoom = async (uid, roomId) => { - const inRoom = db.isSortedSetMember('chat:room:' + roomId + ':uids', uid); + const inRoom = await db.isSortedSetMember('chat:room:' + roomId + ':uids', uid); const data = await plugins.fireHook('filter:messaging.isUserInRoom', { uid: uid, roomId: roomId, inRoom: inRoom }); return data.inRoom; }; @@ -113,6 +111,9 @@ module.exports = function (Messaging) { }; Messaging.leaveRoom = async (uids, roomId) => { + const isInRoom = await Promise.all(uids.map(uid => Messaging.isUserInRoom(uid, roomId))); + uids = uids.filter((uid, index) => isInRoom[index]); + const keys = uids .map(uid => 'uid:' + uid + ':chat:rooms') .concat(uids.map(uid => 'uid:' + uid + ':chat:rooms:unread')); @@ -127,6 +128,9 @@ module.exports = function (Messaging) { }; Messaging.leaveRooms = async (uid, roomIds) => { + const isInRoom = await Promise.all(roomIds.map(roomId => Messaging.isUserInRoom(uid, roomId))); + roomIds = roomIds.filter((roomId, index) => isInRoom[index]); + const roomKeys = roomIds.map(roomId => 'chat:room:' + roomId + ':uids'); await Promise.all([ db.sortedSetsRemove(roomKeys, uid), @@ -192,7 +196,7 @@ module.exports = function (Messaging) { }; Messaging.canReply = async (roomId, uid) => { - const inRoom = db.isSortedSetMember('chat:room:' + roomId + ':uids', uid); + const inRoom = await db.isSortedSetMember('chat:room:' + roomId + ':uids', uid); const data = await plugins.fireHook('filter:messaging.canReply', { uid: uid, roomId: roomId, inRoom: inRoom, canReply: inRoom }); return data.canReply; }; diff --git a/src/meta/blacklist.js b/src/meta/blacklist.js index ae3412832d..2c7ddf4678 100644 --- a/src/meta/blacklist.js +++ b/src/meta/blacklist.js @@ -3,6 +3,7 @@ const ipaddr = require('ipaddr.js'); const winston = require('winston'); const _ = require('lodash'); +const validator = require('validator'); const db = require('../database'); const pubsub = require('../pubsub'); @@ -128,7 +129,7 @@ Blacklist.validate = function (rules) { } if (!addr || whitelist.includes(rule)) { - invalid.push(rule); + invalid.push(validator.escape(rule)); return false; } diff --git a/src/meta/build.js b/src/meta/build.js index 0a99bd5837..f9370d1b9f 100644 --- a/src/meta/build.js +++ b/src/meta/build.js @@ -1,5 +1,6 @@ 'use strict'; +const os = require('os'); const async = require('async'); const winston = require('winston'); const nconf = require('nconf'); @@ -150,7 +151,14 @@ exports.build = function (targets, options, callback) { targets = targets.split(','); } - var parallel = !nconf.get('series') && !options.series; + let series = nconf.get('series') || options.series; + if (series === undefined) { + // Detect # of CPUs and select strategy as appropriate + winston.verbose('[build] Querying CPU core count for build strategy'); + const cpus = os.cpus(); + series = cpus.length < 4; + winston.verbose('[build] System returned ' + cpus.length + ' cores, opting for ' + (series ? 'series' : 'parallel') + ' build strategy'); + } targets = targets // get full target name @@ -195,14 +203,14 @@ exports.build = function (targets, options, callback) { require('./minifier').maxThreads = threads - 1; } - if (parallel) { + if (!series) { winston.info('[build] Building in parallel mode'); } else { winston.info('[build] Building in series mode'); } startTime = Date.now(); - buildTargets(targets, parallel, next); + buildTargets(targets, !series, next); }, function (next) { totalTime = (Date.now() - startTime) / 1000; diff --git a/src/meta/cacheBuster.js b/src/meta/cacheBuster.js index 68fa8f9e97..d5fa5680a7 100644 --- a/src/meta/cacheBuster.js +++ b/src/meta/cacheBuster.js @@ -5,7 +5,6 @@ const path = require('path'); const mkdirp = require('mkdirp'); const winston = require('winston'); const util = require('util'); -const mkdirpAsync = util.promisify(mkdirp); const writeFileAsync = util.promisify(fs.writeFile); const readFileAsync = util.promisify(fs.readFile); @@ -19,7 +18,7 @@ function generate() { } exports.write = async function write() { - await mkdirpAsync(path.dirname(filePath)); + await mkdirp(path.dirname(filePath)); await writeFileAsync(filePath, generate()); }; diff --git a/src/meta/js.js b/src/meta/js.js index f63799404d..aa329022d9 100644 --- a/src/meta/js.js +++ b/src/meta/js.js @@ -3,7 +3,16 @@ var path = require('path'); var async = require('async'); var fs = require('fs'); +const util = require('util'); var mkdirp = require('mkdirp'); +var mkdirpCallback; +if (mkdirp.hasOwnProperty('native')) { + mkdirpCallback = util.callbackify(mkdirp); +} else { + mkdirpCallback = mkdirp; + mkdirp = util.promisify(mkdirp); +} + var rimraf = require('rimraf'); var file = require('../file'); @@ -119,7 +128,7 @@ function minifyModules(modules, fork, callback) { return prev; }, []); - async.each(moduleDirs, mkdirp, function (err) { + async.each(moduleDirs, mkdirpCallback, function (err) { if (err) { return callback(err); } @@ -156,7 +165,7 @@ function linkModules(callback) { async.parallel({ dir: function (cb) { - mkdirp(path.dirname(destPath), function (err) { + mkdirpCallback(path.dirname(destPath), function (err) { cb(err); }); }, @@ -272,7 +281,7 @@ JS.linkStatics = function (callback) { var sourceDir = plugins.staticDirs[mappedPath]; var destDir = path.join(__dirname, '../../build/public/plugins', mappedPath); - mkdirp(path.dirname(destDir), function (err) { + mkdirpCallback(path.dirname(destDir), function (err) { if (err) { return next(err); } @@ -343,7 +352,7 @@ JS.buildBundle = function (target, fork, callback) { getBundleScriptList(target, next); }, function (files, next) { - mkdirp(path.join(__dirname, '../../build/public'), function (err) { + mkdirpCallback(path.join(__dirname, '../../build/public'), function (err) { next(err, files); }); }, diff --git a/src/meta/languages.js b/src/meta/languages.js index 61770c0749..7f4afd0a02 100644 --- a/src/meta/languages.js +++ b/src/meta/languages.js @@ -2,12 +2,12 @@ const path = require('path'); const fs = require('fs'); -const mkdirp = require('mkdirp'); +const util = require('util'); +let mkdirp = require('mkdirp'); +mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp); const rimraf = require('rimraf'); const _ = require('lodash'); -const util = require('util'); -const mkdirpAsync = util.promisify(mkdirp); const rimrafAsync = util.promisify(rimraf); const writeFileAsync = util.promisify(fs.writeFile); const readFileAsync = util.promisify(fs.readFile); @@ -46,7 +46,7 @@ async function getTranslationMetadata() { // save a list of languages to `${buildLanguagesPath}/metadata.json` // avoids readdirs later on - await mkdirpAsync(buildLanguagesPath); + await mkdirp(buildLanguagesPath); const result = { languages: languages, namespaces: namespaces, @@ -59,7 +59,7 @@ async function writeLanguageFile(language, namespace, translations) { const dev = global.env === 'development'; const filePath = path.join(buildLanguagesPath, language, namespace + '.json'); - await mkdirpAsync(path.dirname(filePath)); + await mkdirp(path.dirname(filePath)); await writeFileAsync(filePath, JSON.stringify(translations, null, dev ? 2 : 0)); } diff --git a/src/meta/sounds.js b/src/meta/sounds.js index 1e83151da8..3c6fb6eff3 100644 --- a/src/meta/sounds.js +++ b/src/meta/sounds.js @@ -2,14 +2,14 @@ const path = require('path'); const fs = require('fs'); -const rimraf = require('rimraf'); -const mkdirp = require('mkdirp'); - const util = require('util'); +const rimraf = require('rimraf'); +let mkdirp = require('mkdirp'); +mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp); + const readdirAsync = util.promisify(fs.readdir); const rimrafAsync = util.promisify(rimraf); -const mkdirpAsync = util.promisify(mkdirp); const writeFileAsync = util.promisify(fs.writeFile); const file = require('../file'); @@ -70,7 +70,7 @@ Sounds.build = async function build() { map.unshift({}); map = Object.assign.apply(null, map); await rimrafAsync(soundsPath); - await mkdirpAsync(soundsPath); + await mkdirp(soundsPath); await writeFileAsync(path.join(soundsPath, 'fileMap.json'), JSON.stringify(map)); diff --git a/src/meta/templates.js b/src/meta/templates.js index 7397f6c9ea..e3f5005d78 100644 --- a/src/meta/templates.js +++ b/src/meta/templates.js @@ -1,11 +1,11 @@ 'use strict'; -const mkdirp = require('mkdirp'); +const util = require('util'); +let mkdirp = require('mkdirp'); +mkdirp = mkdirp.hasOwnProperty('native') ? mkdirp : util.promisify(mkdirp); const rimraf = require('rimraf'); const winston = require('winston'); const path = require('path'); - -const util = require('util'); const fs = require('fs'); const fsReadFile = util.promisify(fs.readFile); const fsWriteFile = util.promisify(fs.writeFile); @@ -123,10 +123,9 @@ Templates.compileTemplate = compileTemplate; async function compile() { const _rimraf = util.promisify(rimraf); - const _mkdirp = util.promisify(mkdirp); await _rimraf(viewsPath); - await _mkdirp(viewsPath); + await mkdirp(viewsPath); let files = await db.getSortedSetRange('plugins:active', 0, -1); files = await getTemplateDirs(files); @@ -137,7 +136,7 @@ async function compile() { let imported = await fsReadFile(filePath, 'utf8'); imported = await processImports(files, name, imported); - await _mkdirp(path.join(viewsPath, path.dirname(name))); + await mkdirp(path.join(viewsPath, path.dirname(name))); await fsWriteFile(path.join(viewsPath, name), imported); const compiled = await Benchpress.precompile(imported, { minify: global.env !== 'development' }); diff --git a/src/plugins/hooks.js b/src/plugins/hooks.js index 3ad98a809f..7f9e303857 100644 --- a/src/plugins/hooks.js +++ b/src/plugins/hooks.js @@ -148,40 +148,40 @@ module.exports = function (Plugins) { if (!Array.isArray(hookList) || !hookList.length) { return; } + // don't bubble errors from these hooks, so bad plugins don't stop startup + const noErrorHooks = ['static:app.load', 'static:assets.prepare', 'static:app.preload']; await async.each(hookList, function (hookObj, next) { - if (typeof hookObj.method === 'function') { - let timedOut = false; - const timeoutId = setTimeout(function () { - winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\''); - timedOut = true; - next(); - }, 5000); + if (typeof hookObj.method !== 'function') { + return next(); + } - const onError = (err) => { - winston.error('[plugins] Error executing \'' + hook + '\' in plugin \'' + hookObj.id + '\''); - winston.error(err); - clearTimeout(timeoutId); - next(); - }; - const callback = (...args) => { - clearTimeout(timeoutId); - if (!timedOut) { - next(...args); - } - }; - try { - const returned = hookObj.method(params, callback); - if (utils.isPromise(returned)) { - returned.then( - payload => setImmediate(callback, null, payload), - err => setImmediate(onError, err) - ); - } - } catch (err) { - onError(err); - } - } else { + let timedOut = false; + const timeoutId = setTimeout(function () { + winston.warn('[plugins] Callback timed out, hook \'' + hook + '\' in plugin \'' + hookObj.id + '\''); + timedOut = true; next(); + }, 5000); + + const callback = (err) => { + clearTimeout(timeoutId); + if (err) { + winston.error('[plugins] Error executing \'' + hook + '\' in plugin \'' + hookObj.id + '\''); + winston.error(err.stack); + } + if (!timedOut) { + next(noErrorHooks.includes(hook) ? null : err); + } + }; + try { + const returned = hookObj.method(params, callback); + if (utils.isPromise(returned)) { + returned.then( + payload => setImmediate(callback, null, payload), + err => setImmediate(callback, err) + ); + } + } catch (err) { + callback(err); } }); } diff --git a/src/posts/user.js b/src/posts/user.js index 9205bed2f4..49706ec59b 100644 --- a/src/posts/user.js +++ b/src/posts/user.js @@ -123,7 +123,7 @@ module.exports = function (Posts) { }; Posts.changeOwner = async function (pids, toUid) { - const exists = user.exists(toUid); + const exists = await user.exists(toUid); if (!exists) { throw new Error('[[error:no-user]]'); } @@ -163,6 +163,7 @@ module.exports = function (Posts) { reduceCounters(postsByUser), updateTopicPosters(postData, toUid), ]); + return postData; }; async function reduceCounters(postsByUser) { diff --git a/src/privileges/categories.js b/src/privileges/categories.js index 397796b4a4..6f380b6563 100644 --- a/src/privileges/categories.js +++ b/src/privileges/categories.js @@ -110,6 +110,7 @@ module.exports = function (privileges) { return await utils.promiseParallel({ categories: categories.getCategoriesFields(cids, ['disabled']), allowedTo: helpers.isUserAllowedTo(privilege, uid, cids), + view_deleted: helpers.isUserAllowedTo('posts:view_deleted', uid, cids), isAdmin: user.isAdministrator(uid), }); }; diff --git a/src/privileges/posts.js b/src/privileges/posts.js index f8d824f943..ac595c0c31 100644 --- a/src/privileges/posts.js +++ b/src/privileges/posts.js @@ -88,16 +88,17 @@ module.exports = function (privileges) { cids = _.uniq(cids); const results = await privileges.categories.getBase(privilege, cids, uid); - cids = cids.filter(function (cid, index) { + const allowedCids = cids.filter(function (cid, index) { return !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin); }); - const cidsSet = new Set(cids); + const cidsSet = new Set(allowedCids); + const canViewDeleted = _.zipObject(cids, results.view_deleted); pids = postData.filter(function (post) { return post.topic && cidsSet.has(post.topic.cid) && - ((!post.topic.deleted && !post.deleted) || results.isAdmin); + ((!post.topic.deleted && !post.deleted) || canViewDeleted[post.topic.cid] || results.isAdmin); }).map(post => post.pid); const data = await plugins.fireHook('filter:privileges.posts.filter', { diff --git a/src/privileges/topics.js b/src/privileges/topics.js index af9e57698d..d4b34194ff 100644 --- a/src/privileges/topics.js +++ b/src/privileges/topics.js @@ -68,14 +68,15 @@ module.exports = function (privileges) { } const topicsData = await topics.getTopicsFields(tids, ['tid', 'cid', 'deleted']); - let cids = _.uniq(topicsData.map(topic => topic.cid)); + const cids = _.uniq(topicsData.map(topic => topic.cid)); const results = await privileges.categories.getBase(privilege, cids, uid); - cids = cids.filter((cid, index) => !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); + const allowedCids = cids.filter((cid, index) => !results.categories[index].disabled && (results.allowedTo[index] || results.isAdmin)); - const cidsSet = new Set(cids); + const cidsSet = new Set(allowedCids); + const canViewDeleted = _.zipObject(cids, results.view_deleted); - tids = topicsData.filter(t => cidsSet.has(t.cid) && (!t.deleted || results.isAdmin)).map(t => t.tid); + tids = topicsData.filter(t => cidsSet.has(t.cid) && (!t.deleted || canViewDeleted[t.cid] || results.isAdmin)).map(t => t.tid); const data = await plugins.fireHook('filter:privileges.topics.filter', { privilege: privilege, @@ -115,7 +116,7 @@ module.exports = function (privileges) { }; privileges.topics.canDelete = async function (tid, uid) { - const topicData = await topics.getTopicFields(tid, ['cid', 'postcount']); + const topicData = await topics.getTopicFields(tid, ['uid', 'cid', 'postcount', 'deleterUid']); const [isModerator, isAdministrator, isOwner, allowedTo] = await Promise.all([ user.isModerator(uid, topicData.cid), user.isAdministrator(uid), @@ -135,7 +136,8 @@ module.exports = function (privileges) { throw new Error(langKey); } - return allowedTo[0] && (isOwner || isModerator); + const deleterUid = topicData.deleterUid; + return allowedTo[0] && ((isOwner && (deleterUid === 0 || deleterUid === topicData.uid)) || isModerator); }; privileges.topics.canEdit = async function (tid, uid) { diff --git a/src/socket.io/flags.js b/src/socket.io/flags.js index 90115e470e..e2c9a56215 100644 --- a/src/socket.io/flags.js +++ b/src/socket.io/flags.js @@ -21,7 +21,7 @@ SocketFlags.create = async function (socket, data) { const flagObj = await flags.create(data.type, data.id, socket.uid, data.reason); await flags.notify(flagObj, socket.uid); - return flagObj; + return flagObj.flagId; }; SocketFlags.update = async function (socket, data) { diff --git a/src/socket.io/groups.js b/src/socket.io/groups.js index b00af300a6..7b8e383a85 100644 --- a/src/socket.io/groups.js +++ b/src/socket.io/groups.js @@ -22,6 +22,10 @@ SocketGroups.join = async (socket, data) => { throw new Error('[[error:invalid-uid]]'); } + if (typeof data.groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } + if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) { throw new Error('[[error:not-allowed]]'); } @@ -66,6 +70,10 @@ SocketGroups.leave = async (socket, data) => { throw new Error('[[error:invalid-uid]]'); } + if (typeof data.groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } + if (data.groupName === 'administrators') { throw new Error('[[error:cant-remove-self-as-admin]]'); } @@ -104,6 +112,9 @@ SocketGroups.addMember = async (socket, data) => { }; async function isOwner(socket, data) { + if (typeof data.groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } const results = await utils.promiseParallel({ isAdmin: await user.isAdministrator(socket.uid), isGlobalModerator: await user.isGlobalModerator(socket.uid), @@ -118,6 +129,9 @@ async function isOwner(socket, data) { } async function isInvited(socket, data) { + if (typeof data.groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } const invited = await groups.isInvited(socket.uid, data.groupName); if (!invited) { throw new Error('[[error:not-invited]]'); @@ -171,6 +185,9 @@ SocketGroups.rejectAll = async (socket, data) => { }; async function acceptRejectAll(method, socket, data) { + if (typeof data.groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } const uids = await groups.getPending(data.groupName); await Promise.all(uids.map(async (uid) => { await method(socket, { groupName: data.groupName, toUid: uid }); @@ -251,7 +268,7 @@ SocketGroups.kick = async (socket, data) => { SocketGroups.create = async (socket, data) => { if (!socket.uid) { throw new Error('[[error:no-privileges]]'); - } else if (groups.isPrivilegeGroup(data.name)) { + } else if (typeof data.name !== 'string' || groups.isPrivilegeGroup(data.name)) { throw new Error('[[error:invalid-group-name]]'); } @@ -260,6 +277,7 @@ SocketGroups.create = async (socket, data) => { throw new Error('[[error:no-privileges]]'); } data.ownerUid = socket.uid; + data.system = false; const groupData = await groups.create(data); logGroupEvent(socket, 'group-create', { groupName: data.name, @@ -338,7 +356,6 @@ SocketGroups.cover.update = async (socket, data) => { if (!socket.uid) { throw new Error('[[error:no-privileges]]'); } - await canModifyGroup(socket.uid, data.groupName); return await groups.updateCover(socket.uid, data); }; @@ -353,12 +370,17 @@ SocketGroups.cover.remove = async (socket, data) => { }; async function canModifyGroup(uid, groupName) { + if (typeof groupName !== 'string') { + throw new Error('[[error:invalid-group-name]]'); + } const results = await utils.promiseParallel({ isOwner: groups.ownership.isOwner(uid, groupName), - isAdminOrGlobalMod: user.isAdminOrGlobalMod(uid), + system: groups.getGroupField(groupName, 'system'), + isAdmin: user.isAdministrator(uid), + isGlobalMod: user.isGlobalModerator(uid), }); - if (!results.isOwner && !results.isAdminOrGlobalMod) { + if (!(results.isOwner || results.isAdmin || (results.isGlobalMod && !results.system))) { throw new Error('[[error:no-privileges]]'); } } diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 38814e49a1..f44da3ad68 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -113,11 +113,14 @@ SocketModules.chats.getUsersInRoom = async function (socket, data) { if (!data || !data.roomId) { throw new Error('[[error:invalid-data]]'); } - const [userData, isOwner] = await Promise.all([ - Messaging.getUsersInRoom(data.roomId, 0, -1), + const [isUserInRoom, isOwner, userData] = await Promise.all([ + Messaging.isUserInRoom(socket.uid, data.roomId), Messaging.isRoomOwner(socket.uid, data.roomId), + Messaging.getUsersInRoom(data.roomId, 0, -1), ]); - + if (!isUserInRoom) { + throw new Error('[[error:no-privileges]]'); + } userData.forEach((user) => { user.canKick = (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)) && isOwner; }); diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index 7723320d5a..9c3ee0af49 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -167,11 +167,22 @@ module.exports = function (SocketPosts) { if (!data || !Array.isArray(data.pids) || !data.toUid) { throw new Error('[[error:invalid-data]]'); } - const isAdminOrGlobalMod = user.isAdminOrGlobalMod(socket.uid); + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); if (!isAdminOrGlobalMod) { throw new Error('[[error:no-privileges]]'); } - await posts.changeOwner(data.pids, data.toUid); + var postData = await posts.changeOwner(data.pids, data.toUid); + var logs = postData.map(({ pid, uid, cid }) => (events.log({ + type: 'post-change-owner', + uid: socket.uid, + ip: socket.ip, + targetUid: data.toUid, + pid: pid, + originalUid: uid, + cid: cid, + }))); + + await Promise.all(logs); }; }; diff --git a/src/topics/data.js b/src/topics/data.js index de5677e16b..44625c47f2 100644 --- a/src/topics/data.js +++ b/src/topics/data.js @@ -11,6 +11,7 @@ const intFields = [ 'tid', 'cid', 'uid', 'mainPid', 'postcount', 'viewcount', 'deleted', 'locked', 'pinned', 'timestamp', 'upvotes', 'downvotes', 'lastposttime', + 'deleterUid', ]; module.exports = function (Topics) { @@ -90,6 +91,10 @@ function modifyTopic(topic, fields) { escapeTitle(topic); + if (topic.hasOwnProperty('thumb')) { + topic.thumb = validator.escape(String(topic.thumb)); + } + if (topic.hasOwnProperty('timestamp')) { topic.timestampISO = utils.toISOString(topic.timestamp); } diff --git a/src/topics/index.js b/src/topics/index.js index 7473f5754a..57f738b282 100644 --- a/src/topics/index.js +++ b/src/topics/index.js @@ -50,8 +50,7 @@ Topics.getTopics = async function (tids, options) { } tids = await privileges.topics.filterTids('topics:read', tids, uid); - const topics = await Topics.getTopicsByTids(tids, options); - return topics; + return await Topics.getTopicsByTids(tids, options); }; Topics.getTopicsByTids = async function (tids, options) { diff --git a/src/user/create.js b/src/user/create.js index 64aa8bdbaa..2edc676c33 100644 --- a/src/user/create.js +++ b/src/user/create.js @@ -127,7 +127,7 @@ module.exports = function (User) { }; User.isPasswordValid = function (password, minStrength) { - minStrength = minStrength || meta.config.minimumPasswordStrength; + minStrength = (minStrength || minStrength === 0) ? minStrength : meta.config.minimumPasswordStrength; // Sanity checks: Checks if defined and is string if (!password || !utils.isPasswordValid(password)) { diff --git a/src/user/online.js b/src/user/online.js index 3f07cd619d..05c40464f7 100644 --- a/src/user/online.js +++ b/src/user/online.js @@ -30,11 +30,8 @@ module.exports = function (User) { const now = Date.now(); const isArray = Array.isArray(uid); uid = isArray ? uid : [uid]; - const lastonline = db.sortedSetScores('users:online', uid); - const isOnline = uid.map(function (uid, index) { - return (now - lastonline[index]) < (meta.config.onlineCutoff * 60000); - }); - + const lastonline = await db.sortedSetScores('users:online', uid); + const isOnline = uid.map((uid, index) => (now - lastonline[index]) < (meta.config.onlineCutoff * 60000)); return isArray ? isOnline : isOnline[0]; }; }; diff --git a/src/user/password.js b/src/user/password.js index a4bfc161a5..e5a48fc7b1 100644 --- a/src/user/password.js +++ b/src/user/password.js @@ -23,7 +23,7 @@ module.exports = function (User) { hashedPassword = ''; } - User.isPasswordValid(password); + User.isPasswordValid(password, 0); await User.auth.logAttempt(uid, ip); const ok = await Password.compare(password, hashedPassword); if (ok) { diff --git a/src/user/settings.js b/src/user/settings.js index c9223c8cd2..0a1ca94839 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -1,10 +1,13 @@ 'use strict'; +const validator = require('validator'); + const meta = require('../meta'); const db = require('../database'); const plugins = require('../plugins'); const notifications = require('../notifications'); +const languages = require('../languages'); module.exports = function (User) { User.getSettings = async function (uid) { @@ -55,7 +58,8 @@ module.exports = function (User) { settings.upvoteNotifFreq = getSetting(settings, 'upvoteNotifFreq', 'all'); settings.restrictChat = parseInt(getSetting(settings, 'restrictChat', 0), 10) === 1; settings.topicSearchEnabled = parseInt(getSetting(settings, 'topicSearchEnabled', 0), 10) === 1; - settings.bootswatchSkin = settings.bootswatchSkin || ''; + settings.bootswatchSkin = validator.escape(String(settings.bootswatchSkin || '')); + settings.homePageRoute = validator.escape(String(settings.homePageRoute || '')); settings.scrollToMyPost = parseInt(getSetting(settings, 'scrollToMyPost', 1), 10) === 1; settings.categoryWatchState = getSetting(settings, 'categoryWatchState', 'notwatching'); @@ -87,6 +91,13 @@ module.exports = function (User) { throw new Error('[[error:invalid-pagination-value, 2, ' + maxTopicsPerPage + ']]'); } + const languageCodes = await languages.listCodes(); + if (data.userLang && !languageCodes.includes(data.userLang)) { + throw new Error('[[error:invalid-language]]'); + } + if (data.acpLang && !languageCodes.includes(data.acpLang)) { + throw new Error('[[error:invalid-language]]'); + } data.userLang = data.userLang || meta.config.defaultLang; plugins.fireHook('action:user.saveSettings', { uid: uid, settings: data }); diff --git a/src/user/uploads.js b/src/user/uploads.js index 9ba5efc9ca..aa77df6e9d 100644 --- a/src/user/uploads.js +++ b/src/user/uploads.js @@ -18,10 +18,14 @@ module.exports = function (User) { throw new Error('[[error:no-privileges]]'); } + const finalPath = path.join(nconf.get('upload_path'), uploadName); + if (!finalPath.startsWith(nconf.get('upload_path'))) { + throw new Error('[[error:invalid-path]]'); + } winston.verbose('[user/deleteUpload] Deleting ' + uploadName); await Promise.all([ - file.delete(path.join(nconf.get('upload_path'), uploadName)), - file.delete(path.join(nconf.get('upload_path'), path.dirname(uploadName), path.basename(uploadName, path.extname(uploadName)) + '-resized' + path.extname(uploadName))), + file.delete(finalPath), + file.delete(file.appendToFileName(finalPath, '-resized')), ]); await db.sortedSetRemove('uid:' + uid + ':uploads', uploadName); }; diff --git a/src/views/admin/manage/category.tpl b/src/views/admin/manage/category.tpl index 89adea01f3..178b55a605 100644 --- a/src/views/admin/manage/category.tpl +++ b/src/views/admin/manage/category.tpl @@ -161,6 +161,13 @@ [[admin/manage/categories:copy-settings]]
    + diff --git a/src/views/admin/partials/menu.tpl b/src/views/admin/partials/menu.tpl index d08c1eb90f..b236cf99de 100644 --- a/src/views/admin/partials/menu.tpl +++ b/src/views/admin/partials/menu.tpl @@ -257,6 +257,7 @@
  • {{{end}}} {{{end}}} +
  • [[admin/menu:extend/plugins.install]] diff --git a/src/webserver.js b/src/webserver.js index 54e09e5424..dc506c45ff 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -164,7 +164,9 @@ function setupExpressApp(app) { saveUninitialized: nconf.get('sessionSaveUninitialized') || false, })); - app.use(helmet()); + app.use(helmet({ + hsts: !!meta.config['hsts-enabled'], + })); app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' })); if (meta.config['hsts-enabled']) { app.use(helmet.hsts({ diff --git a/test/build.js b/test/build.js index f3dea9a468..f55aa81b38 100644 --- a/test/build.js +++ b/test/build.js @@ -11,8 +11,8 @@ var db = require('./mocks/databasemock'); var file = require('../src/file'); describe('minifier', function () { - before(function (done) { - mkdirp(path.join(__dirname, '../build/test'), done); + before(async function () { + await mkdirp(path.join(__dirname, '../build/test')); }); var minifier = require('../src/meta/minifier'); diff --git a/test/controllers-admin.js b/test/controllers-admin.js index 476bd65ad4..371685866f 100644 --- a/test/controllers-admin.js +++ b/test/controllers-admin.js @@ -684,10 +684,10 @@ describe('Admin Controllers', function () { var socketFlags = require('../src/socket.io/flags'); var oldValue = meta.config['min:rep:flag']; meta.config['min:rep:flag'] = 0; - socketFlags.create({ uid: regularUid }, { id: pid, type: 'post', reason: 'spam' }, function (err, data) { + socketFlags.create({ uid: regularUid }, { id: pid, type: 'post', reason: 'spam' }, function (err, flagId) { meta.config['min:rep:flag'] = oldValue; assert.ifError(err); - request(nconf.get('url') + '/api/flags/' + data.flagId, { jar: moderatorJar, json: true }, function (err, res, body) { + request(nconf.get('url') + '/api/flags/' + flagId, { jar: moderatorJar, json: true }, function (err, res, body) { assert.ifError(err); assert(body); assert.equal(body.reporter.username, 'regular'); diff --git a/test/flags.js b/test/flags.js index b157ba26c0..95ac38a3fa 100644 --- a/test/flags.js +++ b/test/flags.js @@ -11,42 +11,33 @@ var Posts = require('../src/posts'); var User = require('../src/user'); var Groups = require('../src/groups'); var Meta = require('../src/meta'); +var Privileges = require('../src/privileges'); describe('Flags', function () { - before(function (done) { + let uid1; + let adminUid; + let uid3; + let category; + before(async () => { // Create some stuff to flag - async.waterfall([ - async.apply(User.create, { username: 'testUser', password: 'abcdef', email: 'b@c.com' }), - function (uid, next) { - Categories.create({ - name: 'test category', - }, function (err, category) { - if (err) { - return done(err); - } + uid1 = await User.create({ username: 'testUser', password: 'abcdef', email: 'b@c.com' }); - Topics.post({ - cid: category.cid, - uid: uid, - title: 'Topic to flag', - content: 'This is flaggable content', - }, next); - }); - }, - function (topicData, next) { - User.create({ - username: 'testUser2', password: 'abcdef', email: 'c@d.com', - }, next); - }, - function (uid, next) { - Groups.join('administrators', uid, next); - }, - function (next) { - User.create({ - username: 'unprivileged', password: 'abcdef', email: 'd@e.com', - }, next); - }, - ], done); + adminUid = await User.create({ username: 'testUser2', password: 'abcdef', email: 'c@d.com' }); + await Groups.join('administrators', adminUid); + + category = await Categories.create({ + name: 'test category', + }); + await Topics.post({ + cid: category.cid, + uid: uid1, + title: 'Topic to flag', + content: 'This is flaggable content', + }); + + uid3 = await User.create({ + username: 'unprivileged', password: 'abcdef', email: 'd@e.com', + }); }); describe('.create()', function () { @@ -274,9 +265,9 @@ describe('Flags', function () { describe('.update()', function () { it('should alter a flag\'s various attributes and persist them to the database', function (done) { - Flags.update(1, 1, { + Flags.update(1, adminUid, { state: 'wip', - assignee: 1, + assignee: adminUid, }, function (err) { assert.ifError(err); db.getObjectFields('flag:1', ['state', 'assignee'], function (err, data) { @@ -286,7 +277,7 @@ describe('Flags', function () { assert.strictEqual('wip', data.state); assert.ok(!isNaN(parseInt(data.assignee, 10))); - assert.strictEqual(1, parseInt(data.assignee, 10)); + assert.strictEqual(adminUid, parseInt(data.assignee, 10)); done(); }); }); @@ -313,6 +304,65 @@ describe('Flags', function () { done(); }); }); + + it('should allow assignment if user is an admin and do nothing otherwise', async () => { + await Flags.update(1, adminUid, { + assignee: adminUid, + }); + let assignee = await db.getObjectField('flag:1', 'assignee'); + assert.strictEqual(adminUid, parseInt(assignee, 10)); + + await Flags.update(1, adminUid, { + assignee: uid3, + }); + assignee = await db.getObjectField('flag:1', 'assignee'); + assert.strictEqual(adminUid, parseInt(assignee, 10)); + }); + + it('should allow assignment if user is a global mod and do nothing otherwise', async () => { + await Groups.join('Global Moderators', uid3); + + await Flags.update(1, uid3, { + assignee: uid3, + }); + let assignee = await db.getObjectField('flag:1', 'assignee'); + assert.strictEqual(uid3, parseInt(assignee, 10)); + + await Flags.update(1, uid3, { + assignee: uid1, + }); + assignee = await db.getObjectField('flag:1', 'assignee'); + assert.strictEqual(uid3, parseInt(assignee, 10)); + + await Groups.leave('Global Moderators', uid3); + }); + + it('should allow assignment if user is a mod of the category, do nothing otherwise', async () => { + await Groups.join('cid:' + category.cid + ':privileges:moderate', uid3); + + await Flags.update(1, uid3, { + assignee: uid3, + }); + let assignee = await db.getObjectField('flag:1', 'assignee'); + assert.strictEqual(uid3, parseInt(assignee, 10)); + + await Flags.update(1, uid3, { + assignee: uid1, + }); + assignee = await db.getObjectField('flag:1', 'assignee'); + assert.strictEqual(uid3, parseInt(assignee, 10)); + + await Groups.leave('cid:' + category.cid + ':privileges:moderate', uid3); + }); + + it('should do nothing when you attempt to set a bogus state', async () => { + await Flags.update(1, adminUid, { + state: 'hocus pocus', + }); + + const state = await db.getObjectField('flag:1', 'state'); + assert.strictEqual('wip', state); + }); }); describe('.getTarget()', function () { @@ -546,9 +596,7 @@ describe('Flags', function () { describe('(websockets)', function () { var SocketFlags = require('../src/socket.io/flags.js'); - var tid; var pid; - var flag; before(function (done) { Topics.post({ @@ -557,7 +605,6 @@ describe('Flags', function () { title: 'Another topic', content: 'This is flaggable content', }, function (err, topic) { - tid = topic.postData.tid; pid = topic.postData.pid; done(err); @@ -570,8 +617,7 @@ describe('Flags', function () { type: 'post', id: pid, reason: 'foobar', - }, function (err, flagObj) { - flag = flagObj; + }, function (err) { assert.ifError(err); Flags.exists('post', pid, 1, function (err, exists) { @@ -581,6 +627,23 @@ describe('Flags', function () { }); }); }); + + it('should not allow flagging post in private category', async function () { + const category = await Categories.create({ name: 'private category' }); + + await Privileges.categories.rescind(['topics:read'], category.cid, 'registered-users'); + const result = await Topics.post({ + cid: category.cid, + uid: adminUid, + title: 'private topic', + content: 'private post', + }); + try { + await SocketFlags.create({ uid: uid3 }, { type: 'post', id: result.postData.pid, reason: 'foobar' }); + } catch (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + } + }); }); describe('.update()', function () { diff --git a/test/groups.js b/test/groups.js index 986509e863..ee4f91f565 100644 --- a/test/groups.js +++ b/test/groups.js @@ -9,6 +9,8 @@ var db = require('./mocks/databasemock'); var helpers = require('./helpers'); var Groups = require('../src/groups'); var User = require('../src/user'); +var socketGroups = require('../src/socket.io/groups'); +var meta = require('../src/meta'); describe('Groups', function () { var adminUid; @@ -360,6 +362,31 @@ describe('Groups', function () { }); }); + it('should fail if group name is invalid', function (done) { + Groups.create({ name: ['array/'] }, function (err) { + assert.equal(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should fail if group name is invalid', function (done) { + socketGroups.create({ uid: adminUid }, { name: ['test', 'administrators'] }, function (err) { + assert.equal(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should not create a system group', function (done) { + socketGroups.create({ uid: adminUid }, { name: 'mysystemgroup', system: true }, function (err) { + assert.ifError(err); + Groups.getGroupData('mysystemgroup', function (err, data) { + assert.ifError(err); + assert.strictEqual(data.system, 0); + done(); + }); + }); + }); + it('should fail if group name is invalid', function (done) { Groups.create({ name: 'not:valid' }, function (err) { assert.equal(err.message, '[[error:invalid-group-name]]'); @@ -444,6 +471,62 @@ describe('Groups', function () { }); }); + it('should fail to rename if group name is invalid', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: ['updateTestGroup?'], values: {} }, function (err) { + assert.strictEqual(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should fail to rename if group name is too short', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: 'updateTestGroup?', values: { name: '' } }, function (err) { + assert.strictEqual(err.message, '[[error:group-name-too-short]]'); + done(); + }); + }); + + it('should fail to rename if group name is invalid', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: 'updateTestGroup?', values: { name: ['invalid'] } }, function (err) { + assert.strictEqual(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should fail to rename if group name is invalid', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: 'updateTestGroup?', values: { name: 'cid:0:privileges:ban' } }, function (err) { + assert.strictEqual(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should fail to rename if group name is too long', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: 'updateTestGroup?', values: { name: 'verylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstringverylongstring' } }, function (err) { + assert.strictEqual(err.message, '[[error:group-name-too-long]]'); + done(); + }); + }); + + it('should fail to rename if group name is invalid', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: 'updateTestGroup?', values: { name: 'test:test' } }, function (err) { + assert.strictEqual(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should fail to rename if group name is invalid', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: 'updateTestGroup?', values: { name: 'another/test' } }, function (err) { + assert.strictEqual(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + + it('should fail to rename if group name is invalid', function (done) { + socketGroups.update({ uid: adminUid }, { groupName: 'updateTestGroup?', values: { name: '---' } }, function (err) { + assert.strictEqual(err.message, '[[error:invalid-group-name]]'); + done(); + }); + }); + it('should fail to rename group to an existing group', function (done) { Groups.create({ name: 'group2', @@ -538,6 +621,20 @@ describe('Groups', function () { }); }); + it('should fail to add user to admin group', async function () { + const oldValue = meta.config.allowPrivateGroups; + try { + meta.config.allowPrivateGroups = false; + const newUid = await User.create({ username: 'newadmin' }); + await socketGroups.join({ uid: newUid }, { groupName: ['test', 'administrators'], uid: newUid }, 1); + const isMember = await Groups.isMember(newUid, 'administrators'); + assert(!isMember); + } catch (err) { + assert.strictEqual(err.message, '[[error:invalid-group-name]]'); + } + meta.config.allowPrivateGroups = oldValue; + }); + it('should fail to add user to group if group name is invalid', function (done) { Groups.join(0, 1, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); @@ -667,11 +764,7 @@ describe('Groups', function () { }); }); - describe('socket methods', function () { - var socketGroups = require('../src/socket.io/groups'); - var meta = require('../src/meta'); - it('should error if data is null', function (done) { socketGroups.before({ uid: 0 }, 'groups.join', null, function (err) { assert.equal(err.message, '[[error:invalid-data]]'); @@ -1012,7 +1105,7 @@ describe('Groups', function () { }); it('should fail to create group if group creation is disabled', function (done) { - socketGroups.create({ uid: testUid }, {}, function (err) { + socketGroups.create({ uid: testUid }, { name: 'avalidname' }, function (err) { assert.equal(err.message, '[[error:no-privileges]]'); done(); }); @@ -1296,7 +1389,7 @@ describe('Groups', function () { it('should fail if user is not logged in or not owner', function (done) { socketGroups.cover.update({ uid: 0 }, {}, function (err) { assert.equal(err.message, '[[error:no-privileges]]'); - socketGroups.cover.update({ uid: regularUid }, {}, function (err) { + socketGroups.cover.update({ uid: regularUid }, { groupName: 'Test' }, function (err) { assert.equal(err.message, '[[error:no-privileges]]'); done(); }); diff --git a/test/messaging.js b/test/messaging.js index c49c6d6ee5..cc2b101676 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -116,10 +116,13 @@ describe('Messaging Library', function () { it('should send a user-join system message when a chat room is created', (done) => { socketModules.chats.getMessages({ uid: fooUid }, { uid: fooUid, roomId: roomId, start: 0 }, function (err, messages) { assert.ifError(err); - assert.equal(messages.length, 1); + assert.equal(messages.length, 2); assert.strictEqual(messages[0].system, true); assert.strictEqual(messages[0].content, 'user-join'); - done(); + socketModules.chats.edit({ uid: fooUid }, { roomId: roomId, mid: messages[0].messageId, message: 'test' }, function (err) { + assert.equal(err.message, '[[error:cant-edit-chat-message]]'); + done(); + }); }); }); @@ -147,6 +150,19 @@ describe('Messaging Library', function () { }); }); + it('should get users in room', async function () { + const data = await socketModules.chats.getUsersInRoom({ uid: fooUid }, { roomId: roomId }); + assert(Array.isArray(data) && data.length === 3); + }); + + it('should throw error if user is not in room', async function () { + try { + const data = await socketModules.chats.getUsersInRoom({ uid: 123123123 }, { roomId: roomId }); + } catch (err) { + assert.equal(err.message, '[[error:no-privileges]]'); + } + }); + it('should fail to add users to room if max is reached', function (done) { meta.config.maximumUsersInChatRoom = 2; socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'test' }, function (err) { @@ -198,7 +214,7 @@ describe('Messaging Library', function () { it('should send a user-leave system message when a user leaves the chat room', (done) => { socketModules.chats.getMessages({ uid: fooUid }, { uid: fooUid, roomId: roomId, start: 0 }, function (err, messages) { assert.ifError(err); - assert.equal(messages.length, 3); + assert.equal(messages.length, 4); const message = messages.pop(); assert.strictEqual(message.system, true); assert.strictEqual(message.content, 'user-leave'); @@ -206,6 +222,15 @@ describe('Messaging Library', function () { }); }); + it('should send not a user-leave system message when a user tries to leave a room they are not in', async () => { + await socketModules.chats.leave({ uid: bazUid }, roomId); + const messages = await socketModules.chats.getMessages({ uid: fooUid }, { uid: fooUid, roomId: roomId, start: 0 }); + assert.equal(messages.length, 4); + const message = messages.pop(); + assert.strictEqual(message.system, true); + assert.strictEqual(message.content, 'user-leave'); + }); + it('should change owner when owner leaves room', function (done) { socketModules.chats.newRoom({ uid: herpUid }, { touid: fooUid }, function (err, roomId) { assert.ifError(err); @@ -351,7 +376,7 @@ describe('Messaging Library', function () { myRoomId = _roomId; assert.ifError(err); assert(myRoomId); - socketModules.chats.getRaw({ uid: bazUid }, { mid: 2 }, function (err) { + socketModules.chats.getRaw({ uid: bazUid }, { mid: 200 }, function (err) { assert(err); assert.equal(err.message, '[[error:not-allowed]]'); socketModules.chats.send({ uid: bazUid }, { roomId: myRoomId, message: 'admin will see this' }, function (err, message) { @@ -594,14 +619,14 @@ describe('Messaging Library', function () { }); it('should fail to edit message if new content is empty string', function (done) { - socketModules.chats.edit({ uid: fooUid }, { mid: 5, roomId: roomId, message: ' ' }, function (err) { + socketModules.chats.edit({ uid: fooUid }, { mid: mid, roomId: roomId, message: ' ' }, function (err) { assert.equal(err.message, '[[error:invalid-chat-message]]'); done(); }); }); it('should fail to edit message if not own message', function (done) { - socketModules.chats.edit({ uid: herpUid }, { mid: 5, roomId: roomId, message: 'message edited' }, function (err) { + socketModules.chats.edit({ uid: herpUid }, { mid: mid, roomId: roomId, message: 'message edited' }, function (err) { assert.equal(err.message, '[[error:cant-edit-chat-message]]'); done(); }); diff --git a/test/mocks/databasemock.js b/test/mocks/databasemock.js index 4e26093aa8..2ec2fc7921 100644 --- a/test/mocks/databasemock.js +++ b/test/mocks/databasemock.js @@ -182,7 +182,7 @@ async function setupMockDefaults() { const rimraf = util.promisify(require('rimraf')); await rimraf('test/uploads'); - const mkdirp = util.promisify(require('mkdirp')); + const mkdirp = require('mkdirp'); const folders = [ 'test/uploads', diff --git a/test/plugins.js b/test/plugins.js index e01d74ed18..a029d06c06 100644 --- a/test/plugins.js +++ b/test/plugins.js @@ -120,7 +120,7 @@ describe('Plugins', function () { }); }); - it('should register and fire a static hook returning a promise that gets rejected with a warning only', function (done) { + it('should register and fire a static hook returning a promise that gets rejected with a error', function (done) { function method(data) { assert.equal(data.bar, 'test'); return new Promise(function (resolve, reject) { @@ -129,7 +129,8 @@ describe('Plugins', function () { } plugins.registerHook('test-plugin', { hook: 'static:test.hook', method: method }); plugins.fireHook('static:test.hook', { bar: 'test' }, function (err) { - assert.ifError(err); + assert.strictEqual(err.message, 'just because'); + plugins.unregisterHook('test-plugin', 'static:test.hook', method); done(); }); }); @@ -144,6 +145,7 @@ describe('Plugins', function () { plugins.registerHook('test-plugin', { hook: 'static:test.hook', method: method }); plugins.fireHook('static:test.hook', { bar: 'test' }, function (err) { assert.ifError(err); + plugins.unregisterHook('test-plugin', 'static:test.hook', method); done(); }); }); diff --git a/test/posts.js b/test/posts.js index 48e9df4200..dc82563463 100644 --- a/test/posts.js +++ b/test/posts.js @@ -134,6 +134,22 @@ describe('Post\'s', function () { assert.strictEqual(await topics.isOwner(postResult.topicData.tid, newUid), true); }); + it('should fail to change owner if new owner does not exist', async function () { + try { + await posts.changeOwner([1], '9999999'); + } catch (err) { + assert.strictEqual(err.message, '[[error:no-user]]'); + } + }); + + it('should fail to change owner if user is not authorized', async function () { + try { + await socketPosts.changeOwner({ uid: voterUid }, { pids: [1, 2], toUid: voterUid }); + } catch (err) { + assert.strictEqual(err.message, '[[error:no-privileges]]'); + } + }); + it('should return falsy if post does not exist', function (done) { posts.getPostData(9999, function (err, postData) { assert.ifError(err); diff --git a/test/topics.js b/test/topics.js index 3ba4fc2de4..c91395407b 100644 --- a/test/topics.js +++ b/test/topics.js @@ -23,9 +23,11 @@ describe('Topic\'s', function () { var categoryObj; var adminUid; var adminJar; + var fooUid; before(async function () { adminUid = await User.create({ username: 'admin', password: '123456' }); + fooUid = await User.create({ username: 'foo' }); await groups.join('administrators', adminUid); adminJar = await helpers.loginUser('admin', '123456'); @@ -572,6 +574,21 @@ describe('Topic\'s', function () { }); }); }); + + it('should not allow user to restore their topic if it was deleted by an admin', async function () { + const result = await topics.post({ + uid: fooUid, + title: 'topic for restore test', + content: 'topic content', + cid: categoryObj.cid, + }); + await socketTopics.delete({ uid: adminUid }, { tids: [result.topicData.tid], cid: categoryObj.cid }); + try { + await socketTopics.restore({ uid: fooUid }, { tids: [result.topicData.tid], cid: categoryObj.cid }); + } catch (err) { + assert.strictEqual(err.message, '[[error:no-privileges]]'); + } + }); }); describe('order pinned topics', function () { @@ -918,6 +935,16 @@ describe('Topic\'s', function () { }); }); + it('should load topic api data', function (done) { + request(nconf.get('url') + '/api/topic/' + topicData.slug, { json: true }, function (err, response, body) { + assert.ifError(err); + assert.equal(response.statusCode, 200); + assert.strictEqual(body._header.tags.meta.find(t => t.name === 'description').content, 'topic content'); + assert.strictEqual(body._header.tags.meta.find(t => t.property === 'og:description').content, 'topic content'); + done(); + }); + }); + it('should 404 if post index is invalid', function (done) { request(nconf.get('url') + '/topic/' + topicData.slug + '/derp', function (err, response) { assert.ifError(err); diff --git a/test/uploads.js b/test/uploads.js index 44ea53cdcd..ff303a20d3 100644 --- a/test/uploads.js +++ b/test/uploads.js @@ -48,9 +48,12 @@ describe('Upload Controllers', function () { cid = results.category.cid; topics.post({ uid: adminUid, title: 'test topic title', content: 'test topic content', cid: results.category.cid }, function (err, result) { + if (err) { + return done(err); + } tid = result.topicData.tid; pid = result.postData.pid; - done(err); + groups.join('administrators', adminUid, done); }); }); }); @@ -107,6 +110,20 @@ describe('Upload Controllers', function () { }); }); + it('should not allow deleting if path is not correct', function (done) { + socketUser.deleteUpload({ uid: adminUid }, { uid: regularUid, name: '../../bkconfig.json' }, function (err) { + assert.equal(err.message, '[[error:invalid-path]]'); + done(); + }); + }); + + it('should not allow deleting if path is not correct', function (done) { + socketUser.deleteUpload({ uid: adminUid }, { uid: regularUid, name: '/files/../../bkconfig.json' }, function (err) { + assert.equal(err.message, '[[error:invalid-path]]'); + done(); + }); + }); + it('should resize and upload an image to a post', function (done) { var oldValue = meta.config.resizeImageWidth; meta.config.resizeImageWidth = 10; @@ -288,7 +305,7 @@ describe('Upload Controllers', function () { assert.ifError(err); jar = _jar; csrf_token = _csrf_token; - groups.join('administrators', adminUid, done); + done(); }); }); diff --git a/test/user.js b/test/user.js index c06660ac63..66654cd649 100644 --- a/test/user.js +++ b/test/user.js @@ -1519,7 +1519,7 @@ describe('User', function () { it('should save user settings', function (done) { var data = { - uid: 1, + uid: testUid, settings: { bootswatchSkin: 'default', homePageRoute: 'none', @@ -1550,6 +1550,21 @@ describe('User', function () { }); }); + it('should error if language is invalid', function (done) { + var data = { + uid: testUid, + settings: { + userLang: '', + topicsPerPage: '10', + postsPerPage: '5', + }, + }; + socketUser.saveSettings({ uid: testUid }, data, function (err) { + assert.equal(err.message, '[[error:invalid-language]]'); + done(); + }); + }); + it('should set moderation note', function (done) { var adminUid; async.waterfall([ @@ -2149,10 +2164,20 @@ describe('User', function () { }); }); - it('should return offline if user is guest', function (done) { - var status = User.getStatus({ uid: 0 }); - assert.strictEqual(status, 'offline'); - done(); + describe('status/online', function () { + it('should return offline if user is guest', function (done) { + var status = User.getStatus({ uid: 0 }); + assert.strictEqual(status, 'offline'); + done(); + }); + + it('should return offline if user is guest', async function () { + assert.strictEqual(await User.isOnline(0), false); + }); + + it('should return true', async function () { + assert.strictEqual(await User.isOnline(testUid), true); + }); }); describe('isPrivilegedOrSelf', function () { @@ -2191,4 +2216,17 @@ describe('User', function () { done(); }); }); + + it('should allow user to login even if password is weak', function (done) { + User.create({ username: 'weakpwd', password: '123456' }, function (err) { + assert.ifError(err); + const oldValue = meta.config.minimumPasswordStrength; + meta.config.minimumPasswordStrength = 3; + helpers.loginUser('weakpwd', '123456', function (err, jar, csrfs_token) { + assert.ifError(err); + meta.config.minimumPasswordStrength = oldValue; + done(); + }); + }); + }); });